From 8159ca7c9795f6e2d6f543ab7c4a82b0bb205d79 Mon Sep 17 00:00:00 2001 From: "guangli.bao" Date: Mon, 15 Sep 2025 12:07:08 +0800 Subject: [PATCH 01/57] first draft Signed-off-by: guangli.bao --- docs/assets/sample-output1.png | Bin 0 -> 331830 bytes docs/assets/sample-output2.png | Bin 0 -> 305629 bytes docs/assets/sample-output3.png | Bin 0 -> 190330 bytes docs/examples/practice_on_vllm_simulator.md | 117 ++++++++++++++++++++ 4 files changed, 117 insertions(+) create mode 100644 docs/assets/sample-output1.png create mode 100644 docs/assets/sample-output2.png create mode 100644 docs/assets/sample-output3.png create mode 100644 docs/examples/practice_on_vllm_simulator.md diff --git a/docs/assets/sample-output1.png b/docs/assets/sample-output1.png new file mode 100644 index 0000000000000000000000000000000000000000..526d34759323919b65110a2b438a6342233bd245 GIT binary patch literal 331830 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

~M8B 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 e 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

VG%eP9O5k)a})P0tH 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

R^L;;P~ZXUmu3OZH&3N_w?TZpv5Ev1=O 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
Zui- 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’zY0S;+T|6j-I=zzOX0m7s9DCdeZCFp<*0N% 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

&_C4j7%?^oCN>;#T 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%0DT^-hmsufac*;2P@n{G~j}}oP(a+|huMIXe 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

    $bOs1z3E1BCdJ?lvb$2x zb&!<~$}@*7)UPv04AYwNoGHl;zG!H~VnYTkmY(yGaz1Fwvohj%ca+goGi5{T||vnr^l_?e6ca5UBbN`5S_n0j?(TeVl$W=Buxg#G4tn~9f)yLmpc-`wl_70#?; zC66n*V}!e$Rte3_&$=>`S$OFL7v`6JL3rMV&%Upp^`cBFPYi&E)d9kn$vgZcHsRZSn3Ji^Nad0Ct%sB3 z{qqNB22LT=LLO((&D!1#@+WRZy9%ZMJSX)189h9fLvXANJ+%mAa3Al8qk76)GI^}% zj^I+}$vsu0^f*HSD>tyhMnjbz?-g3j#3<93i;5CsPL!i_T`tiF1 zM-2BRL-j*RL*v zS5T5sts_wk76K=E#GH@w;S?wS(z+73kNJ^Ca#}iGLNXgQz<=N#4v|gQD3UB zT`8TTqUo)tXbk$+n~Ju^{?e)eu38h3kCg^XB&v;Lmb{GL`uwM&{7s+1$12fzAO{YP z*-0_R2Qc&&fo6;77+_suG?6%Y0S8? zlWwK~LE^a?)3^O#w)8W* zWDC?60?b{Eo=$c7t@5tYB_xzB$5VR0hC+kgr;A5Vdk$W_7$7dSKPWROF|YVS-IyjD zdL*g0*yU8|9nnL+%v5}YHvqm}$(a9Jl|70-Spb^K>8KC3{+#QZ^Ix!lJFR>KbJ!mq zbDu8hcx6zmy|huXJ!zovWnl}OqI$WSK^2AdDcfo2E0GBgZzGY9_}iBnv`2iJEb%Bq z{Qmym8vTlEdhd&KlZ$)qPzB0&Lk;Jz?0b%ia{+?%_*w!}soJBcs^)Dn?1+j>GPJ$& z>F19*oC_HG@(F^XRrScpB8Det)(ALsUeQ#&DS>Op>@$|O0X7zhCd>GPR=oMFtSu32 z$wqv+3oXZNCmV;Se%lS}c_0w$2GML>J+1ekWdTFgAq$wtqwMqdCY2d>Mt9?P`7(+N ziQ}{pd@5W~oQ2qYTn-^VM?H@d9E9SFi!v8ZzwEDL(0)gFKdQ6j(RD^utguv@B-X8i z(50ofbwBO>=yYEI5+^;w;*Z84i|wDX*Mq1j{kqm)`(=)D!^?6=%U<@+VuC^8depM&c>htUysU;;olP$+cGNNC&+#MdyVPLa+WxjK ziAW`}DlWmU*lVSP0C5i7Lo&=8lSt3OaKAh<=Ix?OX(&bEUHvHVC7aa2K`HFvaXv5~lZt_L>7X`cXn&NoSQ`dPDGZ#LisliDBaQbqB@K8a8BDlHcKK;T& z%(?$sJ<>^hCayvy6P1^pV`n_5DBcUN{Ru6t?kM{`IC#b5AN-JQ-ShB z;8?oe?3;k7A$)Fr(ydh(+|3a|`C2tk_VSIHm&dVjOW6}}N?W*H(EeO5iha`UR6KzK zhWz~;WoO-7i8BQvE)Jrqbyd-W_6^COmq*XNyzpXTj$d$0TYV;7##5b}c;o$)Wx-j< zey5B4qP?Ed#K`#`WN?=9jKg>};8dWYkQ9_mcS)SY(*~c%@HIJ+tz2Nae-jbq_DwA! z{O+3X!1S%BBiH}lO_IG9YPS|XX7V8SqEVq#7{t?pPl zg@1l8kDKrHW$xR3>?|q?0=N&Ze)`dKBQXw}6h?DhO4r>QvFJ$q4@NT8lw{0H**2*9 zK~sx^6|dPdRTVYyZJ#ttu2xg~bQOelnhlb>9pB-@(Uk(==}pjwN5Vk%pH)5|h&zCC zpbyFvb*5PG8$_FqK}%i;qKc{-%H<#tB3w@$;zkVK{SfM3S|eqi-*)T;yk-Yz!bADQ z?t}=gYBLise*NYr#)T_d1?S8z4(Tz+~qj_I3)mPqrqmP*FeUm1n4Da0=tMFE4LU-VHhNCRjCvhkz6lr%VK#{T&^MRNv%k z6~2*J>4PttHm$YAem#4BH$4He=%YjN2YH70TSNoYYV}mj-xOT z!`^o|@p?xTBZuWxj7y8eZwb|S$9Ssrl!zslPW({nC~x4TTn%t9o6ZA~>nI<_!0&8P;`!Am;Scp0xP_~vbGPitA9c#iTh5V@V zfEEs@_PrDkX)qyk(T-c{x|Bb;oKj}&8|ZwWIvtf1eXne;)W;W3?V<}87ZL7e$ITxz zcN7r7ZB@C#7Ps;9o~I|>d2&VU>x>D4jo8VW@QkH(B1{Yqui|9D_lyDJcYSW?%Ws#f zS*Zl{hli882L7}IoM=v`@!Z2T7puJ-RAW(L^Ql6i=Y7SK8ntNWRXWu77dIt_kFb;W zdZv`&re)8s+WOA*_?MmgL$i6qZ*u)69*C~5f1Q+3D7RF8)NjA^g^Qo8 zfT_n`Af-m#mw%!1movA)Ofv?J34WQH8jWZlYNaO0(0LU6K>SBG*eN_32nG88Gi3HU zkn?;bEo-hrhuEK4HQJpjbsVVo=|EceAKSKwi8ijB6)F}j*N$%?EHgDK>_!^v+O0;T zGza&|+{vSQPkcO@)n!*CS$e&1E}mFzy@Tm~72`Vg$gZn3iCINu(&1bKBdka9s}leH zZmZV*5KaBMR~$x$bKNlEa}@6vWz*ZB-ayYA3GVA8TYw*2g-sTLBA0A{(tj)^Bp6Nj z=WcbDxf>!$Ab~YIyNpX9iSOzD!HFHE*VtTu>R<$J#J+82nonKoDcY3`<^c)yCcMU4ygSW)%GS41G~G1 z_yyiJ(UD@&u$k95F!l7pLwH=BkuMlc6qIh%I|*m_-4uav;w=|ANY@JI63TgP1V5w>Q`eGTv-3+tLmf z6(I5Vu-=h>dQ^vKQ3(aRmFr6j+UV%q=J~YmehK+1-DFg9F!zM*;!>Y*ll<9r`%8|t zXfwY~3)IO-D%7%sFo+);8Aep<^R9qoSBK*>HUTrUsFx4B*OssVi`N-uK676ZXYa>* zA#iM2*zUFk`JS+6v{p8KMruCE&Q^G+D5vYg{*#gy0rI^-ju8p&PB$yEoE&l3Yu>MJ z^kCv$ybxEO|D9$WlsPfWc^<*`Pp7J(nrlXdfZ8XD7U>1$qXss{^_iwnIw{qh55r3<7Kpgoo5M(uUZtTzsZ0 zfXsCvAu+K!yjsIBZzQX0gmZ1|4?z;Vy@Oa)usuAK>QC8JR1~%}YISiqDQSVnm} zz3~}Kii{E)ayK=0fcQ}w$Iu6@kQ%Bm5gay#+S`MzQd?v-8dm8KjUD$0H|LpX1>1hk zZHI~{$t|T;=O$CwZ>++Dva)?eg7Ca@HtyP#v5{q}`asm0vgQ-HrxI~^>KYHtDdhiT z(y8}*SJ#SA*Zz*Y7eLls=rD^uU6~GTYBmIe9CGl*esNqhrTOy-rstK3nf2Gcj=Xt; zw!T1lxGhjbv__k?9Hs=q#Yjd9eDKu9d|8J^nJN|u1ZMA2@$HZ2hh~>8$2W~uCZ^W# zyC7UL1<@F#gEqO1ji1tBr!w;sosc>-KAWvSe*>G_FIk@d%7hAkbr{h8wVyM)pcuo> z`?RlNyBH%Dgk86JrxYbP@DaOdCwv*1CT^w#sk4_~dzFIZiqjq++s_;3xEn?mS zNk^0fIe51`tQ|byDVeLo<<5wMEO&zmZ4js8qw7J=tHomM_p$lX=62W7Tgjr0Wn*cf z91Cfu4lf<<2g-G?r+PvL$45e6zt^C(;vKZCR8Upf+s*^2SwL;#$?C3Hr%@CoQX7*w zt;6S}5ijX?Jf1!hd;KYvirfo_ACfIB90n#67cAf1`!K{&BVtI+k*Dn-`Ock6&(^Uv z?X#G|%L2D=bh|Ha(y=4b0fo{xKv9AIcz>zRL~wHQU3OI50&`{;y`y|)bJlQRipHYcn=NEk04h5eU`58zXKaO?nsB--M)ItQDx=@ zVNZ@3J0v|dbzU6}(zQYhk=RvVD8(W1!Ab=}nYRQ2J48iQ*naCd_YO}reA??xS8Z!} zNJ^cTw*f-QWBKO3*{>wT>ram>Bn3{wucel0fbu~t+#dEBO;}9ma@HR!2jj>-oQHjG z8-(7Aot0{gW9$iEVXZVjl)KI>OlicVWIu&tiv;#o^8+5-X@P>X=DO=P%> z0*c}AuFE%aj93Yu+YadV9hFFkRhMKOsp7XtK(QJZlBd7lqyr+NUMVzeMX09S5kF#Q zE@IOX#pmEiS7c`+ty;Bcx!_hfpM6qc_Y#Q*jnwNT=%D6uWApMbaqH-$Fy7FwC%^mk z_M?-PX$)LSj&Wo1fl33)y92AZJC_6B$hYsQ}&b9Oyp{p8J?*$TKfkK`NS|&RRQbOdYPQ>3{B3iB&%@{m2s6#g0kZ zyc8=v&HKlj29Rz-IEBPWhD;551J;#Ib0RPp9H<4OdT+Bv+lZh@ZU zUbbp%5!(f?Z7c|`FNWeV_!f+jmJ$;(yghz_$Sm$Em~kH!Vs?+aDNl2Gd3kMHy8q8X zp49zydzPuX?>)4pccew?`RFMIq^fQNWU)J4=aQ4dwRo=n&h%iQIyWUqM^ zB1S?QF0_$jw%F0>t66TeJn%Cn#z^(yYb^qY@=UHzuyu}^hEjgzhyEFAs%_j)d*KE+ z46dQ!j}Z(WGtXk*$^QUqfK*Z

    D_NS;t$alj-?%gi;nlcCiQ~!8PmuFG*9~vcZ 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

    >}&UF~c4dDD<+)u}95AF?2P4v6^HyDT6fJXk{# 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 X_J@c2{~rJV|NjF3P@u7{S3n8?q;DzF diff --git a/src/guidellm/data/utils.py b/src/guidellm/data/utils.py new file mode 100644 index 00000000..7d53a054 --- /dev/null +++ b/src/guidellm/data/utils.py @@ -0,0 +1,161 @@ +from __future__ import annotations + +import contextlib +import math +from collections.abc import Iterator +from typing import Any, Literal + +from datasets import Dataset, DatasetDict, IterableDataset, IterableDatasetDict + +__all__ = [ + "DEFAULT_COLUMN_NAMES", + "DEFAULT_SPLITS", + "datasets_item_iterator", + "resolve_dataset_split", +] + + +DEFAULT_SPLITS: dict[Literal["train", "calib", "val", "test"], list[str]] = { + "train": [ + "train", + "training", + "train_set", + "training_set", + "train_dataset", + "training_dataset", + "train_data", + "training_data", + "pretrain", + "pretrain_set", + "pretrain_dataset", + "pretrain_data", + "pretraining", + ], + "calib": [ + "calibration", + "calib", + "cal", + "calibration_set", + "calib_set", + "cal_set", + "calibration_dataset", + "calib_dataset", + "cal_set", + "calibration_data", + "calib_data", + "cal_data", + ], + "val": [ + "validation", + "val", + "valid", + "validation_set", + "val_set", + "validation_dataset", + "val_dataset", + "validation_data", + "val_data", + "dev", + "dev_set", + "dev_dataset", + "dev_data", + ], + "test": [ + "test", + "testing", + "test_set", + "testing_set", + "test_dataset", + "testing_dataset", + "test_data", + "testing_data", + "eval", + "eval_set", + "eval_dataset", + "eval_data", + ], +} + + +DEFAULT_COLUMN_NAMES: dict[str, list[str]] = { + "prompt_tokens_count": ["prompt_tokens_count", "input_tokens_count"], + "output_tokens_count": ["output_tokens_count", "completion_tokens_count"], + "text_column": [ + "prompt", + "instruction", + "question", + "input", + "context", + "content", + "conversation", + "turn", + "text", + ], + "image_column": [ + "image", + "picture", + "photo", + "img", + ], + "video_column": [ + "video", + "clip", + "movie", + "footage", + "mp4", + "mov", + "avi", + ], + "audio_column": [ + "audio", + "sound", + "voice", + "speech", + "wav", + "mp3", + ], +} + + +def resolve_dataset_split( + dataset: Dataset | IterableDataset | DatasetDict | IterableDatasetDict, + split: str | None, +) -> Dataset | IterableDataset: + if split is not None and isinstance(dataset, (DatasetDict, IterableDatasetDict)): + if split in dataset: + return dataset[split] + + raise ValueError(f"Requested split '{split}' not found in dataset: {dataset}.") + elif split is not None: + raise ValueError( + f"Requested split '{split}' but dataset has no splits: {dataset}." + ) + + if isinstance(dataset, (Dataset, IterableDataset)): + return dataset + + for _, default_splits in DEFAULT_SPLITS.items(): + for default_split in default_splits: + if default_split in dataset: + return dataset[default_split] + + return dataset[list(dataset.keys())[0]] + + +def datasets_item_iterator( + datasets: list[Dataset | IterableDataset], + data_samples: int, +) -> Iterator[dict[Literal["items"], tuple[dict[str, Any]]]]: + dataset_iters = [iter(dataset) for dataset in datasets] + gen_count = 0 + + with contextlib.suppress(StopIteration): + while gen_count < data_samples or data_samples <= 0 or data_samples == math.inf: + yield {"items": tuple(next(dataset_iter) for dataset_iter in dataset_iters)} + gen_count += 1 + + if gen_count < data_samples and data_samples > 0 and data_samples != math.inf: + raise ValueError( + f"Requested {data_samples} samples, but only {gen_count} available " + "from the provided datasets." + ) diff --git a/src/guidellm/dataset/__init__.py b/src/guidellm/dataset/__init__.py deleted file mode 100644 index b90b72ff..00000000 --- a/src/guidellm/dataset/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -from .creator import ColumnInputTypes, DatasetCreator -from .entrypoints import load_dataset -from .file import FileDatasetCreator -from .hf_datasets import HFDatasetsCreator -from .in_memory import InMemoryDatasetCreator -from .synthetic import ( - SyntheticDatasetConfig, - SyntheticDatasetCreator, - SyntheticTextItemsGenerator, -) - -__all__ = [ - "ColumnInputTypes", - "DatasetCreator", - "FileDatasetCreator", - "HFDatasetsCreator", - "InMemoryDatasetCreator", - "SyntheticDatasetConfig", - "SyntheticDatasetCreator", - "SyntheticTextItemsGenerator", - "load_dataset", -] diff --git a/src/guidellm/dataset/creator.py b/src/guidellm/dataset/creator.py deleted file mode 100644 index a74ec8c0..00000000 --- a/src/guidellm/dataset/creator.py +++ /dev/null @@ -1,213 +0,0 @@ -from abc import ABC, abstractmethod -from pathlib import Path -from typing import Any, Literal, Optional, Union - -from datasets import Dataset, DatasetDict, IterableDataset, IterableDatasetDict -from transformers import PreTrainedTokenizerBase # type: ignore[import] - -__all__ = ["ColumnInputTypes", "DatasetCreator"] - -ColumnInputTypes = Literal[ - "prompt_column", - "text_column", - "prompt_tokens_count_column", - "output_tokens_count_column", -] - - -class DatasetCreator(ABC): - DEFAULT_SPLITS_TRAIN = [ - "train", - "training", - "train_set", - "training_set", - "train_dataset", - "training_dataset", - "train_data", - "training_data", - "pretrain", - "pretrain_set", - "pretrain_dataset", - "pretrain_data", - "pretraining", - ] - DEFAULT_SPLITS_CALIB = [ - "calibration", - "calib", - "cal", - "calibration_set", - "calib_set", - "cal_set", - "calibration_dataset", - "calib_dataset", - "cal_set", - "calibration_data", - "calib_data", - "cal_data", - ] - DEFAULT_SPLITS_VAL = [ - "validation", - "val", - "valid", - "validation_set", - "val_set", - "validation_dataset", - "val_dataset", - "validation_data", - "val_data", - "dev", - "dev_set", - "dev_dataset", - "dev_data", - ] - DEFAULT_SPLITS_TEST = [ - "test", - "testing", - "test_set", - "testing_set", - "test_dataset", - "testing_dataset", - "test_data", - "testing_data", - "eval", - "eval_set", - "eval_dataset", - "eval_data", - ] - DEFAULT_SPLITS_DATASET: dict[str, str] = {} - - @classmethod - def create( - cls, - data: Any, - data_args: Optional[dict[str, Any]], - processor: Optional[Union[str, Path, PreTrainedTokenizerBase]], - processor_args: Optional[dict[str, Any]], - random_seed: int = 42, - split_pref_order: Optional[list[str]] = None, - ) -> tuple[Union[Dataset, IterableDataset], dict[ColumnInputTypes, str]]: - if not cls.is_supported(data, data_args): - raise ValueError(f"Unsupported data type: {type(data)} given for {data}. ") - - split = cls.extract_args_split(data_args) - column_mappings = cls.extract_args_column_mappings(data_args) - dataset = cls.handle_create( - data, data_args, processor, processor_args, random_seed - ) - - if isinstance(dataset, (DatasetDict, IterableDatasetDict)): - dataset = cls.extract_dataset_split(dataset, split, split_pref_order) - - if not isinstance(dataset, (Dataset, IterableDataset)): - raise ValueError( - f"Unsupported data type: {type(dataset)} given for {dataset}." - ) - - return dataset, column_mappings - - @classmethod - def extract_args_split(cls, data_args: Optional[dict[str, Any]]) -> str: - split = "auto" - - if data_args and "split" in data_args: - split = data_args["split"] - del data_args["split"] - - return split - - @classmethod - def extract_args_column_mappings( - cls, - data_args: Optional[dict[str, Any]], - ) -> dict[ColumnInputTypes, str]: - columns: dict[ColumnInputTypes, str] = {} - - if data_args: - if "prompt_column" in data_args: - columns["prompt_column"] = data_args["prompt_column"] - del data_args["prompt_column"] - - if "prompt_tokens_count_column" in data_args: - columns["prompt_tokens_count_column"] = data_args[ - "prompt_tokens_count_column" - ] - del data_args["prompt_tokens_count_column"] - - if "output_tokens_count_column" in data_args: - columns["output_tokens_count_column"] = data_args[ - "output_tokens_count_column" - ] - del data_args["output_tokens_count_column"] - - return columns - - @classmethod - def extract_dataset_name( - cls, dataset: Union[Dataset, IterableDataset, DatasetDict, IterableDatasetDict] - ) -> Optional[str]: - if isinstance(dataset, (DatasetDict, IterableDatasetDict)): - dataset = dataset[list(dataset.keys())[0]] - - if isinstance(dataset, (Dataset, IterableDataset)): - if not hasattr(dataset, "info") or not hasattr( - dataset.info, "dataset_name" - ): - return None - - return dataset.info.dataset_name - - raise ValueError(f"Unsupported data type: {type(dataset)} given for {dataset}.") - - @classmethod - def extract_dataset_split( - cls, - dataset: Union[DatasetDict, IterableDatasetDict], - specified_split: Union[Literal["auto"], str] = "auto", - split_pref_order: Optional[Union[Literal["auto"], list[str]]] = "auto", - ) -> Union[Dataset, IterableDataset]: - if not isinstance(dataset, (DatasetDict, IterableDatasetDict)): - raise ValueError( - f"Unsupported data type: {type(dataset)} given for {dataset}." - ) - - if specified_split != "auto": - if specified_split not in dataset: - raise ValueError( - f"Split {specified_split} not found in dataset {dataset}." - ) - - return dataset[specified_split] - - dataset_name = cls.extract_dataset_name(dataset) - - if dataset_name and dataset_name in cls.DEFAULT_SPLITS_DATASET: - return dataset[cls.DEFAULT_SPLITS_DATASET[dataset_name]] - - if split_pref_order == "auto": - split_pref_order = [ - *cls.DEFAULT_SPLITS_TEST, - *cls.DEFAULT_SPLITS_VAL, - *cls.DEFAULT_SPLITS_CALIB, - *cls.DEFAULT_SPLITS_TRAIN, - ] - - for test_split in split_pref_order or []: - if test_split in dataset: - return dataset[test_split] - - return dataset[list(dataset.keys())[0]] - - @classmethod - @abstractmethod - def is_supported(cls, data: Any, data_args: Optional[dict[str, Any]]) -> bool: ... - - @classmethod - @abstractmethod - def handle_create( - cls, - data: Any, - data_args: Optional[dict[str, Any]], - processor: Optional[Union[str, Path, PreTrainedTokenizerBase]], - processor_args: Optional[dict[str, Any]], - random_seed: int, - ) -> Union[Dataset, DatasetDict, IterableDataset, IterableDatasetDict]: ... diff --git a/src/guidellm/dataset/entrypoints.py b/src/guidellm/dataset/entrypoints.py deleted file mode 100644 index cf689956..00000000 --- a/src/guidellm/dataset/entrypoints.py +++ /dev/null @@ -1,42 +0,0 @@ -from pathlib import Path -from typing import Any, Optional, Union - -from datasets import Dataset, IterableDataset -from transformers import PreTrainedTokenizerBase # type: ignore[import] - -from guidellm.dataset.creator import ColumnInputTypes -from guidellm.dataset.file import FileDatasetCreator -from guidellm.dataset.hf_datasets import HFDatasetsCreator -from guidellm.dataset.in_memory import InMemoryDatasetCreator -from guidellm.dataset.synthetic import SyntheticDatasetCreator - -__all__ = ["load_dataset"] - - -def load_dataset( - data: Any, - data_args: Optional[dict[str, Any]], - processor: Optional[Union[str, Path, PreTrainedTokenizerBase]], - processor_args: Optional[dict[str, Any]], - random_seed: int = 42, - split_pref_order: Optional[list[str]] = None, -) -> tuple[Union[Dataset, IterableDataset], dict[ColumnInputTypes, str]]: - creators = [ - InMemoryDatasetCreator, - SyntheticDatasetCreator, - FileDatasetCreator, - HFDatasetsCreator, - ] - - for creator in creators: - if creator.is_supported(data, data_args): - return creator.create( - data, - data_args, - processor, - processor_args, - random_seed, - split_pref_order, - ) - - raise ValueError(f"Unsupported data type: {type(data)} given for {data}. ") diff --git a/src/guidellm/dataset/file.py b/src/guidellm/dataset/file.py deleted file mode 100644 index 5d6df1d9..00000000 --- a/src/guidellm/dataset/file.py +++ /dev/null @@ -1,92 +0,0 @@ -from pathlib import Path -from typing import Any, Optional, Union - -import pandas as pd # type: ignore[import] -from datasets import ( - Dataset, - DatasetDict, - IterableDataset, - IterableDatasetDict, - load_dataset, -) -from transformers import PreTrainedTokenizerBase # type: ignore[import] - -from guidellm.dataset.creator import DatasetCreator - -__all__ = ["FileDatasetCreator"] - - -class FileDatasetCreator(DatasetCreator): - SUPPORTED_TYPES = { - ".txt", - ".text", - ".csv", - ".json", - ".jsonl", - ".parquet", - ".arrow", - ".hdf5", - ".tar", - } - - @classmethod - def is_supported(cls, data: Any, data_args: Optional[dict[str, Any]]) -> bool: # noqa: ARG003 - if isinstance(data, (str, Path)) and (path := Path(data)).exists(): - # local folder or py file, assume supported - return path.suffix.lower() in cls.SUPPORTED_TYPES - - return False - - @classmethod - def handle_create( - cls, - data: Any, - data_args: Optional[dict[str, Any]], - processor: Optional[Union[str, Path, PreTrainedTokenizerBase]], # noqa: ARG003 - processor_args: Optional[dict[str, Any]], # noqa: ARG003 - random_seed: int, # noqa: ARG003 - ) -> Union[Dataset, DatasetDict, IterableDataset, IterableDatasetDict]: - if not isinstance(data, (str, Path)): - raise ValueError(f"Unsupported data type: {type(data)} given for {data}. ") - - path = Path(data) - if not path.exists(): - raise FileNotFoundError(f"File not found: {path}") - - if not path.is_file(): - raise ValueError(f"Unsupported data type: {path} given for {path}. ") - - if path.suffix.lower() not in cls.SUPPORTED_TYPES: - raise ValueError(f"Unsupported file type: {path.suffix} given for {path}. ") - - return cls.load_dataset(path, data_args) - - @classmethod - def load_dataset( - cls, path: Path, data_args: Optional[dict[str, Any]] - ) -> Union[Dataset, IterableDataset]: - if path.suffix.lower() in {".txt", ".text"}: - with path.open("r") as file: - items = file.readlines() - - dataset = Dataset.from_dict({"text": items}, **(data_args or {})) - elif path.suffix.lower() == ".csv": - dataset = load_dataset("csv", data_files=str(path), **(data_args or {})) - elif path.suffix.lower() in {".json", ".jsonl"}: - dataset = load_dataset("json", data_files=str(path), **(data_args or {})) - elif path.suffix.lower() == ".parquet": - dataset = load_dataset("parquet", data_files=str(path), **(data_args or {})) - elif path.suffix.lower() == ".arrow": - dataset = load_dataset("arrow", data_files=str(path), **(data_args or {})) - elif path.suffix.lower() == ".hdf5": - dataset = Dataset.from_pandas(pd.read_hdf(str(path)), **(data_args or {})) - elif path.suffix.lower() == ".db": - dataset = Dataset.from_sql(con=str(path), **(data_args or {})) - elif path.suffix.lower() == ".tar": - dataset = load_dataset( - "webdataset", data_files=str(path), **(data_args or {}) - ) - else: - raise ValueError(f"Unsupported file type: {path.suffix} given for {path}. ") - - return dataset diff --git a/src/guidellm/dataset/hf_datasets.py b/src/guidellm/dataset/hf_datasets.py deleted file mode 100644 index 7f91facd..00000000 --- a/src/guidellm/dataset/hf_datasets.py +++ /dev/null @@ -1,62 +0,0 @@ -from pathlib import Path -from typing import Any, Optional, Union - -from datasets import ( - Dataset, - DatasetDict, - IterableDataset, - IterableDatasetDict, - get_dataset_config_info, - load_dataset, -) -from transformers import PreTrainedTokenizerBase # type: ignore[import] - -from guidellm.dataset.creator import DatasetCreator - -__all__ = ["HFDatasetsCreator"] - - -class HFDatasetsCreator(DatasetCreator): - @classmethod - def is_supported(cls, data: Any, data_args: Optional[dict[str, Any]]) -> bool: # noqa: ARG003 - if isinstance( - data, (Dataset, DatasetDict, IterableDataset, IterableDatasetDict) - ): - # base type is supported - return True - - if isinstance(data, (str, Path)) and (path := Path(data)).exists(): - # local folder or py file, assume supported - return path.is_dir() or path.suffix == ".py" - - if isinstance(data, (str, Path)): - try: - # try to load dataset - return get_dataset_config_info(data) is not None - except Exception: # noqa: BLE001, S110 - pass - - return False - - @classmethod - def handle_create( - cls, - data: Any, - data_args: Optional[dict[str, Any]], - processor: Optional[Union[str, Path, PreTrainedTokenizerBase]], # noqa: ARG003 - processor_args: Optional[dict[str, Any]], # noqa: ARG003 - random_seed: int, # noqa: ARG003 - ) -> Union[Dataset, DatasetDict, IterableDataset, IterableDatasetDict]: - if isinstance(data, (str, Path)): - data = load_dataset(data, **(data_args or {})) - elif data_args: - raise ValueError( - f"data_args should not be provided when data is a {type(data)}" - ) - - if isinstance( - data, (Dataset, DatasetDict, IterableDataset, IterableDatasetDict) - ): - return data - - raise ValueError(f"Unsupported data type: {type(data)} given for {data}. ") diff --git a/src/guidellm/dataset/in_memory.py b/src/guidellm/dataset/in_memory.py deleted file mode 100644 index af84f658..00000000 --- a/src/guidellm/dataset/in_memory.py +++ /dev/null @@ -1,132 +0,0 @@ -from collections.abc import Iterable -from pathlib import Path -from typing import Any, Optional, Union - -from datasets import ( - Dataset, - DatasetDict, - IterableDataset, - IterableDatasetDict, -) -from transformers import PreTrainedTokenizerBase # type: ignore[import] - -from guidellm.dataset.creator import DatasetCreator - -__all__ = ["InMemoryDatasetCreator"] - - -class InMemoryDatasetCreator(DatasetCreator): - @classmethod - def is_supported(cls, data: Any, data_args: Optional[dict[str, Any]]) -> bool: # noqa: ARG003 - return isinstance(data, Iterable) and not isinstance(data, str) - - @classmethod - def handle_create( - cls, - data: Any, - data_args: Optional[dict[str, Any]], - processor: Optional[Union[str, Path, PreTrainedTokenizerBase]], # noqa: ARG003 - processor_args: Optional[dict[str, Any]], # noqa: ARG003 - random_seed: int, # noqa: ARG003 - ) -> Union[Dataset, DatasetDict, IterableDataset, IterableDatasetDict]: - if not isinstance(data, Iterable): - raise TypeError( - f"Unsupported data format. Expected Iterable[Any], got {type(data)}" - ) - - if not data: - raise ValueError("Data is empty") - - if isinstance(data, dict): - # assume data is a dictionary of columns and values: {"c1": ["i1", "i2"]} - data_dict = cls.format_data_dict(data) - elif isinstance(data[0], dict): # type: ignore[index] - # assume data is a list of dictionaries: [{"c1": "i1"}, {"c1": "i2"}] - data_dict = cls.format_data_iterable_dicts(data) - else: - # assume data is a list of items with no columns: ["i1", "i2"] - data_dict = cls.format_data_iterable_values(data) - - return Dataset.from_dict(data_dict, **(data_args or {})) - - @classmethod - def format_data_dict(cls, data: dict[Any, Any]) -> dict[str, Any]: - if not isinstance(data, dict): - raise TypeError( - f"Unsupported data format. Expected Dict[str, Iterable[Any]], " - f"got {type(data)}" - ) - - if not all( - isinstance(key, str) and isinstance(val, Iterable) - for key, val in data.items() - ): - raise TypeError( - "Unsupported data format. Expected Dict[str, Iterable[Any]], " - f"got {type(data)}" - ) - - samples = len(list(data.values())[0]) - if not all(len(val) == samples for val in data.values()): - raise ValueError( - "Unsupported data format. Not all columns have the same number samples " - f"for {data}" - ) - - return data - - @classmethod - def format_data_iterable_dicts( - cls, data: Iterable[dict[Any, Any]] - ) -> dict[str, Any]: - if not isinstance(data, Iterable): - raise TypeError( - f"Unsupported data format. Expected Iterable[Dict[str, Any]], " - f"got {type(data)}" - ) - - if not all(isinstance(item, dict) for item in data): - raise TypeError( - f"Unsupported data format. Expected Iterable[Dict[str, Any]], " - f"got {type(data)}" - ) - - if not all(isinstance(key, str) for key in data[0]): # type: ignore[index] - raise TypeError( - "Unsupported data format. Expected Dict[str, Any], " - f"but one of the items had a non string column for {data}" - ) - - columns = list(data[0].keys()) # type: ignore[index] - if not all( - len(item) == len(columns) and all(key in item for key in columns) - for item in data - ): - raise ValueError( - "Unsupported data format. Not all items have the same columns " - f"for {data}" - ) - - data_dict: dict[str, Any] = {key: [] for key in columns} - for item in data: - for key, value in item.items(): - data_dict[key].append(value) - - return data_dict - - @classmethod - def format_data_iterable_values(cls, data: Iterable[Any]) -> dict[str, Any]: - if not isinstance(data, Iterable): - raise TypeError( - f"Unsupported data format. Expected Iterable[Iterable[Any]], " - f"got {type(data)}" - ) - - first_item = next(iter(data), None) - first_type = type(first_item) - if not all(isinstance(item, first_type) for item in data): - raise TypeError( - f"Unsupported data format. Not all types are the same for {data}" - ) - - return {"data": list(data)} diff --git a/src/guidellm/dataset/synthetic.py b/src/guidellm/dataset/synthetic.py deleted file mode 100644 index 8c30f0f7..00000000 --- a/src/guidellm/dataset/synthetic.py +++ /dev/null @@ -1,287 +0,0 @@ -import json -import random -from collections.abc import Iterable, Iterator -from itertools import cycle -from pathlib import Path -from typing import Any, Literal, Optional, Union - -import yaml -from datasets import ( - Dataset, - DatasetDict, - IterableDataset, - IterableDatasetDict, -) -from pydantic import BaseModel, Field -from transformers import PreTrainedTokenizerBase # type: ignore[import] - -from guidellm.dataset.creator import ColumnInputTypes, DatasetCreator -from guidellm.utils import EndlessTextCreator, IntegerRangeSampler, check_load_processor - -__all__ = [ - "SyntheticDatasetConfig", - "SyntheticDatasetCreator", - "SyntheticTextItemsGenerator", -] - - -class SyntheticDatasetConfig(BaseModel): - prefix_tokens: int = Field( - description="The number of shared prefix tokens to prepend to each prompt.", - ge=0, - default=0, - ) - prompt_tokens: int = Field( - description="The average number of text tokens generated for prompts.", - gt=0, - ) - prompt_tokens_stdev: Optional[int] = Field( - description="The standard deviation of the tokens generated for prompts.", - gt=0, - default=None, - ) - prompt_tokens_min: Optional[int] = Field( - description="The minimum number of text tokens generated for prompts.", - gt=0, - default=None, - ) - prompt_tokens_max: Optional[int] = Field( - description="The maximum number of text tokens generated for prompts.", - gt=0, - default=None, - ) - output_tokens: int = Field( - description="The average number of text tokens generated for outputs.", - gt=0, - ) - output_tokens_stdev: Optional[int] = Field( - description="The standard deviation of the tokens generated for outputs.", - gt=0, - default=None, - ) - output_tokens_min: Optional[int] = Field( - description="The minimum number of text tokens generated for outputs.", - gt=0, - default=None, - ) - output_tokens_max: Optional[int] = Field( - description="The maximum number of text tokens generated for outputs.", - gt=0, - default=None, - ) - samples: int = Field( - description="The number of samples to generate for the dataset.", - gt=0, - default=1000, - ) - source: str = Field( - description="The source of the text data to be used for generation.", - default="data:prideandprejudice.txt.gz", - ) - - @staticmethod - def parse_str(data: Union[str, Path]) -> "SyntheticDatasetConfig": - if ( - isinstance(data, Path) - or data.strip().endswith(".config") - or data.strip().endswith(".yaml") - ): - return SyntheticDatasetConfig.parse_config_file(data) - - if data.strip().startswith("{"): - return SyntheticDatasetConfig.parse_json(data) - - if data.count("=") > 1: - return SyntheticDatasetConfig.parse_key_value_pairs(data) - - raise ValueError( - f"Unsupported data format. Expected JSON or key-value pairs, got {data}" - ) - - @staticmethod - def parse_json(data: str) -> "SyntheticDatasetConfig": - config_dict = json.loads(data.strip()) - - return SyntheticDatasetConfig(**config_dict) - - @staticmethod - def parse_key_value_pairs(data: str) -> "SyntheticDatasetConfig": - config_dict = {} - items = data.strip().split(",") - for item in items: - key, value = item.split("=") - config_dict[key.strip()] = ( - int(value.strip()) if value.strip().isnumeric() else value.strip() - ) - - return SyntheticDatasetConfig(**config_dict) # type: ignore[arg-type] - - @staticmethod - def parse_config_file(data: Union[str, Path]) -> "SyntheticDatasetConfig": - with Path(data).open("r") as file: - config_dict = yaml.safe_load(file) - - return SyntheticDatasetConfig(**config_dict) - - -class SyntheticTextItemsGenerator( - Iterable[ - dict[ - Literal["prompt", "prompt_tokens_count", "output_tokens_count"], - Union[str, int], - ] - ] -): - def __init__( - self, - config: SyntheticDatasetConfig, - processor: PreTrainedTokenizerBase, - random_seed: int, - ): - self.config = config - self.processor = processor - self.random_seed = random_seed - self.text_creator = EndlessTextCreator( - data=config.source, - ) - - def __iter__( - self, - ) -> Iterator[ - dict[ - Literal["prompt", "prompt_tokens_count", "output_tokens_count"], - Union[str, int], - ] - ]: - prompt_tokens_sampler = IntegerRangeSampler( - average=self.config.prompt_tokens, - variance=self.config.prompt_tokens_stdev, - min_value=self.config.prompt_tokens_min, - max_value=self.config.prompt_tokens_max, - random_seed=self.random_seed, - ) - output_tokens_sampler = IntegerRangeSampler( - average=self.config.output_tokens, - variance=self.config.output_tokens_stdev, - min_value=self.config.output_tokens_min, - max_value=self.config.output_tokens_max, - random_seed=self.random_seed + 1, # ensure diff dist from prompts - ) - # ensure diff distribution from output tokens - rand = random.Random(self.random_seed + 2) # noqa: S311 - unique_prefix_iter = cycle(self.processor.get_vocab().values()) - - prefix_index = rand.randint(0, len(self.text_creator.words)) - prefix_tokens = self._create_prompt(self.config.prefix_tokens, prefix_index) - - for _, prompt_tokens, output_tokens in zip( - range(self.config.samples), - prompt_tokens_sampler, - output_tokens_sampler, - ): - start_index = rand.randint(0, len(self.text_creator.words)) - prompt_text = self.processor.decode( - prefix_tokens - + self._create_prompt( - prompt_tokens, start_index, next(unique_prefix_iter) - ), - skip_special_tokens=True, - ) - yield { - "prompt": prompt_text, - "prompt_tokens_count": self.config.prefix_tokens + prompt_tokens, - "output_tokens_count": output_tokens, - } - - def _create_prompt( - self, prompt_tokens: int, start_index: int, unique_prefix: Optional[int] = None - ) -> list[int]: - if prompt_tokens <= 0: - return [] - - left = start_index - right = start_index + 4 * prompt_tokens - start_tokens = [unique_prefix] if unique_prefix else [] - - while left < right: - mid = (left + right) // 2 - test_prompt = self.text_creator.create_text(start_index, mid - start_index) - test_tokens = start_tokens + self.processor.encode(test_prompt) - - if len(test_tokens) == prompt_tokens: - return test_tokens - elif len(test_tokens) < prompt_tokens: - left = mid + 1 - else: - right = mid - - final_text = self.text_creator.create_text(start_index, left - start_index) - return start_tokens + self.processor.encode(final_text) - - -class SyntheticDatasetCreator(DatasetCreator): - @classmethod - def is_supported( - cls, - data: Any, - data_args: Optional[dict[str, Any]], # noqa: ARG003 - ) -> bool: - if ( - isinstance(data, Path) - and data.exists() - and data.suffix in {".config", ".yaml"} - ): - return True - - if isinstance(data, str): - data_str: str = data.strip() - if ( - data_str.startswith("{") - or data_str.count("=") > 1 - or data_str.endswith((".config", ".yaml")) - ): - return True - - return False - - @classmethod - def handle_create( - cls, - data: Any, - data_args: Optional[dict[str, Any]], - processor: Optional[Union[str, Path, PreTrainedTokenizerBase]], - processor_args: Optional[dict[str, Any]], - random_seed: int, - ) -> Union[Dataset, DatasetDict, IterableDataset, IterableDatasetDict]: - processor = check_load_processor( - processor, - processor_args, - error_msg=( - "Processor/tokenizer required for synthetic dataset generation." - ), - ) - - config = SyntheticDatasetConfig.parse_str(data) - generator = SyntheticTextItemsGenerator(config, processor, random_seed) - items = list(generator) - - return Dataset.from_list(items, **(data_args or {})) - - @classmethod - def extract_args_column_mappings( - cls, - data_args: Optional[dict[str, Any]], - ) -> dict[ColumnInputTypes, str]: - data_args_columns = super().extract_args_column_mappings(data_args) - - if data_args_columns: - raise ValueError( - f"Column mappings are not supported for synthetic datasets. " - f"Got {data_args_columns}" - ) - - return { - "prompt_column": "prompt", - "prompt_tokens_count_column": "prompt_tokens_count", - "output_tokens_count_column": "output_tokens_count", - } diff --git a/src/guidellm/logger.py b/src/guidellm/logger.py index 70259bad..da3464f9 100644 --- a/src/guidellm/logger.py +++ b/src/guidellm/logger.py @@ -72,7 +72,7 @@ def configure_logger(config: LoggingSettings = settings.logging): sys.stdout, level=config.console_log_level.upper(), format="{time:YY-MM-DD HH:mm:ss}|{level: <8} \ - |{name}:{function}:{line} - {message}" + |{name}:{function}:{line} - {message}", ) if config.log_file or config.log_file_level: diff --git a/src/guidellm/preprocess/dataset.py b/src/guidellm/preprocess/dataset.py index a94b8a14..9d65dcd6 100644 --- a/src/guidellm/preprocess/dataset.py +++ b/src/guidellm/preprocess/dataset.py @@ -11,7 +11,6 @@ from pydantic import BaseModel, Field from transformers import PreTrainedTokenizerBase -from guidellm.dataset import load_dataset as guidellm_load_dataset from guidellm.utils import IntegerRangeSampler, check_load_processor from guidellm.utils.hf_datasets import SUPPORTED_TYPES, save_dataset_to_file @@ -239,7 +238,7 @@ def process_dataset( prompt_tokens: Union[str, Path], output_tokens: Union[str, Path], processor_args: Optional[dict[str, Any]] = None, - data_args: Optional[dict[str, Any]] = None, + data_args: Optional[dict[str, Any]] = None, # noqa: ARG001 short_prompt_strategy: ShortPromptStrategy = ShortPromptStrategy.IGNORE, pad_char: Optional[str] = None, concat_delimiter: Optional[str] = None, @@ -271,9 +270,7 @@ def process_dataset( f"Starting dataset conversion | Input: {data} | Output directory: {output_path}" ) - dataset, column_mappings = guidellm_load_dataset( - data, data_args, processor, processor_args - ) + dataset, column_mappings = None, None tokenizer = check_load_processor( processor, processor_args, diff --git a/src/guidellm/request/__init__.py b/src/guidellm/request/__init__.py deleted file mode 100644 index 85b447d6..00000000 --- a/src/guidellm/request/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -from .loader import ( - GenerativeRequestLoader, - GenerativeRequestLoaderDescription, - RequestLoader, - RequestLoaderDescription, -) -from .request import GenerationRequest -from .types import RequestT, ResponseT - -__all__ = [ - "GenerationRequest", - "GenerativeRequestLoader", - "GenerativeRequestLoaderDescription", - "RequestLoader", - "RequestLoaderDescription", - "RequestT", - "ResponseT", -] diff --git a/src/guidellm/request/loader.py b/src/guidellm/request/loader.py deleted file mode 100644 index 607a7455..00000000 --- a/src/guidellm/request/loader.py +++ /dev/null @@ -1,284 +0,0 @@ -from abc import abstractmethod -from collections.abc import Iterable, Iterator -from pathlib import Path -from typing import ( - Any, - Literal, - Optional, - Union, -) - -from datasets import Dataset, DatasetDict, IterableDataset, IterableDatasetDict -from transformers import PreTrainedTokenizerBase # type: ignore[import] - -from guidellm.dataset import ColumnInputTypes, load_dataset -from guidellm.request.request import GenerationRequest -from guidellm.settings import settings -from guidellm.utils import StandardBaseModel - -__all__ = [ - "GenerativeRequestLoader", - "GenerativeRequestLoaderDescription", - "RequestLoader", - "RequestLoaderDescription", -] - - -class RequestLoaderDescription(StandardBaseModel): - type_: Literal["request_loader"] = "request_loader" - - -class RequestLoader(Iterable): - @abstractmethod - def __iter__(self) -> Iterator: ... - - @abstractmethod - def __len__(self) -> int: ... - - @property - @abstractmethod - def description(self) -> RequestLoaderDescription: ... - - -class GenerativeRequestLoaderDescription(RequestLoaderDescription): - type_: Literal["generative_request_loader"] = "generative_request_loader" # type: ignore[assignment] - data: str - data_args: Optional[dict[str, Any]] - processor: str - processor_args: Optional[dict[str, Any]] - - -class GenerativeRequestLoader(RequestLoader): - DEFAULT_PROMPT_COLUMNS = [ - "prompt", - "prompts", - "instruction", - "instructions", - "question", - "questions", - "input", - "inputs", - "context", - "content", - "conversation", - "conversations", - "turn", - "turns", - "text", - ] - - def __init__( - self, - data: Union[ - str, - Path, - Iterable[Union[str, dict[str, Any]]], - Dataset, - DatasetDict, - IterableDataset, - IterableDatasetDict, - ], - data_args: Optional[dict[str, Any]], - processor: Optional[Union[str, Path, PreTrainedTokenizerBase]], - processor_args: Optional[dict[str, Any]], - shuffle: bool = True, - iter_type: Literal["finite", "infinite"] = "finite", - random_seed: int = 42, - ): - self.data = data - self.data_args = data_args - dataset, args_column_mappings = load_dataset( - data, - data_args, - processor, - processor_args, - random_seed, - ) - self.dataset = dataset - self.processor = processor - self.processor_args = processor_args - self.shuffle = shuffle - self.iter_type = iter_type - self.random_seed = random_seed - - self.column_mappings = self._create_column_mappings(args_column_mappings) - self.preserve_iter_state = iter_type == "infinite" # ensure no caching requests - self._preserved_iter = None - - def __iter__(self) -> Iterator[GenerationRequest]: - scope_create_count = 0 - - while (dataset_iter := self._get_dataset_iter(scope_create_count)) is not None: - scope_create_count += 1 - - for item in dataset_iter: - yield self._create_request(item) - - self._preserved_iter = None - - def __len__(self) -> int: - if self.iter_type == "finite": - return self.num_unique_items() - - raise ValueError(f"Unable to determine length of dataset: {self.data}") - - @property - def description(self) -> GenerativeRequestLoaderDescription: - return GenerativeRequestLoaderDescription( - data=str(self.data), - data_args=self.data_args, - processor=str(self.processor), - processor_args=self.processor_args, - ) - - def num_unique_items(self, raise_err: bool = True) -> int: - try: - return len(self.dataset) - except Exception: # noqa: BLE001, S110 - pass - - dataset_size = self.dataset.info.dataset_size - if dataset_size is not None: - return dataset_size - - if raise_err: - raise ValueError("Unable to determine number of items in the dataset") - - return -1 - - def _create_column_mappings( - self, - args_column_mappings: dict[ColumnInputTypes, str], - ) -> dict[ColumnInputTypes, str]: - column_mappings: dict[ColumnInputTypes, str] = {} - - if "text_column" in args_column_mappings: - column_mappings["prompt_column"] = args_column_mappings["text_column"] - else: - column_mappings["prompt_column"] = self._extract_text_column() - - if "prompt_tokens_count_column" in args_column_mappings: - column_mappings["prompt_tokens_count_column"] = args_column_mappings[ - "prompt_tokens_count_column" - ] - elif prompt_tokens_count_column := self._extract_prompt_tokens_count_column(): - column_mappings["prompt_tokens_count_column"] = prompt_tokens_count_column - - if "output_tokens_count_column" in args_column_mappings: - column_mappings["output_tokens_count_column"] = args_column_mappings[ - "output_tokens_count_column" - ] - elif output_tokens_count_column := self._extract_output_tokens_count_column(): - column_mappings["output_tokens_count_column"] = output_tokens_count_column - - return column_mappings - - def _extract_text_column(self) -> str: - column_names = self._dataset_columns( - err_msg=( - "Unable to determine text column from dataset and it is required. " - "To specify the text column, set the 'text_column' key in the " - "'data_args' dictionary." - ) - ) - - if not column_names: - raise ValueError( - "Unable to determine text column from dataset and it is required. " - "To specify the text column, set the 'text_column' key in the " - "'data_args' dictionary." - ) - - if len(column_names) == 1: - return column_names[0] - - for def_column in self.DEFAULT_PROMPT_COLUMNS: - if def_column in column_names: - return def_column - - raise ValueError( - f"Unable to determine text column from dataset columns: {column_names}. " - "To specify the text column, set the 'text_column' key in the " - "'data_args' dictionary." - ) - - def _extract_prompt_tokens_count_column(self) -> Optional[str]: - column_names = self._dataset_columns() - - if column_names and "prompt_tokens_count" in column_names: - return "prompt_tokens_count" - - if column_names and "prompt_tokens" in column_names: - return "prompt_tokens" - - return None - - def _extract_output_tokens_count_column(self) -> Optional[str]: - column_names = self._dataset_columns() - - if column_names and "output_tokens_count" in column_names: - return "output_tokens_count" - - if column_names and "output_tokens" in column_names: - return "output_tokens" - - return None - - def _dataset_columns(self, err_msg: Optional[str] = None) -> Optional[list[str]]: - try: - column_names = self.dataset.column_names - - if not column_names and err_msg: - raise ValueError(f"No column names found in dataset: {self.data}") - except Exception as err: - if err_msg: - raise ValueError(err_msg) from err - - column_names = None - - return column_names - - def _get_dataset_iter( - self, scope_create_count: int - ) -> Optional[Iterator[dict[str, Any]]]: - if scope_create_count > 0 and self.iter_type != "infinite": - return None - - if self.preserve_iter_state and self._preserved_iter is not None: - return self._preserved_iter - - dataset = ( - self.dataset - if not self.shuffle - else self.dataset.shuffle(seed=self.random_seed) - ) - - dataset_iter = iter(dataset) - - if self.preserve_iter_state: - self._preserved_iter = dataset_iter - - return dataset_iter - - def _create_request(self, item: dict[str, Any]) -> GenerationRequest: - prompt_tokens = ( - item[self.column_mappings["prompt_tokens_count_column"]] - if "prompt_tokens_count_column" in self.column_mappings - else None - ) - output_tokens = ( - item[self.column_mappings["output_tokens_count_column"]] - if "output_tokens_count_column" in self.column_mappings - else None - ) - - return GenerationRequest( - request_type=settings.preferred_route, - content=item[self.column_mappings["prompt_column"]], - stats=( - {"prompt_tokens": prompt_tokens} if prompt_tokens is not None else {} - ), - constraints=( - {"output_tokens": output_tokens} if output_tokens is not None else {} - ), - ) diff --git a/src/guidellm/request/request.py b/src/guidellm/request/request.py deleted file mode 100644 index bf4e59fb..00000000 --- a/src/guidellm/request/request.py +++ /dev/null @@ -1,79 +0,0 @@ -import uuid -from typing import Any, Literal, Optional - -from pydantic import Field - -from guidellm.utils import StandardBaseModel - -__all__ = ["GenerationRequest"] - - -class GenerationRequest(StandardBaseModel): - """ - A class representing a request for generation. - This class is used to encapsulate the details of a generation request, - including the request ID, type, content, parameters, statistics, and constraints. - It is designed to be used with the BackendRequestsWorker class to handle - the generation process. - - :param request_id: The unique identifier for the request. - :param request_type: The type of request (e.g., text, chat). - :param content: The content for the request to send to the backend. - If request_type is 'text', this should be a string or list of strings - which will be resolved by backend.text_completions. - If request_type is 'chat', this should be a string, - a list of (str, Dict[str, Union[str, Dict[str, str]], Path, Image]), - or Any raw content which will be resolved by backend.chat_completions. - If raw content, raw_content=True must be passed in the params. - :param params: Additional parameters for the request passed in as kwargs. - For an http backend, these are passed into the body of the request. - :param stats: Statistics for the request, such as the number of prompt tokens. - Used for tracking and reporting purposes. - :param constraints: Constraints for the request, such as the maximum number - of output tokens. Used for controlling the behavior of the backend. - """ - - request_id: Optional[str] = Field( - default_factory=lambda: str(uuid.uuid4()), - description="The unique identifier for the request.", - ) - request_type: Literal["text_completions", "chat_completions"] = Field( - default="text_completions", - description=( - "The type of request (e.g., text, chat). " - "If request_type='text_completions', resolved by backend.text_completions. " - "If request_typ='chat_completions', resolved by backend.chat_completions." - ), - ) - content: Any = Field( - description=( - "The content for the request to send to the backend. " - "If request_type is 'text', this should be a string or list of strings " - "which will be resolved by backend.text_completions. " - "If request_type is 'chat', this should be a string, " - "a list of (str, Dict[str, Union[str, Dict[str, str]], Path, Image]), " - "or Any raw content which will be resolved by backend.chat_completions. " - "If raw content, raw_content=True must be passed in the params." - ) - ) - params: dict[str, Any] = Field( - default_factory=dict, - description=( - "Additional parameters for the request that will be passed in as kwargs. " - "For an http backend, these are passed into the body of the request. " - ), - ) - stats: dict[Literal["prompt_tokens"], int] = Field( - default_factory=dict, - description=( - "Statistics for the request, such as the number of prompt tokens. " - "Used for tracking and reporting purposes." - ), - ) - constraints: dict[Literal["output_tokens"], int] = Field( - default_factory=dict, - description=( - "Constraints for the request, such as the maximum number of output tokens. " - "Used for controlling the behavior of the backend." - ), - ) diff --git a/src/guidellm/request/types.py b/src/guidellm/request/types.py deleted file mode 100644 index f82493be..00000000 --- a/src/guidellm/request/types.py +++ /dev/null @@ -1,10 +0,0 @@ -from typing import TypeVar - -__all__ = [ - "RequestT", - "ResponseT", -] - - -RequestT = TypeVar("RequestT") -ResponseT = TypeVar("ResponseT") diff --git a/src/guidellm/scheduler/scheduler.py b/src/guidellm/scheduler/scheduler.py index e7d8b2c6..d9bb7c23 100644 --- a/src/guidellm/scheduler/scheduler.py +++ b/src/guidellm/scheduler/scheduler.py @@ -124,7 +124,7 @@ async def run( # Setup the worker group, sync start with the environment worker_group = WorkerProcessGroup[RequestT, ResponseT]( - requests=None, + requests=local_requests, cycle_requests=local_requests, backend=backend, strategy=local_strategy, diff --git a/src/guidellm/scheduler/worker_group.py b/src/guidellm/scheduler/worker_group.py index c1d516f1..e64d64fc 100644 --- a/src/guidellm/scheduler/worker_group.py +++ b/src/guidellm/scheduler/worker_group.py @@ -98,10 +98,9 @@ def __init__( :raises ValueError: If neither requests nor cycle_requests are provided, or if cycle_requests is an Iterator rather than Iterable """ - if not requests and not cycle_requests: + if requests is None and cycle_requests is None: raise ValueError( "At least one of 'requests' or 'cycle_requests' must be provided. " - f"Got requests: {requests}, cycle_requests: {cycle_requests}" ) if isinstance(cycle_requests, Iterator): @@ -487,10 +486,10 @@ def requests_generator( """ def _iter(): - if requests: + if requests is not None: yield from requests - if cycle_requests: + if cycle_requests is not None: while True: yield from cycle_requests @@ -512,6 +511,8 @@ def _iter(): scheduler_start_time=self.start_time, ) state_update = self._locked_update(request_info) + request_info.scheduler_timings.queued = time.time() + yield (request, request_info) if state_update.stop_queueing: From b1b1b78d47f81dac8c14acf153bc027f665441d5 Mon Sep 17 00:00:00 2001 From: Benjamin Blue Date: Wed, 1 Oct 2025 13:14:47 -0400 Subject: [PATCH 05/57] update default build values to use versioned builds (#310) ## Summary With the default path referring to the versioned build now, users will no longer experience their html reports breaking randomly when the build files are updated. Also fixed versioned build directory path issue that I missed previously --------- Signed-off-by: dalthecow --- .github/workflows/release-candidate.yml | 2 +- .github/workflows/release.yml | 2 +- src/guidellm/config.py | 4 ++-- tests/unit/test_config.py | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index 703ca4c9..52425491 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -228,7 +228,7 @@ jobs: uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./ui/out + publish_dir: .src/ui/out destination_dir: ui/release/${TAG} keep_files: false user_name: ${{ github.actor }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9f3d9d75..7f71b633 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -227,7 +227,7 @@ jobs: uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./ui/out + publish_dir: ./src/ui/out destination_dir: ui/${TAG} keep_files: false user_name: ${{ github.actor }} diff --git a/src/guidellm/config.py b/src/guidellm/config.py index 72178425..d9dcef23 100644 --- a/src/guidellm/config.py +++ b/src/guidellm/config.py @@ -31,8 +31,8 @@ class Environment(str, Enum): ENV_REPORT_MAPPING = { - Environment.PROD: "https://blog.vllm.ai/guidellm/ui/latest/index.html", - Environment.STAGING: "https://blog.vllm.ai/guidellm/ui/release/latest/index.html", + Environment.PROD: "https://blog.vllm.ai/guidellm/ui/v0.3.0/index.html", + Environment.STAGING: "https://blog.vllm.ai/guidellm/ui/release/v0.3.0/index.html", Environment.DEV: "https://blog.vllm.ai/guidellm/ui/dev/index.html", Environment.LOCAL: "http://localhost:3000/index.html", } diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index f5d9415c..89dae5ce 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -21,7 +21,7 @@ def test_default_settings(): assert settings.openai == OpenAISettings() assert ( settings.report_generation.source - == "https://blog.vllm.ai/guidellm/ui/latest/index.html" + == "https://blog.vllm.ai/guidellm/ui/v0.3.0/index.html" ) @@ -60,13 +60,13 @@ def test_report_generation_default_source(): settings = Settings(env=Environment.STAGING) assert ( settings.report_generation.source - == "https://blog.vllm.ai/guidellm/ui/release/latest/index.html" + == "https://blog.vllm.ai/guidellm/ui/release/v0.3.0/index.html" ) settings = Settings(env=Environment.PROD) assert ( settings.report_generation.source - == "https://blog.vllm.ai/guidellm/ui/latest/index.html" + == "https://blog.vllm.ai/guidellm/ui/v0.3.0/index.html" ) From 108a657f4fff75c5117557957898281b92ed0cf4 Mon Sep 17 00:00:00 2001 From: Benjamin Blue Date: Fri, 3 Oct 2025 10:35:32 -0400 Subject: [PATCH 06/57] update tpot to itl in labels and code use (#386) ## Summary We want to use ITL instead of TPOT. The data we had previously happened to be ITL data, but all of the labels indicate that it is TPOT data. Now the code and labels reflect that it is ITL data. ## Test Plan - Everything works, tests pass, No use of TPOT in the UI --------- Signed-off-by: dalthecow Co-authored-by: Samuel Monson --- src/guidellm/presentation/data_models.py | 4 +-- .../MetricsSummary.component.tsx | 30 +++++++++---------- .../components/MetricsSummary/useSummary.ts | 10 +++---- .../WorkloadMetrics.component.tsx | 28 ++++++++--------- src/ui/lib/store/benchmarksWindowData.ts | 20 ++++++------- src/ui/lib/store/mockData.ts | 2 +- .../store/slices/benchmarks/benchmarks.api.ts | 10 +++---- .../benchmarks/benchmarks.interfaces.ts | 2 +- .../slices/benchmarks/benchmarks.selectors.ts | 12 ++++---- .../store/slices/metrics/metrics.constants.ts | 2 +- .../slices/metrics/metrics.interfaces.ts | 2 +- src/ui/lib/store/slices/slo/slo.constants.ts | 4 +-- src/ui/lib/store/slices/slo/slo.interfaces.ts | 4 +-- tests/ui/unit/mocks/mockBenchmarks.ts | 4 +-- 14 files changed, 67 insertions(+), 67 deletions(-) diff --git a/src/guidellm/presentation/data_models.py b/src/guidellm/presentation/data_models.py index 989ca8ab..fefcf7d8 100644 --- a/src/guidellm/presentation/data_models.py +++ b/src/guidellm/presentation/data_models.py @@ -208,7 +208,7 @@ def from_distribution_summary( class BenchmarkDatum(BaseModel): requests_per_second: float - tpot: TabularDistributionSummary + itl: TabularDistributionSummary ttft: TabularDistributionSummary throughput: TabularDistributionSummary time_per_request: TabularDistributionSummary @@ -217,7 +217,7 @@ class BenchmarkDatum(BaseModel): def from_benchmark(cls, bm: "GenerativeBenchmark"): return cls( requests_per_second=bm.metrics.requests_per_second.successful.mean, - tpot=TabularDistributionSummary.from_distribution_summary( + itl=TabularDistributionSummary.from_distribution_summary( bm.metrics.inter_token_latency_ms.successful ), ttft=TabularDistributionSummary.from_distribution_summary( diff --git a/src/ui/lib/components/MetricsSummary/MetricsSummary.component.tsx b/src/ui/lib/components/MetricsSummary/MetricsSummary.component.tsx index 0d804f5c..8f5e3aed 100644 --- a/src/ui/lib/components/MetricsSummary/MetricsSummary.component.tsx +++ b/src/ui/lib/components/MetricsSummary/MetricsSummary.component.tsx @@ -54,7 +54,7 @@ export const Component = () => { const { ttft: ttftSLO, - tpot: tpotSLO, + itl: itlSLO, timePerRequest: timePerRequestSLO, throughput: throughputSLO, percentile, @@ -62,7 +62,7 @@ export const Component = () => { maxX, errors, handleTtft, - handleTpot, + handleItl, handleTimePerRequest, handleThroughput, handlePercentileChange, @@ -72,8 +72,8 @@ export const Component = () => { const isTtftMatch = Boolean( ttftSLO && interpolatedMetricData.ttft.enforcedPercentileValue <= ttftSLO ); - const isTpotMatch = Boolean( - tpotSLO && interpolatedMetricData.tpot.enforcedPercentileValue <= tpotSLO + const isItlMatch = Boolean( + itlSLO && interpolatedMetricData.itl.enforcedPercentileValue <= itlSLO ); const isTprMatch = Boolean( timePerRequestSLO && @@ -123,7 +123,7 @@ export const Component = () => { { @@ -212,7 +212,7 @@ export const Component = () => { { diff --git a/src/ui/lib/components/MetricsSummary/useSummary.ts b/src/ui/lib/components/MetricsSummary/useSummary.ts index 0a6f550c..3046fcb9 100644 --- a/src/ui/lib/components/MetricsSummary/useSummary.ts +++ b/src/ui/lib/components/MetricsSummary/useSummary.ts @@ -13,7 +13,7 @@ type Errors = { [key: string]: string | undefined }; const initErrorsState: Errors = { ttft: undefined, - tpot: undefined, + itl: undefined, timePerRequest: undefined, throughput: undefined, }; @@ -47,20 +47,20 @@ export const useSummary = () => { const dispatch = useDispatch(); const { current, enforcedPercentile, tasksDefaults } = useSelector(selectSloState); - const { ttft, tpot, timePerRequest, throughput } = useSelector( + const { ttft, itl, timePerRequest, throughput } = useSelector( selectMetricsSummaryLineData ); const [errors, setErrors] = useState(initErrorsState); const ttftLimits = findMinMax(ttft || []); - const tpotLimits = findMinMax(tpot || []); + const itlLimits = findMinMax(itl || []); const timePerRequestLimits = findMinMax(timePerRequest || []); const throughputLimits = findMinMax(throughput || []); const limitsByMetric = { ttft: ttftLimits, - tpot: tpotLimits, + itl: itlLimits, timePerRequest: timePerRequestLimits, throughput: throughputLimits, }; @@ -112,7 +112,7 @@ export const useSummary = () => { maxX: ttftLimits.maxX, errors, handleTtft: handleChange('ttft'), - handleTpot: handleChange('tpot'), + handleItl: handleChange('itl'), handleTimePerRequest: handleChange('timePerRequest'), handleThroughput: handleChange('throughput'), handlePercentileChange, diff --git a/src/ui/lib/components/WorkloadMetrics/WorkloadMetrics.component.tsx b/src/ui/lib/components/WorkloadMetrics/WorkloadMetrics.component.tsx index ac333982..e7e632ca 100644 --- a/src/ui/lib/components/WorkloadMetrics/WorkloadMetrics.component.tsx +++ b/src/ui/lib/components/WorkloadMetrics/WorkloadMetrics.component.tsx @@ -36,14 +36,14 @@ export const leftColumn3 = (rpsValue: number, value: number, units: string) => ( export const Component = () => { const { data } = useGetBenchmarksQuery(); - const { ttft, tpot, timePerRequest, throughput } = useSelector( + const { ttft, itl, timePerRequest, throughput } = useSelector( selectMetricsDetailsLineData ); const { currentRequestRate } = useSelector(selectSloState); const formattedRequestRate = formatNumber(currentRequestRate); const { ttft: ttftAtRPS, - tpot: tpotAtRPS, + itl: itlAtRPS, timePerRequest: timePerRequestAtRPS, throughput: throughputAtRPS, } = useSelector(selectInterpolatedMetrics); @@ -57,7 +57,7 @@ export const Component = () => { { )} rightColumn={columnContent(formattedRequestRate, ttftAtRPS.percentiles, 'ms')} > - + - + @@ -99,7 +99,7 @@ export const Component = () => { { 's' )} > - + diff --git a/src/ui/lib/store/benchmarksWindowData.ts b/src/ui/lib/store/benchmarksWindowData.ts index a589e8ed..87faf7bc 100644 --- a/src/ui/lib/store/benchmarksWindowData.ts +++ b/src/ui/lib/store/benchmarksWindowData.ts @@ -1,7 +1,7 @@ export const benchmarksScript = `window.benchmarks = [ { requestsPerSecond: 11.411616848282272, - tpot: { + itl: { mean: 8.758024845683707, median: 8.788176945277623, mode: 7.119315011160714, @@ -172,7 +172,7 @@ export const benchmarksScript = `window.benchmarks = [ }, { requestsPerSecond: 36.289181300710815, - tpot: { + itl: { mean: 588.0161376137819, median: 461.7137227739607, mode: 323.1611592429025, @@ -343,7 +343,7 @@ export const benchmarksScript = `window.benchmarks = [ }, { requestsPerSecond: 20.752070927855794, - tpot: { + itl: { mean: 116.28360712595156, median: 26.769569941929408, mode: 10.624987738473076, @@ -514,7 +514,7 @@ export const benchmarksScript = `window.benchmarks = [ }, { requestsPerSecond: 26.81917480361788, - tpot: { + itl: { mean: 299.7306064613554, median: 372.7384294782366, mode: 13.360295976911273, @@ -685,7 +685,7 @@ export const benchmarksScript = `window.benchmarks = [ }, { requestsPerSecond: 26.823988819498975, - tpot: { + itl: { mean: 683.8011571339198, median: 742.2689029148647, mode: 317.1694278717041, @@ -856,7 +856,7 @@ export const benchmarksScript = `window.benchmarks = [ }, { requestsPerSecond: 24.50047903792646, - tpot: { + itl: { mean: 742.9258901891964, median: 773.0941431862967, mode: 538.750410079956, @@ -1027,7 +1027,7 @@ export const benchmarksScript = `window.benchmarks = [ }, { requestsPerSecond: 25.617829792196602, - tpot: { + itl: { mean: 663.3098317044122, median: 613.7458937508719, mode: 440.9824098859514, @@ -1198,7 +1198,7 @@ export const benchmarksScript = `window.benchmarks = [ }, { requestsPerSecond: 37.02892550982192, - tpot: { + itl: { mean: 606.4144710877113, median: 543.5235500335693, mode: 331.6155501774379, @@ -1369,7 +1369,7 @@ export const benchmarksScript = `window.benchmarks = [ }, { requestsPerSecond: 37.29183354201869, - tpot: { + itl: { mean: 603.3237551205925, median: 528.1183038439069, mode: 400.96027510506764, @@ -1540,7 +1540,7 @@ export const benchmarksScript = `window.benchmarks = [ }, { requestsPerSecond: 37.45318312972309, - tpot: { + itl: { mean: 600.7204526769262, median: 626.2100083487375, mode: 398.7384523664202, diff --git a/src/ui/lib/store/mockData.ts b/src/ui/lib/store/mockData.ts index 8295c60c..2fcb4b8f 100644 --- a/src/ui/lib/store/mockData.ts +++ b/src/ui/lib/store/mockData.ts @@ -95,7 +95,7 @@ export const benchmarks = [ ], bucketWidth: 0, }, - tpot: { + itl: { statistics: { total: 0, mean: 0, diff --git a/src/ui/lib/store/slices/benchmarks/benchmarks.api.ts b/src/ui/lib/store/slices/benchmarks/benchmarks.api.ts index 838dbc7a..efddfc39 100644 --- a/src/ui/lib/store/slices/benchmarks/benchmarks.api.ts +++ b/src/ui/lib/store/slices/benchmarks/benchmarks.api.ts @@ -45,9 +45,9 @@ const setDefaultSLOs = ( lastBM?.ttft, defaultPercentile ); - const tpotAvg = getAverageValueForPercentile( - firstBM?.tpot, - lastBM?.tpot, + const itlAvg = getAverageValueForPercentile( + firstBM?.itl, + lastBM?.itl, defaultPercentile ); const timePerRequestAvg = getAverageValueForPercentile( @@ -66,13 +66,13 @@ const setDefaultSLOs = ( currentRequestRate: firstBM?.requestsPerSecond, current: { ttft: formatNumber(ttftAvg, 0), - tpot: formatNumber(tpotAvg, 0), + itl: formatNumber(itlAvg, 0), timePerRequest: formatNumber(timePerRequestAvg, 0), throughput: formatNumber(throughputAvg, 0), }, tasksDefaults: { ttft: formatNumber(ttftAvg, 0), - tpot: formatNumber(tpotAvg, 0), + itl: formatNumber(itlAvg, 0), timePerRequest: formatNumber(timePerRequestAvg, 0), throughput: formatNumber(throughputAvg, 0), }, diff --git a/src/ui/lib/store/slices/benchmarks/benchmarks.interfaces.ts b/src/ui/lib/store/slices/benchmarks/benchmarks.interfaces.ts index 602ae17e..2a5f319e 100644 --- a/src/ui/lib/store/slices/benchmarks/benchmarks.interfaces.ts +++ b/src/ui/lib/store/slices/benchmarks/benchmarks.interfaces.ts @@ -20,7 +20,7 @@ interface Percentile { export interface BenchmarkMetrics { ttft: Statistics; - tpot: Statistics; + itl: Statistics; timePerRequest: Statistics; throughput: Statistics; } diff --git a/src/ui/lib/store/slices/benchmarks/benchmarks.selectors.ts b/src/ui/lib/store/slices/benchmarks/benchmarks.selectors.ts index 53d54f40..9aa5fd81 100644 --- a/src/ui/lib/store/slices/benchmarks/benchmarks.selectors.ts +++ b/src/ui/lib/store/slices/benchmarks/benchmarks.selectors.ts @@ -21,13 +21,13 @@ export const selectMetricsSummaryLineData = createSelector( const lineData: { [K in keyof BenchmarkMetrics]: Point[] } = { ttft: [], - tpot: [], + itl: [], timePerRequest: [], throughput: [], }; const metrics: (keyof BenchmarkMetrics)[] = [ 'ttft', - 'tpot', + 'itl', 'timePerRequest', 'throughput', ]; @@ -66,7 +66,7 @@ export const selectInterpolatedMetrics = createSelector( }; } = { ttft: getDefaultMetricValues(), - tpot: getDefaultMetricValues(), + itl: getDefaultMetricValues(), timePerRequest: getDefaultMetricValues(), throughput: getDefaultMetricValues(), mean: getDefaultMetricValues(), @@ -81,7 +81,7 @@ export const selectInterpolatedMetrics = createSelector( const { enforcedPercentile, currentRequestRate } = sloState; const metrics: (keyof BenchmarkMetrics)[] = [ 'ttft', - 'tpot', + 'itl', 'timePerRequest', 'throughput', ]; @@ -137,13 +137,13 @@ export const selectMetricsDetailsLineData = createSelector( [K in keyof BenchmarkMetrics]: { data: Point[]; id: string; solid?: boolean }[]; } = { ttft: [], - tpot: [], + itl: [], timePerRequest: [], throughput: [], }; const props: (keyof BenchmarkMetrics)[] = [ 'ttft', - 'tpot', + 'itl', 'timePerRequest', 'throughput', ]; diff --git a/src/ui/lib/store/slices/metrics/metrics.constants.ts b/src/ui/lib/store/slices/metrics/metrics.constants.ts index a9ae8414..d61efac1 100644 --- a/src/ui/lib/store/slices/metrics/metrics.constants.ts +++ b/src/ui/lib/store/slices/metrics/metrics.constants.ts @@ -5,6 +5,6 @@ export const initialState: MetricsState = { currentRequestRate: 0, timePerRequest: { valuesByRps: {} }, ttft: { valuesByRps: {} }, - tpot: { valuesByRps: {} }, + itl: { valuesByRps: {} }, throughput: { valuesByRps: {} }, }; diff --git a/src/ui/lib/store/slices/metrics/metrics.interfaces.ts b/src/ui/lib/store/slices/metrics/metrics.interfaces.ts index b38dc98b..bd56018b 100644 --- a/src/ui/lib/store/slices/metrics/metrics.interfaces.ts +++ b/src/ui/lib/store/slices/metrics/metrics.interfaces.ts @@ -4,7 +4,7 @@ export interface MetricsState { currentRequestRate: number; timePerRequest: SingleMetricsState; ttft: SingleMetricsState; - tpot: SingleMetricsState; + itl: SingleMetricsState; throughput: SingleMetricsState; } diff --git a/src/ui/lib/store/slices/slo/slo.constants.ts b/src/ui/lib/store/slices/slo/slo.constants.ts index f58ccc05..d491b712 100644 --- a/src/ui/lib/store/slices/slo/slo.constants.ts +++ b/src/ui/lib/store/slices/slo/slo.constants.ts @@ -10,13 +10,13 @@ export const initialState: SloState = { current: { timePerRequest: 0, ttft: 0, - tpot: 0, + itl: 0, throughput: 0, }, tasksDefaults: { timePerRequest: 0, ttft: 0, - tpot: 0, + itl: 0, throughput: 0, }, }; diff --git a/src/ui/lib/store/slices/slo/slo.interfaces.ts b/src/ui/lib/store/slices/slo/slo.interfaces.ts index 0d59baa2..ecbc2f71 100644 --- a/src/ui/lib/store/slices/slo/slo.interfaces.ts +++ b/src/ui/lib/store/slices/slo/slo.interfaces.ts @@ -6,13 +6,13 @@ export interface SloState { current: { timePerRequest: number; ttft: number; - tpot: number; + itl: number; throughput: number; }; tasksDefaults: { timePerRequest: number; ttft: number; - tpot: number; + itl: number; throughput: number; }; } diff --git a/tests/ui/unit/mocks/mockBenchmarks.ts b/tests/ui/unit/mocks/mockBenchmarks.ts index 884e8b89..e8947508 100644 --- a/tests/ui/unit/mocks/mockBenchmarks.ts +++ b/tests/ui/unit/mocks/mockBenchmarks.ts @@ -1,7 +1,7 @@ export const mockBenchmarks = [ { requestsPerSecond: 0.6668550387660497, - tpot: { + itl: { total: 80, mean: 23.00635663936911, median: 22.959455611213805, @@ -132,7 +132,7 @@ export const mockBenchmarks = [ }, { requestsPerSecond: 28.075330129628725, - tpot: { + itl: { total: 3416, mean: 126.08707076148656, median: 125.30853256346687, From dd7a4b884f821da37e7a1c36cd9d9accf61d961a Mon Sep 17 00:00:00 2001 From: Samuel Monson Date: Fri, 3 Oct 2025 17:00:00 -0400 Subject: [PATCH 07/57] [GuideLLM Refactor] Advanced Prefix Cache Controls (#382) ## TODO - Docs - ~CSV arg string support~ CSV arg string now supports single bucket (see last example). Might leave it at that for now. - More validation ## Summary This PR is a port of #287 to the v0.4.0 refactor branch. Adds controls for sharing one or more fixed prefixes between samples. See examples bellow. ## Details Adds a `prefix_buckets` argument to the `SyntheticTextDatasetConfig`, each bucket consists of a prefix count, token count, and bucket weight. Prefix count sets the number of unique prefixes to generate for a given bucket, token count is the length of each prompt in the bucket, and bucket weight is used to calculate the proportion of requests the bucket applies to relative to the sum of all bucket weights. Here are a few examples: Here we have one bucket of 32 prefixes of length 2048. Since there are 1024 total samples each prefix will apply to 32 samples. If there is only one bucket than weight can be omitted as the bucket applies to 100% of samples. ```yaml data: prefix_buckets: - prefix_tokens: 2048 prefix_count: 32 prompt_tokens: 256 output_tokens: 256 samples: 1024 ``` In this modified version of the first example 16 of the prompts have 2048 tokens while the other 16 have 1024 tokens. ```yaml data: prefix_buckets: - prefix_tokens: 2048 prefix_count: 16 bucket_weight: 50 - prefix_tokens: 1024 prefix_count: 16 bucket_weight: 50 prompt_tokens: 256 output_tokens: 256 samples: 1024 ``` The prefix tokens of a bucket can also be 0 to disable prefixes for those samples. Here is an example where 40% of the samples have a prefix of 2048 tokens while the other 60% have no prefix. ```yaml data: prefix_buckets: - prefix_tokens: 2048 bucket_weight: 40 - prefix_tokens: 0 bucket_weight: 60 prompt_tokens: 256 output_tokens: 256 samples: 1000 ``` If only a single bucket is needed, it can be set at the top level. This make the changes backwards compatible with the previous interface and allows the CSV string format to work without parsing nested structures (at least for this use-case). ```yaml data: prefix_tokens: 128 prefix_count: 10 prompt_tokens: 256 output_tokens: 256 samples: 1000 ``` ## Test Plan - PR includes unit tests for all synthetic dataset changes (`pytest tests/unit/dataset`) - Scenearios in the Details section can be used against a model server with prefix caching and the cache rate can be confirmed by inspecting console output. ## Related Issues - Resolves #232 - Closes #287 --- - [x] "I certify that all code in this PR is my own, except as noted below." ## Use of AI - [x] Includes AI-assisted code completion - [ ] Includes code generated by an AI application - [x] Includes AI-generated tests (NOTE: AI written tests should have a docstring that includes `## WRITTEN BY AI ##`) --------- Signed-off-by: Samuel Monson --- pylock.toml | 2465 +++++++++-------- pyproject.toml | 9 + src/guidellm/data/deserializers/__init__.py | 2 + src/guidellm/data/deserializers/synthetic.py | 96 +- src/guidellm/data/formatters/templates.py | 16 +- src/guidellm/data/objects.py | 2 + src/guidellm/data/utils.py | 5 + tests/unit/{dataset => data}/__init__.py | 0 tests/unit/data/deserializers/__init__.py | 0 .../unit/data/deserializers/test_synthetic.py | 587 ++++ tests/unit/dataset/test_synthetic.py | 873 ------ 11 files changed, 2035 insertions(+), 2020 deletions(-) rename tests/unit/{dataset => data}/__init__.py (100%) create mode 100644 tests/unit/data/deserializers/__init__.py create mode 100644 tests/unit/data/deserializers/test_synthetic.py delete mode 100644 tests/unit/dataset/test_synthetic.py diff --git a/pylock.toml b/pylock.toml index 2fa1b28e..4c6468c2 100644 --- a/pylock.toml +++ b/pylock.toml @@ -3,8 +3,8 @@ lock-version = "1.0" requires-python = "<4.0,>=3.9.0" environments = [ - "python_version ~= \"3.10\"", - "python_version < \"3.10\" and python_version >= \"3.9\"", + "python_version ~= \"3.12\"", + "python_version < \"3.12\" and python_version >= \"3.9\"", ] extras = ["dev", "recommended"] dependency-groups = ["default"] @@ -44,6 +44,24 @@ dependencies = [ "tomli>=2.0.1; python_version < \"3.11\"", ] +[[packages]] +name = "blobfile" +version = "3.1.0" +requires-python = ">=3.8.0" +sdist = {name = "blobfile-3.1.0.tar.gz", url = "https://files.pythonhosted.org/packages/f0/6d/2e7567da75ddbb24fe979f52284b708da349d67a41042635af36071a5a6b/blobfile-3.1.0.tar.gz", hashes = {sha256 = "d45b6b1fa3b0920732314c23ddbdb4f494ca12f787c2b6eb6bba6faa51382671"}} +wheels = [ + {name = "blobfile-3.1.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/77/a7/51af11120d75af2828f8eede0b13a4caff650d708ac50e62d000aefe1ffb/blobfile-3.1.0-py3-none-any.whl",hashes = {sha256 = "2b4c5e766ebb7dfa20e4990cf6ec3d2106bdc91d632fb9377f170a234c5a5c6a"}}, +] +marker = "\"recommended\" in extras" + +[packages.tool.pdm] +dependencies = [ + "pycryptodomex>=3.8", + "urllib3<3,>=1.25.3", + "lxml>=4.9", + "filelock>=3.0", +] + [[packages]] name = "build" version = "1.2.2.post1" @@ -63,6 +81,21 @@ dependencies = [ "tomli>=1.1.0; python_version < \"3.11\"", ] +[[packages]] +name = "culsans" +version = "0.9.0" +requires-python = ">=3.8" +sdist = {name = "culsans-0.9.0.tar.gz", url = "https://files.pythonhosted.org/packages/90/5d/12e7e16b0caafaa8cca0728dd817204afd1274ddb35531b029b1c5cf7b2a/culsans-0.9.0.tar.gz", hashes = {sha256 = "942dd3c3c77f20e9ac3383d9a5ef8b7b24c0dac1a593bdb20d46c8a38720a5f3"}} +wheels = [ + {name = "culsans-0.9.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/6f/b4/1e3cccb48f09e89e0cfc06925182cbcd36abf80b8eda2489430b41c7eaff/culsans-0.9.0-py3-none-any.whl",hashes = {sha256 = "d3537b65bbb341c2ac72e7d152deb8ab893b2a00452d2a68702a1a1a41619d6f"}}, +] +marker = "\"default\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [ + "aiologic>=0.13.0", +] + [[packages]] name = "ftfy" version = "6.3.1" @@ -78,6 +111,118 @@ dependencies = [ "wcwidth", ] +[[packages]] +name = "librosa" +version = "0.11.0" +requires-python = ">=3.8" +sdist = {name = "librosa-0.11.0.tar.gz", url = "https://files.pythonhosted.org/packages/64/36/360b5aafa0238e29758729e9486c6ed92a6f37fa403b7875e06c115cdf4a/librosa-0.11.0.tar.gz", hashes = {sha256 = "f5ed951ca189b375bbe2e33b2abd7e040ceeee302b9bbaeeffdfddb8d0ace908"}} +wheels = [ + {name = "librosa-0.11.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/b5/ba/c63c5786dfee4c3417094c4b00966e61e4a63efecee22cb7b4c0387dda83/librosa-0.11.0-py3-none-any.whl",hashes = {sha256 = "0b6415c4fd68bff4c29288abe67c6d80b587e0e1e2cfb0aad23e4559504a7fa1"}}, +] +marker = "\"default\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [ + "audioread>=2.1.9", + "numba>=0.51.0", + "numpy>=1.22.3", + "scipy>=1.6.0", + "scikit-learn>=1.1.0", + "joblib>=1.0", + "decorator>=4.3.0", + "soundfile>=0.12.1", + "pooch>=1.1", + "soxr>=0.3.2", + "typing-extensions>=4.1.1", + "lazy-loader>=0.1", + "msgpack>=1.0", + "standard-aifc; python_version >= \"3.13\"", + "standard-sunau; python_version >= \"3.13\"", +] + +[[packages]] +name = "scipy" +version = "1.15.3" +requires-python = ">=3.10" +sdist = {name = "scipy-1.15.3.tar.gz", url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hashes = {sha256 = "eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf"}} +wheels = [ + {name = "scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl",hashes = {sha256 = "2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759"}}, + {name = "scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl",url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl",hashes = {sha256 = "7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62"}}, + {name = "scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl",hashes = {sha256 = "8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb"}}, + {name = "scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl",hashes = {sha256 = "0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730"}}, + {name = "scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825"}}, + {name = "scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7"}}, + {name = "scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11"}}, + {name = "scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126"}}, + {name = "scipy-1.15.3-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl",hashes = {sha256 = "b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163"}}, + {name = "scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl",hashes = {sha256 = "3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8"}}, + {name = "scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl",url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl",hashes = {sha256 = "6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5"}}, + {name = "scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl",hashes = {sha256 = "50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e"}}, + {name = "scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl",hashes = {sha256 = "14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb"}}, + {name = "scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723"}}, + {name = "scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb"}}, + {name = "scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4"}}, + {name = "scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5"}}, + {name = "scipy-1.15.3-cp313-cp313t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl",hashes = {sha256 = "76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca"}}, + {name = "scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl",hashes = {sha256 = "6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019"}}, + {name = "scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl",url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl",hashes = {sha256 = "185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6"}}, + {name = "scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl",hashes = {sha256 = "05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477"}}, + {name = "scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl",hashes = {sha256 = "06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c"}}, + {name = "scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45"}}, + {name = "scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49"}}, + {name = "scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e"}}, + {name = "scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539"}}, + {name = "scipy-1.15.3-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl",hashes = {sha256 = "52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed"}}, +] +marker = "python_version ~= \"3.12\"" + +[packages.tool.pdm] +dependencies = [ + "numpy<2.5,>=1.23.5", +] + +[[packages]] +name = "numpy" +version = "2.2.6" +requires-python = ">=3.10" +sdist = {name = "numpy-2.2.6.tar.gz", url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hashes = {sha256 = "e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd"}} +wheels = [ + {name = "numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl",hashes = {sha256 = "0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84"}}, + {name = "numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b"}}, + {name = "numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl",hashes = {sha256 = "f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d"}}, + {name = "numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl",hashes = {sha256 = "55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566"}}, + {name = "numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f"}}, + {name = "numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f"}}, + {name = "numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868"}}, + {name = "numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d"}}, + {name = "numpy-2.2.6-cp313-cp313-win32.whl",url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl",hashes = {sha256 = "5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd"}}, + {name = "numpy-2.2.6-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl",hashes = {sha256 = "b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c"}}, + {name = "numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl",hashes = {sha256 = "0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6"}}, + {name = "numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl",hashes = {sha256 = "fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda"}}, + {name = "numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl",hashes = {sha256 = "5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40"}}, + {name = "numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl",hashes = {sha256 = "fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8"}}, + {name = "numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f"}}, + {name = "numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa"}}, + {name = "numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571"}}, + {name = "numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1"}}, + {name = "numpy-2.2.6-cp313-cp313t-win32.whl",url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl",hashes = {sha256 = "038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff"}}, + {name = "numpy-2.2.6-cp313-cp313t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl",hashes = {sha256 = "6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06"}}, + {name = "numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl",hashes = {sha256 = "41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff"}}, + {name = "numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c"}}, + {name = "numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl",hashes = {sha256 = "894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3"}}, + {name = "numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl",hashes = {sha256 = "71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282"}}, + {name = "numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87"}}, + {name = "numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249"}}, + {name = "numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49"}}, + {name = "numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de"}}, + {name = "numpy-2.2.6-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl",hashes = {sha256 = "4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4"}}, + {name = "numpy-2.2.6-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl",hashes = {sha256 = "c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2"}}, +] +marker = "python_version ~= \"3.12\"" + +[packages.tool.pdm] +dependencies = [] + [[packages]] name = "lorem" version = "0.1.1" @@ -173,6 +318,38 @@ dependencies = [ "aiohttp>=3.3", ] +[[packages]] +name = "msgpack" +version = "1.1.1" +requires-python = ">=3.8" +sdist = {name = "msgpack-1.1.1.tar.gz", url = "https://files.pythonhosted.org/packages/45/b1/ea4f68038a18c77c9467400d166d74c4ffa536f34761f7983a104357e614/msgpack-1.1.1.tar.gz", hashes = {sha256 = "77b79ce34a2bdab2594f490c8e80dd62a02d650b91a75159a63ec413b8d104cd"}} +wheels = [ + {name = "msgpack-1.1.1-cp313-cp313-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/a1/38/561f01cf3577430b59b340b51329803d3a5bf6a45864a55f4ef308ac11e3/msgpack-1.1.1-cp313-cp313-macosx_10_13_x86_64.whl",hashes = {sha256 = "3765afa6bd4832fc11c3749be4ba4b69a0e8d7b728f78e68120a157a4c5d41f0"}}, + {name = "msgpack-1.1.1-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/09/48/54a89579ea36b6ae0ee001cba8c61f776451fad3c9306cd80f5b5c55be87/msgpack-1.1.1-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "8ddb2bcfd1a8b9e431c8d6f4f7db0773084e107730ecf3472f1dfe9ad583f3d9"}}, + {name = "msgpack-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/a0/60/daba2699b308e95ae792cdc2ef092a38eb5ee422f9d2fbd4101526d8a210/msgpack-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "196a736f0526a03653d829d7d4c5500a97eea3648aebfd4b6743875f28aa2af8"}}, + {name = "msgpack-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/20/22/2ebae7ae43cd8f2debc35c631172ddf14e2a87ffcc04cf43ff9df9fff0d3/msgpack-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "9d592d06e3cc2f537ceeeb23d38799c6ad83255289bb84c2e5792e5a8dea268a"}}, + {name = "msgpack-1.1.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/40/1b/54c08dd5452427e1179a40b4b607e37e2664bca1c790c60c442c8e972e47/msgpack-1.1.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "4df2311b0ce24f06ba253fda361f938dfecd7b961576f9be3f3fbd60e87130ac"}}, + {name = "msgpack-1.1.1-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/2e/60/6bb17e9ffb080616a51f09928fdd5cac1353c9becc6c4a8abd4e57269a16/msgpack-1.1.1-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "e4141c5a32b5e37905b5940aacbc59739f036930367d7acce7a64e4dec1f5e0b"}}, + {name = "msgpack-1.1.1-cp313-cp313-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/ee/97/88983e266572e8707c1f4b99c8fd04f9eb97b43f2db40e3172d87d8642db/msgpack-1.1.1-cp313-cp313-musllinux_1_2_i686.whl",hashes = {sha256 = "b1ce7f41670c5a69e1389420436f41385b1aa2504c3b0c30620764b15dded2e7"}}, + {name = "msgpack-1.1.1-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/bc/66/36c78af2efaffcc15a5a61ae0df53a1d025f2680122e2a9eb8442fed3ae4/msgpack-1.1.1-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "4147151acabb9caed4e474c3344181e91ff7a388b888f1e19ea04f7e73dc7ad5"}}, + {name = "msgpack-1.1.1-cp313-cp313-win32.whl",url = "https://files.pythonhosted.org/packages/8c/87/a75eb622b555708fe0427fab96056d39d4c9892b0c784b3a721088c7ee37/msgpack-1.1.1-cp313-cp313-win32.whl",hashes = {sha256 = "500e85823a27d6d9bba1d057c871b4210c1dd6fb01fbb764e37e4e8847376323"}}, + {name = "msgpack-1.1.1-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/ca/91/7dc28d5e2a11a5ad804cf2b7f7a5fcb1eb5a4966d66a5d2b41aee6376543/msgpack-1.1.1-cp313-cp313-win_amd64.whl",hashes = {sha256 = "6d489fba546295983abd142812bda76b57e33d0b9f5d5b71c09a583285506f69"}}, + {name = "msgpack-1.1.1-cp312-cp312-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/e3/26/389b9c593eda2b8551b2e7126ad3a06af6f9b44274eb3a4f054d48ff7e47/msgpack-1.1.1-cp312-cp312-macosx_10_13_x86_64.whl",hashes = {sha256 = "ae497b11f4c21558d95de9f64fff7053544f4d1a17731c866143ed6bb4591238"}}, + {name = "msgpack-1.1.1-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/ab/65/7d1de38c8a22cf8b1551469159d4b6cf49be2126adc2482de50976084d78/msgpack-1.1.1-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "33be9ab121df9b6b461ff91baac6f2731f83d9b27ed948c5b9d1978ae28bf157"}}, + {name = "msgpack-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/0f/bd/cacf208b64d9577a62c74b677e1ada005caa9b69a05a599889d6fc2ab20a/msgpack-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "6f64ae8fe7ffba251fecb8408540c34ee9df1c26674c50c4544d72dbf792e5ce"}}, + {name = "msgpack-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/4d/ec/fd869e2567cc9c01278a736cfd1697941ba0d4b81a43e0aa2e8d71dab208/msgpack-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "a494554874691720ba5891c9b0b39474ba43ffb1aaf32a5dac874effb1619e1a"}}, + {name = "msgpack-1.1.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/55/2a/35860f33229075bce803a5593d046d8b489d7ba2fc85701e714fc1aaf898/msgpack-1.1.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "cb643284ab0ed26f6957d969fe0dd8bb17beb567beb8998140b5e38a90974f6c"}}, + {name = "msgpack-1.1.1-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/8c/16/69ed8f3ada150bf92745fb4921bd621fd2cdf5a42e25eb50bcc57a5328f0/msgpack-1.1.1-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "d275a9e3c81b1093c060c3837e580c37f47c51eca031f7b5fb76f7b8470f5f9b"}}, + {name = "msgpack-1.1.1-cp312-cp312-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/c6/b6/0c398039e4c6d0b2e37c61d7e0e9d13439f91f780686deb8ee64ecf1ae71/msgpack-1.1.1-cp312-cp312-musllinux_1_2_i686.whl",hashes = {sha256 = "4fd6b577e4541676e0cc9ddc1709d25014d3ad9a66caa19962c4f5de30fc09ef"}}, + {name = "msgpack-1.1.1-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/b8/d0/0cf4a6ecb9bc960d624c93effaeaae75cbf00b3bc4a54f35c8507273cda1/msgpack-1.1.1-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "bb29aaa613c0a1c40d1af111abf025f1732cab333f96f285d6a93b934738a68a"}}, + {name = "msgpack-1.1.1-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/62/83/9697c211720fa71a2dfb632cad6196a8af3abea56eece220fde4674dc44b/msgpack-1.1.1-cp312-cp312-win32.whl",hashes = {sha256 = "870b9a626280c86cff9c576ec0d9cbcc54a1e5ebda9cd26dab12baf41fee218c"}}, + {name = "msgpack-1.1.1-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/c0/23/0abb886e80eab08f5e8c485d6f13924028602829f63b8f5fa25a06636628/msgpack-1.1.1-cp312-cp312-win_amd64.whl",hashes = {sha256 = "5692095123007180dca3e788bb4c399cc26626da51629a31d40207cb262e67f4"}}, +] +marker = "\"default\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [] + [[packages]] name = "mypy" version = "1.15.0" @@ -191,25 +368,7 @@ wheels = [ {name = "mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/b3/d0/92ae4cde706923a2d3f2d6c39629134063ff64b9dedca9c1388363da072d/mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee"}}, {name = "mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/46/8b/df49974b337cce35f828ba6fda228152d6db45fed4c86ba56ffe442434fd/mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e"}}, {name = "mypy-1.15.0-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/13/50/da5203fcf6c53044a0b699939f31075c45ae8a4cadf538a9069b165c1050/mypy-1.15.0-cp312-cp312-win_amd64.whl",hashes = {sha256 = "171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22"}}, - {name = "mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/03/bc/f6339726c627bd7ca1ce0fa56c9ae2d0144604a319e0e339bdadafbbb599/mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl",hashes = {sha256 = "2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f"}}, - {name = "mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/e2/90/8dcf506ca1a09b0d17555cc00cd69aee402c203911410136cd716559efe7/mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5"}}, - {name = "mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/05/05/a10f9479681e5da09ef2f9426f650d7b550d4bafbef683b69aad1ba87457/mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e"}}, - {name = "mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/e9/9a/1f7d18b30edd57441a6411fcbc0c6869448d1a4bacbaee60656ac0fc29c8/mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c"}}, - {name = "mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/72/af/19ff499b6f1dafcaf56f9881f7a965ac2f474f69f6f618b5175b044299f5/mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f"}}, - {name = "mypy-1.15.0-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/96/39/11b57431a1f686c1aed54bf794870efe0f6aeca11aca281a0bd87a5ad42c/mypy-1.15.0-cp311-cp311-win_amd64.whl",hashes = {sha256 = "c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f"}}, - {name = "mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/68/f8/65a7ce8d0e09b6329ad0c8d40330d100ea343bd4dd04c4f8ae26462d0a17/mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl",hashes = {sha256 = "979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13"}}, - {name = "mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/b4/95/9c0ecb8eacfe048583706249439ff52105b3f552ea9c4024166c03224270/mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559"}}, - {name = "mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/84/09/9ec95e982e282e20c0d5407bc65031dfd0f0f8ecc66b69538296e06fcbee/mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b"}}, - {name = "mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/78/13/f7d14e55865036a1e6a0a69580c240f43bc1f37407fe9235c0d4ef25ffb0/mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3"}}, - {name = "mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/48/e1/301a73852d40c241e915ac6d7bcd7fedd47d519246db2d7b86b9d7e7a0cb/mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b"}}, - {name = "mypy-1.15.0-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/77/ba/c37bc323ae5fe7f3f15a28e06ab012cd0b7552886118943e90b15af31195/mypy-1.15.0-cp310-cp310-win_amd64.whl",hashes = {sha256 = "6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828"}}, {name = "mypy-1.15.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl",hashes = {sha256 = "5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e"}}, - {name = "mypy-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/5a/fa/79cf41a55b682794abe71372151dbbf856e3008f6767057229e6649d294a/mypy-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl",hashes = {sha256 = "e601a7fa172c2131bff456bb3ee08a88360760d0d2f8cbd7a75a65497e2df078"}}, - {name = "mypy-1.15.0-cp39-cp39-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/d3/33/dd8feb2597d648de29e3da0a8bf4e1afbda472964d2a4a0052203a6f3594/mypy-1.15.0-cp39-cp39-macosx_11_0_arm64.whl",hashes = {sha256 = "712e962a6357634fef20412699a3655c610110e01cdaa6180acec7fc9f8513ba"}}, - {name = "mypy-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/e4/b5/74508959c1b06b96674b364ffeb7ae5802646b32929b7701fc6b18447592/mypy-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "f95579473af29ab73a10bada2f9722856792a36ec5af5399b653aa28360290a5"}}, - {name = "mypy-1.15.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/6c/53/da61b9d9973efcd6507183fdad96606996191657fe79701b2c818714d573/mypy-1.15.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "8f8722560a14cde92fdb1e31597760dc35f9f5524cce17836c0d22841830fd5b"}}, - {name = "mypy-1.15.0-cp39-cp39-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/c1/72/965bd9ee89540c79a25778cc080c7e6ef40aa1eeac4d52cec7eae6eb5228/mypy-1.15.0-cp39-cp39-musllinux_1_2_x86_64.whl",hashes = {sha256 = "1fbb8da62dc352133d7d7ca90ed2fb0e9d42bb1a32724c287d3c76c58cbaa9c2"}}, - {name = "mypy-1.15.0-cp39-cp39-win_amd64.whl",url = "https://files.pythonhosted.org/packages/46/d0/f41645c2eb263e6c77ada7d76f894c580c9ddb20d77f0c24d34273a4dab2/mypy-1.15.0-cp39-cp39-win_amd64.whl",hashes = {sha256 = "d10d994b41fb3497719bbf866f227b3489048ea4bbbb5015357db306249f7980"}}, ] marker = "\"dev\" in extras" @@ -263,33 +422,6 @@ wheels = [ {name = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl",hashes = {sha256 = "8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}}, {name = "PyYAML-6.0.2-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl",hashes = {sha256 = "ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}}, {name = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl",hashes = {sha256 = "7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}}, - {name = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl",hashes = {sha256 = "cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}}, - {name = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}}, - {name = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}}, - {name = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}}, - {name = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}}, - {name = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl",url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl",hashes = {sha256 = "ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}}, - {name = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl",hashes = {sha256 = "797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}}, - {name = "PyYAML-6.0.2-cp311-cp311-win32.whl",url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl",hashes = {sha256 = "11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}}, - {name = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl",hashes = {sha256 = "e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}}, - {name = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl",hashes = {sha256 = "0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}}, - {name = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}}, - {name = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}}, - {name = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}}, - {name = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}}, - {name = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl",url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl",hashes = {sha256 = "936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}}, - {name = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl",hashes = {sha256 = "23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}}, - {name = "PyYAML-6.0.2-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl",hashes = {sha256 = "2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}}, - {name = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl",hashes = {sha256 = "a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}}, - {name = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl",hashes = {sha256 = "688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}}, - {name = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl",hashes = {sha256 = "a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}}, - {name = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}}, - {name = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}}, - {name = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}}, - {name = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl",url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl",hashes = {sha256 = "0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}}, - {name = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl",hashes = {sha256 = "a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}}, - {name = "PyYAML-6.0.2-cp39-cp39-win32.whl",url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl",hashes = {sha256 = "6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}}, - {name = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl",url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl",hashes = {sha256 = "39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}}, ] marker = "\"default\" in dependency_groups or \"dev\" in extras" @@ -353,17 +485,19 @@ dependencies = [ [[packages]] name = "pytest-asyncio" -version = "0.23.8" -requires-python = ">=3.8" -sdist = {name = "pytest_asyncio-0.23.8.tar.gz", url = "https://files.pythonhosted.org/packages/de/b4/0b378b7bf26a8ae161c3890c0b48a91a04106c5713ce81b4b080ea2f4f18/pytest_asyncio-0.23.8.tar.gz", hashes = {sha256 = "759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3"}} +version = "1.1.1" +requires-python = ">=3.9" +sdist = {name = "pytest_asyncio-1.1.1.tar.gz", url = "https://files.pythonhosted.org/packages/8d/1e/2aa43805d4a320a9489d2b99f7877b69f9094c79aa0732159a1415dd6cd4/pytest_asyncio-1.1.1.tar.gz", hashes = {sha256 = "b72d215c38e2c91dbb32f275e0b5be69602d7869910e109360e375129960a649"}} wheels = [ - {name = "pytest_asyncio-0.23.8-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/ee/82/62e2d63639ecb0fbe8a7ee59ef0bc69a4669ec50f6d3459f74ad4e4189a2/pytest_asyncio-0.23.8-py3-none-any.whl",hashes = {sha256 = "50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2"}}, + {name = "pytest_asyncio-1.1.1-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/28/de/aba79e9ccdb51b5d0d65c67dd857bd78b00c64723df16b9fc800d8b94ce6/pytest_asyncio-1.1.1-py3-none-any.whl",hashes = {sha256 = "726339d30fcfde24691f589445b9b67d058b311ac632b1d704e97f20f1d878da"}}, ] marker = "\"dev\" in extras" [packages.tool.pdm] dependencies = [ - "pytest<9,>=7.0.0", + "backports-asyncio-runner<2,>=1.1; python_version < \"3.11\"", + "pytest<9,>=8.2", + "typing-extensions>=4.12; python_version < \"3.10\"", ] [[packages]] @@ -457,131 +591,6 @@ marker = "\"dev\" in extras" [packages.tool.pdm] dependencies = [] -[[packages]] -name = "scipy" -version = "1.15.3" -requires-python = ">=3.10" -sdist = {name = "scipy-1.15.3.tar.gz", url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hashes = {sha256 = "eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf"}} -wheels = [ - {name = "scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl",hashes = {sha256 = "2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759"}}, - {name = "scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl",url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl",hashes = {sha256 = "7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62"}}, - {name = "scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl",hashes = {sha256 = "8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb"}}, - {name = "scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl",hashes = {sha256 = "0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730"}}, - {name = "scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825"}}, - {name = "scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7"}}, - {name = "scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11"}}, - {name = "scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126"}}, - {name = "scipy-1.15.3-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl",hashes = {sha256 = "b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163"}}, - {name = "scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl",hashes = {sha256 = "3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8"}}, - {name = "scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl",url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl",hashes = {sha256 = "6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5"}}, - {name = "scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl",hashes = {sha256 = "50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e"}}, - {name = "scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl",hashes = {sha256 = "14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb"}}, - {name = "scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723"}}, - {name = "scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb"}}, - {name = "scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4"}}, - {name = "scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5"}}, - {name = "scipy-1.15.3-cp313-cp313t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl",hashes = {sha256 = "76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca"}}, - {name = "scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl",hashes = {sha256 = "6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019"}}, - {name = "scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl",url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl",hashes = {sha256 = "185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6"}}, - {name = "scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl",hashes = {sha256 = "05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477"}}, - {name = "scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl",hashes = {sha256 = "06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c"}}, - {name = "scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45"}}, - {name = "scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49"}}, - {name = "scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e"}}, - {name = "scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539"}}, - {name = "scipy-1.15.3-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl",hashes = {sha256 = "52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed"}}, - {name = "scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl",hashes = {sha256 = "993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b"}}, - {name = "scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl",url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl",hashes = {sha256 = "34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba"}}, - {name = "scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl",hashes = {sha256 = "3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65"}}, - {name = "scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl",hashes = {sha256 = "6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1"}}, - {name = "scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889"}}, - {name = "scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982"}}, - {name = "scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9"}}, - {name = "scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594"}}, - {name = "scipy-1.15.3-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl",hashes = {sha256 = "ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb"}}, - {name = "scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl",hashes = {sha256 = "a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c"}}, - {name = "scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl",url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl",hashes = {sha256 = "ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253"}}, - {name = "scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl",hashes = {sha256 = "aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f"}}, - {name = "scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl",hashes = {sha256 = "1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92"}}, - {name = "scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82"}}, - {name = "scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40"}}, - {name = "scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e"}}, - {name = "scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c"}}, - {name = "scipy-1.15.3-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl",hashes = {sha256 = "9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13"}}, -] -marker = "python_version ~= \"3.10\" and \"dev\" in extras" - -[packages.tool.pdm] -dependencies = [ - "numpy<2.5,>=1.23.5", -] - -[[packages]] -name = "numpy" -version = "2.2.6" -requires-python = ">=3.10" -sdist = {name = "numpy-2.2.6.tar.gz", url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hashes = {sha256 = "e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd"}} -wheels = [ - {name = "numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl",hashes = {sha256 = "0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84"}}, - {name = "numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b"}}, - {name = "numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl",hashes = {sha256 = "f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d"}}, - {name = "numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl",hashes = {sha256 = "55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566"}}, - {name = "numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f"}}, - {name = "numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f"}}, - {name = "numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868"}}, - {name = "numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d"}}, - {name = "numpy-2.2.6-cp313-cp313-win32.whl",url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl",hashes = {sha256 = "5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd"}}, - {name = "numpy-2.2.6-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl",hashes = {sha256 = "b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c"}}, - {name = "numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl",hashes = {sha256 = "0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6"}}, - {name = "numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl",hashes = {sha256 = "fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda"}}, - {name = "numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl",hashes = {sha256 = "5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40"}}, - {name = "numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl",hashes = {sha256 = "fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8"}}, - {name = "numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f"}}, - {name = "numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa"}}, - {name = "numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571"}}, - {name = "numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1"}}, - {name = "numpy-2.2.6-cp313-cp313t-win32.whl",url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl",hashes = {sha256 = "038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff"}}, - {name = "numpy-2.2.6-cp313-cp313t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl",hashes = {sha256 = "6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06"}}, - {name = "numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl",hashes = {sha256 = "41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff"}}, - {name = "numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c"}}, - {name = "numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl",hashes = {sha256 = "894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3"}}, - {name = "numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl",hashes = {sha256 = "71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282"}}, - {name = "numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87"}}, - {name = "numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249"}}, - {name = "numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49"}}, - {name = "numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de"}}, - {name = "numpy-2.2.6-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl",hashes = {sha256 = "4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4"}}, - {name = "numpy-2.2.6-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl",hashes = {sha256 = "c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2"}}, - {name = "numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl",hashes = {sha256 = "f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae"}}, - {name = "numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a"}}, - {name = "numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl",hashes = {sha256 = "3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42"}}, - {name = "numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl",hashes = {sha256 = "481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491"}}, - {name = "numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a"}}, - {name = "numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf"}}, - {name = "numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1"}}, - {name = "numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab"}}, - {name = "numpy-2.2.6-cp311-cp311-win32.whl",url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl",hashes = {sha256 = "0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47"}}, - {name = "numpy-2.2.6-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl",hashes = {sha256 = "e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303"}}, - {name = "numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl",hashes = {sha256 = "b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb"}}, - {name = "numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90"}}, - {name = "numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl",hashes = {sha256 = "37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163"}}, - {name = "numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl",hashes = {sha256 = "5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf"}}, - {name = "numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83"}}, - {name = "numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915"}}, - {name = "numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680"}}, - {name = "numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289"}}, - {name = "numpy-2.2.6-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl",hashes = {sha256 = "b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d"}}, - {name = "numpy-2.2.6-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl",hashes = {sha256 = "f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3"}}, - {name = "numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl",url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl",hashes = {sha256 = "0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d"}}, - {name = "numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl",hashes = {sha256 = "7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db"}}, - {name = "numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543"}}, - {name = "numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl",url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl",hashes = {sha256 = "d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00"}}, -] -marker = "\"default\" in dependency_groups and python_version ~= \"3.10\" or \"dev\" in extras and python_version ~= \"3.10\"" - -[packages.tool.pdm] -dependencies = [] - [[packages]] name = "setuptools" version = "80.9.0" @@ -590,7 +599,7 @@ sdist = {name = "setuptools-80.9.0.tar.gz", url = "https://files.pythonhosted.or wheels = [ {name = "setuptools-80.9.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl",hashes = {sha256 = "062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"}}, ] -marker = "\"dev\" in extras" +marker = "\"default\" in dependency_groups or \"dev\" in extras" [packages.tool.pdm] dependencies = [] @@ -644,24 +653,6 @@ wheels = [ {name = "tiktoken-0.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/d5/2d/4d77f6feb9292bfdd23d5813e442b3bba883f42d0ac78ef5fdc56873f756/tiktoken-0.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "7f929255c705efec7a28bf515e29dc74220b2f07544a8c81b8d69e8efc4578bd"}}, {name = "tiktoken-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/7a/65/7ff0a65d3bb0fc5a1fb6cc71b03e0f6e71a68c5eea230d1ff1ba3fd6df49/tiktoken-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "61f1d15822e4404953d499fd1dcc62817a12ae9fb1e4898033ec8fe3915fdf8e"}}, {name = "tiktoken-0.11.0-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/f5/6e/5b71578799b72e5bdcef206a214c3ce860d999d579a3b56e74a6c8989ee2/tiktoken-0.11.0-cp312-cp312-win_amd64.whl",hashes = {sha256 = "45927a71ab6643dfd3ef57d515a5db3d199137adf551f66453be098502838b0f"}}, - {name = "tiktoken-0.11.0-cp311-cp311-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/8a/91/912b459799a025d2842566fe1e902f7f50d54a1ce8a0f236ab36b5bd5846/tiktoken-0.11.0-cp311-cp311-macosx_10_12_x86_64.whl",hashes = {sha256 = "4ae374c46afadad0f501046db3da1b36cd4dfbfa52af23c998773682446097cf"}}, - {name = "tiktoken-0.11.0-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/8c/e9/6faa6870489ce64f5f75dcf91512bf35af5864583aee8fcb0dcb593121f5/tiktoken-0.11.0-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "25a512ff25dc6c85b58f5dd4f3d8c674dc05f96b02d66cdacf628d26a4e4866b"}}, - {name = "tiktoken-0.11.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/a1/3e/a05d1547cf7db9dc75d1461cfa7b556a3b48e0516ec29dfc81d984a145f6/tiktoken-0.11.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "2130127471e293d385179c1f3f9cd445070c0772be73cdafb7cec9a3684c0458"}}, - {name = "tiktoken-0.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/34/9a/db7a86b829e05a01fd4daa492086f708e0a8b53952e1dbc9d380d2b03677/tiktoken-0.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "21e43022bf2c33f733ea9b54f6a3f6b4354b909f5a73388fb1b9347ca54a069c"}}, - {name = "tiktoken-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/9d/bb/52edc8e078cf062ed749248f1454e9e5cfd09979baadb830b3940e522015/tiktoken-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "adb4e308eb64380dc70fa30493e21c93475eaa11669dea313b6bbf8210bfd013"}}, - {name = "tiktoken-0.11.0-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/60/d9/884b6cd7ae2570ecdcaffa02b528522b18fef1cbbfdbcaa73799807d0d3b/tiktoken-0.11.0-cp311-cp311-win_amd64.whl",hashes = {sha256 = "ece6b76bfeeb61a125c44bbefdfccc279b5288e6007fbedc0d32bfec602df2f2"}}, - {name = "tiktoken-0.11.0-cp310-cp310-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/8b/4d/c6a2e7dca2b4f2e9e0bfd62b3fe4f114322e2c028cfba905a72bc76ce479/tiktoken-0.11.0-cp310-cp310-macosx_10_12_x86_64.whl",hashes = {sha256 = "8a9b517d6331d7103f8bef29ef93b3cca95fa766e293147fe7bacddf310d5917"}}, - {name = "tiktoken-0.11.0-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/41/54/3739d35b9f94cb8dc7b0db2edca7192d5571606aa2369a664fa27e811804/tiktoken-0.11.0-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "b4ddb1849e6bf0afa6cc1c5d809fb980ca240a5fffe585a04e119519758788c0"}}, - {name = "tiktoken-0.11.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/dd/f4/ec8d43338d28d53513004ebf4cd83732a135d11011433c58bf045890cc10/tiktoken-0.11.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "10331d08b5ecf7a780b4fe4d0281328b23ab22cdb4ff65e68d56caeda9940ecc"}}, - {name = "tiktoken-0.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/94/80/fb0ada0a882cb453caf519a4bf0d117c2a3ee2e852c88775abff5413c176/tiktoken-0.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "b062c82300341dc87e0258c69f79bed725f87e753c21887aea90d272816be882"}}, - {name = "tiktoken-0.11.0-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/2f/e9/6c104355b463601719582823f3ea658bc3aa7c73d1b3b7553ebdc48468ce/tiktoken-0.11.0-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "195d84bec46169af3b1349a1495c151d37a0ff4cba73fd08282736be7f92cc6c"}}, - {name = "tiktoken-0.11.0-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/94/75/eaa6068f47e8b3f0aab9e05177cce2cf5aa2cc0ca93981792e620d4d4117/tiktoken-0.11.0-cp310-cp310-win_amd64.whl",hashes = {sha256 = "fe91581b0ecdd8783ce8cb6e3178f2260a3912e8724d2f2d49552b98714641a1"}}, - {name = "tiktoken-0.11.0-cp39-cp39-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/aa/b6/81c5799ab77a9580c6d840cf77d4717e929193a42190fd623a080c647aa6/tiktoken-0.11.0-cp39-cp39-macosx_10_12_x86_64.whl",hashes = {sha256 = "13220f12c9e82e399377e768640ddfe28bea962739cc3a869cad98f42c419a89"}}, - {name = "tiktoken-0.11.0-cp39-cp39-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/50/89/faa668066b2a4640534ef5797c09ecd0a48b43367502129b217339dfaa97/tiktoken-0.11.0-cp39-cp39-macosx_11_0_arm64.whl",hashes = {sha256 = "7f2db627f5c74477c0404b4089fd8a28ae22fa982a6f7d9c7d4c305c375218f3"}}, - {name = "tiktoken-0.11.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/aa/7f/5f950528b54cb3025af4bc3522c23dbfb691afe8ffb292aa1e8dc2e6bddf/tiktoken-0.11.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "2302772f035dceb2bcf8e55a735e4604a0b51a6dd50f38218ff664d46ec43807"}}, - {name = "tiktoken-0.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/27/a4/e82ddf0773835ba24536ac8c0dce561e697698ec020a93212a1e041d39b4/tiktoken-0.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "20b977989afe44c94bcc50db1f76971bb26dca44218bd203ba95925ef56f8e7a"}}, - {name = "tiktoken-0.11.0-cp39-cp39-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/1b/c2/06361e41d176e62797ae65fa678111cdd30553321cf4d83e7b84107ea95f/tiktoken-0.11.0-cp39-cp39-musllinux_1_2_x86_64.whl",hashes = {sha256 = "669a1aa1ad6ebf1b3c26b45deb346f345da7680f845b5ea700bba45c20dea24c"}}, - {name = "tiktoken-0.11.0-cp39-cp39-win_amd64.whl",url = "https://files.pythonhosted.org/packages/bb/ad/ca37e15c46741ebb3904d562d03194e845539a08f7751a6df0f391757312/tiktoken-0.11.0-cp39-cp39-win_amd64.whl",hashes = {sha256 = "e363f33c720a055586f730c00e330df4c7ea0024bf1c83a8a9a9dbc054c4f304"}}, ] marker = "\"recommended\" in extras" @@ -671,6 +662,41 @@ dependencies = [ "requests>=2.26.0", ] +[[packages]] +name = "torch" +version = "2.8.0+cpu" +requires-python = ">=3.9.0" +wheels = [ + {name = "torch-2.8.0+cpu-cp313-cp313-linux_s390x.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-linux_s390x.whl",hashes = {sha256 = "8b5882276633cf91fe3d2d7246c743b94d44a7e660b27f1308007fdb1bb89f7d"}}, + {name = "torch-2.8.0+cpu-cp313-cp313-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl",hashes = {sha256 = "a5064b5e23772c8d164068cc7c12e01a75faf7b948ecd95a0d4007d7487e5f25"}}, + {name = "torch-2.8.0+cpu-cp313-cp313-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl",hashes = {sha256 = "8f81dedb4c6076ec325acc3b47525f9c550e5284a18eae1d9061c543f7b6e7de"}}, + {name = "torch-2.8.0+cpu-cp313-cp313-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-win_amd64.whl",hashes = {sha256 = "e1ee1b2346ade3ea90306dfbec7e8ff17bc220d344109d189ae09078333b0856"}}, + {name = "torch-2.8.0+cpu-cp313-cp313-win_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-win_arm64.whl",hashes = {sha256 = "64c187345509f2b1bb334feed4666e2c781ca381874bde589182f81247e61f88"}}, + {name = "torch-2.8.0+cpu-cp313-cp313t-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl",hashes = {sha256 = "af81283ac671f434b1b25c95ba295f270e72db1fad48831eb5e4748ff9840041"}}, + {name = "torch-2.8.0+cpu-cp313-cp313t-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl",hashes = {sha256 = "a9dbb6f64f63258bc811e2c0c99640a81e5af93c531ad96e95c5ec777ea46dab"}}, + {name = "torch-2.8.0+cpu-cp313-cp313t-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-win_amd64.whl",hashes = {sha256 = "6d93a7165419bc4b2b907e859ccab0dea5deeab261448ae9a5ec5431f14c0e64"}}, + {name = "torch-2.8.0-cp313-cp313t-macosx_14_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0-cp313-cp313t-macosx_14_0_arm64.whl",hashes = {sha256 = "fbe2e149c5174ef90d29a5f84a554dfaf28e003cb4f61fa2c8c024c17ec7ca58"}}, + {name = "torch-2.8.0-cp313-none-macosx_11_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0-cp313-none-macosx_11_0_arm64.whl",hashes = {sha256 = "057efd30a6778d2ee5e2374cd63a63f63311aa6f33321e627c655df60abdd390"}}, + {name = "torch-2.8.0+cpu-cp312-cp312-linux_s390x.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-linux_s390x.whl",hashes = {sha256 = "0e34e276722ab7dd0dffa9e12fe2135a9b34a0e300c456ed7ad6430229404eb5"}}, + {name = "torch-2.8.0+cpu-cp312-cp312-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl",hashes = {sha256 = "610f600c102386e581327d5efc18c0d6edecb9820b4140d26163354a99cd800d"}}, + {name = "torch-2.8.0+cpu-cp312-cp312-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl",hashes = {sha256 = "cb9a8ba8137ab24e36bf1742cb79a1294bd374db570f09fc15a5e1318160db4e"}}, + {name = "torch-2.8.0+cpu-cp312-cp312-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-win_amd64.whl",hashes = {sha256 = "2be20b2c05a0cce10430cc25f32b689259640d273232b2de357c35729132256d"}}, + {name = "torch-2.8.0+cpu-cp312-cp312-win_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-win_arm64.whl",hashes = {sha256 = "99fc421a5d234580e45957a7b02effbf3e1c884a5dd077afc85352c77bf41434"}}, + {name = "torch-2.8.0-cp312-none-macosx_11_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0-cp312-none-macosx_11_0_arm64.whl",hashes = {sha256 = "a47b7986bee3f61ad217d8a8ce24605809ab425baf349f97de758815edd2ef54"}}, +] +marker = "\"default\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [ + "filelock", + "typing-extensions>=4.10.0", + "sympy>=1.13.3", + "networkx", + "jinja2", + "fsspec", + "setuptools; python_version >= \"3.12\"", +] + [[packages]] name = "tox" version = "4.16.0" @@ -724,22 +750,28 @@ dependencies = [ ] [[packages]] -name = "blobfile" -version = "3.1.0" +name = "uvloop" +version = "0.21.0" requires-python = ">=3.8.0" -sdist = {name = "blobfile-3.1.0.tar.gz", url = "https://files.pythonhosted.org/packages/f0/6d/2e7567da75ddbb24fe979f52284b708da349d67a41042635af36071a5a6b/blobfile-3.1.0.tar.gz", hashes = {sha256 = "d45b6b1fa3b0920732314c23ddbdb4f494ca12f787c2b6eb6bba6faa51382671"}} -wheels = [ - {name = "blobfile-3.1.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/77/a7/51af11120d75af2828f8eede0b13a4caff650d708ac50e62d000aefe1ffb/blobfile-3.1.0-py3-none-any.whl",hashes = {sha256 = "2b4c5e766ebb7dfa20e4990cf6ec3d2106bdc91d632fb9377f170a234c5a5c6a"}}, +sdist = {name = "uvloop-0.21.0.tar.gz", url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hashes = {sha256 = "3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3"}} +wheels = [ + {name = "uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl",url = "https://files.pythonhosted.org/packages/3f/8d/2cbef610ca21539f0f36e2b34da49302029e7c9f09acef0b1c3b5839412b/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl",hashes = {sha256 = "bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281"}}, + {name = "uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/93/0d/b0038d5a469f94ed8f2b2fce2434a18396d8fbfb5da85a0a9781ebbdec14/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl",hashes = {sha256 = "787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af"}}, + {name = "uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/50/94/0a687f39e78c4c1e02e3272c6b2ccdb4e0085fda3b8352fecd0410ccf915/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6"}}, + {name = "uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816"}}, + {name = "uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/47/57/66f061ee118f413cd22a656de622925097170b9380b30091b78ea0c6ea75/uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc"}}, + {name = "uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553"}}, + {name = "uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl",url = "https://files.pythonhosted.org/packages/8c/4c/03f93178830dc7ce8b4cdee1d36770d2f5ebb6f3d37d354e061eefc73545/uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl",hashes = {sha256 = "359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c"}}, + {name = "uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/43/3e/92c03f4d05e50f09251bd8b2b2b584a2a7f8fe600008bcc4523337abe676/uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl",hashes = {sha256 = "f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2"}}, + {name = "uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/a6/ef/a02ec5da49909dbbfb1fd205a9a1ac4e88ea92dcae885e7c961847cd51e2/uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d"}}, + {name = "uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/06/a7/b4e6a19925c900be9f98bec0a75e6e8f79bb53bdeb891916609ab3958967/uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc"}}, + {name = "uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/ce/0c/f07435a18a4b94ce6bd0677d8319cd3de61f3a9eeb1e5f8ab4e8b5edfcb3/uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb"}}, + {name = "uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/8f/eb/f7032be105877bcf924709c97b1bf3b90255b4ec251f9340cef912559f28/uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f"}}, ] -marker = "\"recommended\" in extras" +marker = "\"default\" in dependency_groups" [packages.tool.pdm] -dependencies = [ - "pycryptodomex>=3.8", - "urllib3<3,>=1.25.3", - "lxml>=4.9", - "filelock>=3.0", -] +dependencies = [] [[packages]] name = "datasets" @@ -769,31 +801,59 @@ dependencies = [ ] [[packages]] -name = "loguru" -version = "0.7.3" -requires-python = "<4.0,>=3.5" -sdist = {name = "loguru-0.7.3.tar.gz", url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hashes = {sha256 = "19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6"}} +name = "eval-type-backport" +version = "0.2.2" +requires-python = ">=3.8" +sdist = {name = "eval_type_backport-0.2.2.tar.gz", url = "https://files.pythonhosted.org/packages/30/ea/8b0ac4469d4c347c6a385ff09dc3c048c2d021696664e26c7ee6791631b5/eval_type_backport-0.2.2.tar.gz", hashes = {sha256 = "f0576b4cf01ebb5bd358d02314d31846af5e07678387486e2c798af0e7d849c1"}} wheels = [ - {name = "loguru-0.7.3-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl",hashes = {sha256 = "31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c"}}, + {name = "eval_type_backport-0.2.2-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/ce/31/55cd413eaccd39125368be33c46de24a1f639f2e12349b0361b4678f3915/eval_type_backport-0.2.2-py3-none-any.whl",hashes = {sha256 = "cb6ad7c393517f476f96d456d0412ea80f0a8cf96f6892834cd9340149111b0a"}}, ] marker = "\"default\" in dependency_groups" [packages.tool.pdm] -dependencies = [ - "colorama>=0.3.4; sys_platform == \"win32\"", - "aiocontextvars>=0.2.0; python_version < \"3.7\"", - "win32-setctime>=1.0.0; sys_platform == \"win32\"", -] +dependencies = [] [[packages]] -name = "pillow" -version = "11.3.0" +name = "faker" +version = "37.8.0" requires-python = ">=3.9" -sdist = {name = "pillow-11.3.0.tar.gz", url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hashes = {sha256 = "3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523"}} +sdist = {name = "faker-37.8.0.tar.gz", url = "https://files.pythonhosted.org/packages/3a/da/1336008d39e5d4076dddb4e0f3a52ada41429274bf558a3cc28030d324a3/faker-37.8.0.tar.gz", hashes = {sha256 = "090bb5abbec2b30949a95ce1ba6b20d1d0ed222883d63483a0d4be4a970d6fb8"}} wheels = [ - {name = "pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl",hashes = {sha256 = "d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12"}}, - {name = "pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a"}}, - {name = "pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632"}}, + {name = "faker-37.8.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/f5/11/02ebebb09ff2104b690457cb7bc6ed700c9e0ce88cf581486bb0a5d3c88b/faker-37.8.0-py3-none-any.whl",hashes = {sha256 = "b08233118824423b5fc239f7dd51f145e7018082b4164f8da6a9994e1f1ae793"}}, +] +marker = "\"default\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [ + "tzdata", +] + +[[packages]] +name = "loguru" +version = "0.7.3" +requires-python = "<4.0,>=3.5" +sdist = {name = "loguru-0.7.3.tar.gz", url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hashes = {sha256 = "19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6"}} +wheels = [ + {name = "loguru-0.7.3-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl",hashes = {sha256 = "31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c"}}, +] +marker = "\"default\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [ + "colorama>=0.3.4; sys_platform == \"win32\"", + "aiocontextvars>=0.2.0; python_version < \"3.7\"", + "win32-setctime>=1.0.0; sys_platform == \"win32\"", +] + +[[packages]] +name = "pillow" +version = "11.3.0" +requires-python = ">=3.9" +sdist = {name = "pillow-11.3.0.tar.gz", url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hashes = {sha256 = "3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523"}} +wheels = [ + {name = "pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl",hashes = {sha256 = "d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12"}}, + {name = "pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a"}}, + {name = "pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632"}}, {name = "pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673"}}, {name = "pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027"}}, {name = "pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77"}}, @@ -849,53 +909,6 @@ wheels = [ {name = "pillow-11.3.0-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl",hashes = {sha256 = "7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149"}}, {name = "pillow-11.3.0-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl",hashes = {sha256 = "a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d"}}, {name = "pillow-11.3.0-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl",hashes = {sha256 = "2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542"}}, - {name = "pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl",url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl",hashes = {sha256 = "1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722"}}, - {name = "pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288"}}, - {name = "pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d"}}, - {name = "pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494"}}, - {name = "pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58"}}, - {name = "pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f"}}, - {name = "pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e"}}, - {name = "pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94"}}, - {name = "pillow-11.3.0-cp311-cp311-win32.whl",url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl",hashes = {sha256 = "b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0"}}, - {name = "pillow-11.3.0-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl",hashes = {sha256 = "1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac"}}, - {name = "pillow-11.3.0-cp311-cp311-win_arm64.whl",url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl",hashes = {sha256 = "30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd"}}, - {name = "pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl",url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl",hashes = {sha256 = "7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6"}}, - {name = "pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl",hashes = {sha256 = "9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438"}}, - {name = "pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3"}}, - {name = "pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c"}}, - {name = "pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361"}}, - {name = "pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7"}}, - {name = "pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl",url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl",hashes = {sha256 = "c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8"}}, - {name = "pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl",url = "https://files.pythonhosted.org/packages/4c/5d/45a3553a253ac8763f3561371432a90bdbe6000fbdcf1397ffe502aa206c/pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl",hashes = {sha256 = "1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860"}}, - {name = "pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/7c/c8/67c12ab069ef586a25a4a79ced553586748fad100c77c0ce59bb4983ac98/pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad"}}, - {name = "pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/2f/bd/6741ebd56263390b382ae4c5de02979af7f8bd9807346d068700dd6d5cf9/pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0"}}, - {name = "pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/ca/0b/c412a9e27e1e6a829e6ab6c2dca52dd563efbedf4c9c6aa453d9a9b77359/pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b"}}, - {name = "pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/59/9d/9b7076aaf30f5dd17e5e5589b2d2f5a5d7e30ff67a171eb686e4eecc2adf/pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50"}}, - {name = "pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/f0/16/1a6bf01fb622fb9cf5c91683823f073f053005c849b1f52ed613afcf8dae/pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae"}}, - {name = "pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/7b/e6/6ff7077077eb47fde78739e7d570bdcd7c10495666b6afcd23ab56b19a43/pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9"}}, - {name = "pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/c3/3a/b13f36832ea6d279a697231658199e0a03cd87ef12048016bdcc84131601/pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e"}}, - {name = "pillow-11.3.0-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/6c/e4/61b2e1a7528740efbc70b3d581f33937e38e98ef3d50b05007267a55bcb2/pillow-11.3.0-cp310-cp310-win32.whl",hashes = {sha256 = "89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6"}}, - {name = "pillow-11.3.0-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/a9/d3/60c781c83a785d6afbd6a326ed4d759d141de43aa7365725cbcd65ce5e54/pillow-11.3.0-cp310-cp310-win_amd64.whl",hashes = {sha256 = "19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f"}}, - {name = "pillow-11.3.0-cp310-cp310-win_arm64.whl",url = "https://files.pythonhosted.org/packages/9f/28/4f4a0203165eefb3763939c6789ba31013a2e90adffb456610f30f613850/pillow-11.3.0-cp310-cp310-win_arm64.whl",hashes = {sha256 = "819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f"}}, - {name = "pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl",url = "https://files.pythonhosted.org/packages/6f/8b/209bd6b62ce8367f47e68a218bffac88888fdf2c9fcf1ecadc6c3ec1ebc7/pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl",hashes = {sha256 = "3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967"}}, - {name = "pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/2e/e6/231a0b76070c2cfd9e260a7a5b504fb72da0a95279410fa7afd99d9751d6/pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl",hashes = {sha256 = "b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe"}}, - {name = "pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/13/f4/10cf94fda33cb12765f2397fc285fa6d8eb9c29de7f3185165b702fc7386/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c"}}, - {name = "pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/72/c9/583821097dc691880c92892e8e2d41fe0a5a3d6021f4963371d2f6d57250/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25"}}, - {name = "pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/3b/8e/5c9d410f9217b12320efc7c413e72693f48468979a013ad17fd690397b9a/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27"}}, - {name = "pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/62/bb/78347dbe13219991877ffb3a91bf09da8317fbfcd4b5f9140aeae020ad71/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a"}}, - {name = "pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl",url = "https://files.pythonhosted.org/packages/d9/28/1000353d5e61498aaeaaf7f1e4b49ddb05f2c6575f9d4f9f914a3538b6e1/pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl",hashes = {sha256 = "8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f"}}, - {name = "pillow-11.3.0-cp39-cp39-macosx_10_10_x86_64.whl",url = "https://files.pythonhosted.org/packages/9e/8e/9c089f01677d1264ab8648352dcb7773f37da6ad002542760c80107da816/pillow-11.3.0-cp39-cp39-macosx_10_10_x86_64.whl",hashes = {sha256 = "48d254f8a4c776de343051023eb61ffe818299eeac478da55227d96e241de53f"}}, - {name = "pillow-11.3.0-cp39-cp39-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/b5/a9/5749930caf674695867eb56a581e78eb5f524b7583ff10b01b6e5048acb3/pillow-11.3.0-cp39-cp39-macosx_11_0_arm64.whl",hashes = {sha256 = "7aee118e30a4cf54fdd873bd3a29de51e29105ab11f9aad8c32123f58c8f8081"}}, - {name = "pillow-11.3.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/43/46/0b85b763eb292b691030795f9f6bb6fcaf8948c39413c81696a01c3577f7/pillow-11.3.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "23cff760a9049c502721bdb743a7cb3e03365fafcdfc2ef9784610714166e5a4"}}, - {name = "pillow-11.3.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/5e/c6/1a230ec0067243cbd60bc2dad5dc3ab46a8a41e21c15f5c9b52b26873069/pillow-11.3.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "6359a3bc43f57d5b375d1ad54a0074318a0844d11b76abccf478c37c986d3cfc"}}, - {name = "pillow-11.3.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/63/dd/f296c27ffba447bfad76c6a0c44c1ea97a90cb9472b9304c94a732e8dbfb/pillow-11.3.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "092c80c76635f5ecb10f3f83d76716165c96f5229addbd1ec2bdbbda7d496e06"}}, - {name = "pillow-11.3.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/a5/a0/98a3630f0b57f77bae67716562513d3032ae70414fcaf02750279c389a9e/pillow-11.3.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "cadc9e0ea0a2431124cde7e1697106471fc4c1da01530e679b2391c37d3fbb3a"}}, - {name = "pillow-11.3.0-cp39-cp39-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/de/e6/83dfba5646a290edd9a21964da07674409e410579c341fc5b8f7abd81620/pillow-11.3.0-cp39-cp39-musllinux_1_2_aarch64.whl",hashes = {sha256 = "6a418691000f2a418c9135a7cf0d797c1bb7d9a485e61fe8e7722845b95ef978"}}, - {name = "pillow-11.3.0-cp39-cp39-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/bc/41/15ab268fe6ee9a2bc7391e2bbb20a98d3974304ab1a406a992dcb297a370/pillow-11.3.0-cp39-cp39-musllinux_1_2_x86_64.whl",hashes = {sha256 = "97afb3a00b65cc0804d1c7abddbf090a81eaac02768af58cbdcaaa0a931e0b6d"}}, - {name = "pillow-11.3.0-cp39-cp39-win32.whl",url = "https://files.pythonhosted.org/packages/64/79/6d4f638b288300bed727ff29f2a3cb63db054b33518a95f27724915e3fbc/pillow-11.3.0-cp39-cp39-win32.whl",hashes = {sha256 = "ea944117a7974ae78059fcc1800e5d3295172bb97035c0c1d9345fca1419da71"}}, - {name = "pillow-11.3.0-cp39-cp39-win_amd64.whl",url = "https://files.pythonhosted.org/packages/46/05/4106422f45a05716fd34ed21763f8ec182e8ea00af6e9cb05b93a247361a/pillow-11.3.0-cp39-cp39-win_amd64.whl",hashes = {sha256 = "e5c5858ad8ec655450a7c7df532e9842cf8df7cc349df7225c60d5d348c8aada"}}, - {name = "pillow-11.3.0-cp39-cp39-win_arm64.whl",url = "https://files.pythonhosted.org/packages/63/c6/287fd55c2c12761d0591549d48885187579b7c257bef0c6660755b0b59ae/pillow-11.3.0-cp39-cp39-win_arm64.whl",hashes = {sha256 = "6abdbfd3aea42be05702a8dd98832329c167ee84400a1d1f61ab11437f1717eb"}}, ] marker = "\"default\" in dependency_groups" @@ -914,8 +927,6 @@ wheels = [ {name = "protobuf-6.31.1-cp39-abi3-manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/76/a1/7a5a94032c83375e4fe7e7f56e3976ea6ac90c5e85fac8576409e25c39c3/protobuf-6.31.1-cp39-abi3-manylinux2014_aarch64.whl",hashes = {sha256 = "a40fc12b84c154884d7d4c4ebd675d5b3b5283e155f324049ae396b95ddebc39"}}, {name = "protobuf-6.31.1-cp39-abi3-manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/fa/b1/b59d405d64d31999244643d88c45c8241c58f17cc887e73bcb90602327f8/protobuf-6.31.1-cp39-abi3-manylinux2014_x86_64.whl",hashes = {sha256 = "4ee898bf66f7a8b0bd21bce523814e6fbd8c6add948045ce958b73af7e8878c6"}}, {name = "protobuf-6.31.1-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/f7/af/ab3c51ab7507a7325e98ffe691d9495ee3d3aa5f589afad65ec920d39821/protobuf-6.31.1-py3-none-any.whl",hashes = {sha256 = "720a6c7e6b77288b85063569baae8536671b39f15cc22037ec7045658d80489e"}}, - {name = "protobuf-6.31.1-cp39-cp39-win32.whl",url = "https://files.pythonhosted.org/packages/b1/f0/4160dbd205eee8fdf8647d154e7ceaa9d25b3a877b6311274eb6dc896b75/protobuf-6.31.1-cp39-cp39-win32.whl",hashes = {sha256 = "0414e3aa5a5f3ff423828e1e6a6e907d6c65c1d5b7e6e975793d5590bdeecc16"}}, - {name = "protobuf-6.31.1-cp39-cp39-win_amd64.whl",url = "https://files.pythonhosted.org/packages/09/34/13989eb9f482409ed821bfa3e34e6a3878b42607c38e7f7572b4cc825091/protobuf-6.31.1-cp39-cp39-win_amd64.whl",hashes = {sha256 = "8764cf4587791e7564051b35524b72844f845ad0bb011704c3736cce762d8fe9"}}, ] marker = "\"default\" in dependency_groups" @@ -939,6 +950,31 @@ dependencies = [ "typing-extensions<5.0,>=4.0.0; python_version < \"3.11\"", ] +[[packages]] +name = "sanic" +version = "25.3.0" +requires-python = ">=3.8" +sdist = {name = "sanic-25.3.0.tar.gz", url = "https://files.pythonhosted.org/packages/df/8b/08dc376390fe854ef32984973883b646ee68c6727da72ffcc65340d8f192/sanic-25.3.0.tar.gz", hashes = {sha256 = "775d522001ec81f034ec8e4d7599e2175bfc097b8d57884f5e4c9322f5e369bb"}} +wheels = [ + {name = "sanic-25.3.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/a6/e1/b36ddc16862d63d22986ae21b04a79c8fb7ec48d5d664acdfd1c2acf78ac/sanic-25.3.0-py3-none-any.whl",hashes = {sha256 = "fb519b38b4c220569b0e2e868583ffeaffaab96a78b2e42ae78bc56a644a4cd7"}}, +] +marker = "\"default\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [ + "sanic-routing>=23.12.0", + "httptools>=0.0.10", + "uvloop>=0.15.0; sys_platform != \"win32\" and implementation_name == \"cpython\"", + "ujson>=1.35; sys_platform != \"win32\" and implementation_name == \"cpython\"", + "aiofiles>=0.6.0", + "websockets>=10.0", + "multidict<7.0,>=5.0", + "html5tagger>=1.2.1", + "tracerite>=1.0.0", + "typing-extensions>=4.4.0", + "setuptools>=70.1.0", +] + [[packages]] name = "transformers" version = "4.53.1" @@ -1047,73 +1083,6 @@ wheels = [ {name = "pydantic_core-2.33.2-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl",hashes = {sha256 = "9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290"}}, {name = "pydantic_core-2.33.2-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl",hashes = {sha256 = "f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2"}}, {name = "pydantic_core-2.33.2-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl",hashes = {sha256 = "cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab"}}, - {name = "pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl",hashes = {sha256 = "4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7"}}, - {name = "pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246"}}, - {name = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f"}}, - {name = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",hashes = {sha256 = "a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc"}}, - {name = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de"}}, - {name = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a"}}, - {name = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef"}}, - {name = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl",url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl",hashes = {sha256 = "bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e"}}, - {name = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl",url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl",hashes = {sha256 = "fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d"}}, - {name = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl",url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl",hashes = {sha256 = "bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30"}}, - {name = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl",hashes = {sha256 = "235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf"}}, - {name = "pydantic_core-2.33.2-cp311-cp311-win32.whl",url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl",hashes = {sha256 = "6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51"}}, - {name = "pydantic_core-2.33.2-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl",hashes = {sha256 = "1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab"}}, - {name = "pydantic_core-2.33.2-cp311-cp311-win_arm64.whl",url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl",hashes = {sha256 = "6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65"}}, - {name = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl",hashes = {sha256 = "dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8"}}, - {name = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl",hashes = {sha256 = "d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593"}}, - {name = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612"}}, - {name = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7"}}, - {name = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl",url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl",hashes = {sha256 = "64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e"}}, - {name = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl",url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl",hashes = {sha256 = "f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8"}}, - {name = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl",url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl",hashes = {sha256 = "de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf"}}, - {name = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl",hashes = {sha256 = "82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb"}}, - {name = "pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl",url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl",hashes = {sha256 = "329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1"}}, - {name = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/e5/92/b31726561b5dae176c2d2c2dc43a9c5bfba5d32f96f8b4c0a600dd492447/pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl",hashes = {sha256 = "2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"}}, - {name = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/a3/44/3f0b95fafdaca04a483c4e685fe437c6891001bf3ce8b2fded82b9ea3aa1/pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"}}, - {name = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/30/97/e8f13b55766234caae05372826e8e4b3b96e7b248be3157f53237682e43c/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d"}}, - {name = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",url = "https://files.pythonhosted.org/packages/9b/a3/99c48cf7bafc991cc3ee66fd544c0aae8dc907b752f1dad2d79b1b5a471f/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",hashes = {sha256 = "d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572"}}, - {name = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/de/8e/a5b882ec4307010a840fb8b58bd9bf65d1840c92eae7534c7441709bf54b/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02"}}, - {name = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/e4/bb/71e35fc3ed05af6834e890edb75968e2802fe98778971ab5cba20a162315/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b"}}, - {name = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/31/0d/c8f7593e6bc7066289bbc366f2235701dcbebcd1ff0ef8e64f6f239fb47d/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2"}}, - {name = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl",url = "https://files.pythonhosted.org/packages/d2/7a/996d8bd75f3eda405e3dd219ff5ff0a283cd8e34add39d8ef9157e722867/pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl",hashes = {sha256 = "efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a"}}, - {name = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl",url = "https://files.pythonhosted.org/packages/ff/84/daf2a6fb2db40ffda6578a7e8c5a6e9c8affb251a05c233ae37098118788/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl",hashes = {sha256 = "031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac"}}, - {name = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl",url = "https://files.pythonhosted.org/packages/77/fb/2258da019f4825128445ae79456a5499c032b55849dbd5bed78c95ccf163/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl",hashes = {sha256 = "f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a"}}, - {name = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/d8/7a/925ff73756031289468326e355b6fa8316960d0d65f8b5d6b3a3e7866de7/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl",hashes = {sha256 = "73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b"}}, - {name = "pydantic_core-2.33.2-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/0b/b0/249ee6d2646f1cdadcb813805fe76265745c4010cf20a8eba7b0e639d9b2/pydantic_core-2.33.2-cp310-cp310-win32.whl",hashes = {sha256 = "0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22"}}, - {name = "pydantic_core-2.33.2-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/66/ff/172ba8f12a42d4b552917aa65d1f2328990d3ccfc01d5b7c943ec084299f/pydantic_core-2.33.2-cp310-cp310-win_amd64.whl",hashes = {sha256 = "b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640"}}, - {name = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/30/68/373d55e58b7e83ce371691f6eaa7175e3a24b956c44628eb25d7da007917/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl",hashes = {sha256 = "5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa"}}, - {name = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/a4/16/145f54ac08c96a63d8ed6442f9dec17b2773d19920b627b18d4f10a061ea/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl",hashes = {sha256 = "d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29"}}, - {name = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/41/b1/c6dc6c3e2de4516c0bb2c46f6a373b91b5660312342a0cf5826e38ad82fa/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d"}}, - {name = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/12/73/8cd57e20afba760b21b742106f9dbdfa6697f1570b189c7457a1af4cd8a0/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e"}}, - {name = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl",url = "https://files.pythonhosted.org/packages/e3/d5/0bb5d988cc019b3cba4a78f2d4b3854427fc47ee8ec8e9eaabf787da239c/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl",hashes = {sha256 = "2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c"}}, - {name = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl",url = "https://files.pythonhosted.org/packages/f1/c5/00c02d1571913d496aabf146106ad8239dc132485ee22efe08085084ff7c/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl",hashes = {sha256 = "0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec"}}, - {name = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl",url = "https://files.pythonhosted.org/packages/22/a8/dccc38768274d3ed3a59b5d06f59ccb845778687652daa71df0cab4040d7/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl",hashes = {sha256 = "1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052"}}, - {name = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/d4/e7/4f98c0b125dda7cf7ccd14ba936218397b44f50a56dd8c16a3091df116c3/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl",hashes = {sha256 = "fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c"}}, - {name = "pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl",url = "https://files.pythonhosted.org/packages/ce/91/2ec36480fdb0b783cd9ef6795753c1dea13882f2e68e73bce76ae8c21e6a/pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl",hashes = {sha256 = "a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808"}}, - {name = "pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/53/ea/bbe9095cdd771987d13c82d104a9c8559ae9aec1e29f139e286fd2e9256e/pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl",hashes = {sha256 = "a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d"}}, - {name = "pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/49/1d/4ac5ed228078737d457a609013e8f7edc64adc37b91d619ea965758369e5/pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl",hashes = {sha256 = "6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954"}}, - {name = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/23/9a/2e70d6388d7cda488ae38f57bc2f7b03ee442fbcf0d75d848304ac7e405b/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb"}}, - {name = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",url = "https://files.pythonhosted.org/packages/ff/2e/1568934feb43370c1ffb78a77f0baaa5a8b6897513e7a91051af707ffdc4/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",hashes = {sha256 = "53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7"}}, - {name = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/01/1a/1a1118f38ab64eac2f6269eb8c120ab915be30e387bb561e3af904b12499/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4"}}, - {name = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/5c/da/44754d1d7ae0f22d6d3ce6c6b1486fc07ac2c524ed8f6eca636e2e1ee49b/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b"}}, - {name = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/4d/98/f43cd89172220ec5aa86654967b22d862146bc4d736b1350b4c41e7c9c03/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3"}}, - {name = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl",url = "https://files.pythonhosted.org/packages/2b/cc/f77e8e242171d2158309f830f7d5d07e0531b756106f36bc18712dc439df/pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl",hashes = {sha256 = "c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a"}}, - {name = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl",url = "https://files.pythonhosted.org/packages/54/7a/7be6a7bd43e0a47c147ba7fbf124fe8aaf1200bc587da925509641113b2d/pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl",hashes = {sha256 = "44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782"}}, - {name = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl",url = "https://files.pythonhosted.org/packages/2a/07/31cf8fadffbb03be1cb520850e00a8490c0927ec456e8293cafda0726184/pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl",hashes = {sha256 = "eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9"}}, - {name = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/b6/8d/bbaf4c6721b668d44f01861f297eb01c9b35f612f6b8e14173cb204e6240/pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl",hashes = {sha256 = "9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e"}}, - {name = "pydantic_core-2.33.2-cp39-cp39-win32.whl",url = "https://files.pythonhosted.org/packages/bb/93/3cc157026bca8f5006250e74515119fcaa6d6858aceee8f67ab6dc548c16/pydantic_core-2.33.2-cp39-cp39-win32.whl",hashes = {sha256 = "83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9"}}, - {name = "pydantic_core-2.33.2-cp39-cp39-win_amd64.whl",url = "https://files.pythonhosted.org/packages/5b/90/7edc3b2a0d9f0dda8806c04e511a67b0b7a41d2187e2003673a996fb4310/pydantic_core-2.33.2-cp39-cp39-win_amd64.whl",hashes = {sha256 = "f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3"}}, - {name = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/08/98/dbf3fdfabaf81cda5622154fda78ea9965ac467e3239078e0dcd6df159e7/pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl",hashes = {sha256 = "87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101"}}, - {name = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/8d/99/7810aa9256e7f2ccd492590f86b79d370df1e9292f1f80b000b6a75bd2fb/pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl",hashes = {sha256 = "7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64"}}, - {name = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/d8/60/bc06fa9027c7006cc6dd21e48dbf39076dc39d9abbaf718a1604973a9670/pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d"}}, - {name = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/f2/40/9d03997d9518816c68b4dfccb88969756b9146031b61cd37f781c74c9b6a/pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535"}}, - {name = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl",url = "https://files.pythonhosted.org/packages/d8/62/d490198d05d2d86672dc269f52579cad7261ced64c2df213d5c16e0aecb1/pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl",hashes = {sha256 = "970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d"}}, - {name = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl",url = "https://files.pythonhosted.org/packages/9a/ec/4cd215534fd10b8549015f12ea650a1a973da20ce46430b68fc3185573e8/pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl",hashes = {sha256 = "3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6"}}, - {name = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl",url = "https://files.pythonhosted.org/packages/1a/1a/abbd63d47e1d9b0d632fee6bb15785d0889c8a6e0a6c3b5a8e28ac1ec5d2/pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl",hashes = {sha256 = "3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca"}}, - {name = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/80/1c/fa883643429908b1c90598fd2642af8839efd1d835b65af1f75fba4d94fe/pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl",hashes = {sha256 = "3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039"}}, - {name = "pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl",url = "https://files.pythonhosted.org/packages/d4/29/3cade8a924a61f60ccfa10842f75eb12787e1440e2b8660ceffeb26685e7/pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl",hashes = {sha256 = "2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27"}}, ] marker = "\"default\" in dependency_groups" @@ -1122,49 +1091,6 @@ dependencies = [ "typing-extensions!=4.7.0,>=4.6.0", ] -[[packages]] -name = "tomli" -version = "2.2.1" -requires-python = ">=3.8" -sdist = {name = "tomli-2.2.1.tar.gz", url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hashes = {sha256 = "cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}} -wheels = [ - {name = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl",hashes = {sha256 = "f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}}, - {name = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}}, - {name = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}}, - {name = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}}, - {name = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}}, - {name = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}}, - {name = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl",hashes = {sha256 = "d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}}, - {name = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}}, - {name = "tomli-2.2.1-cp313-cp313-win32.whl",url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl",hashes = {sha256 = "d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}}, - {name = "tomli-2.2.1-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl",hashes = {sha256 = "a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}}, - {name = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl",hashes = {sha256 = "4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}}, - {name = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}}, - {name = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}}, - {name = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}}, - {name = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}}, - {name = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}}, - {name = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl",hashes = {sha256 = "02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}}, - {name = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}}, - {name = "tomli-2.2.1-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl",hashes = {sha256 = "889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}}, - {name = "tomli-2.2.1-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl",hashes = {sha256 = "7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}}, - {name = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl",hashes = {sha256 = "678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}}, - {name = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}}, - {name = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}}, - {name = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}}, - {name = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}}, - {name = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}}, - {name = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl",hashes = {sha256 = "e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}}, - {name = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}}, - {name = "tomli-2.2.1-cp311-cp311-win32.whl",url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl",hashes = {sha256 = "465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}}, - {name = "tomli-2.2.1-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl",hashes = {sha256 = "2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}}, - {name = "tomli-2.2.1-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl",hashes = {sha256 = "cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}}, -] -marker = "python_version < \"3.11\" and python_version >= \"3.9\" and \"dev\" in extras" - -[packages.tool.pdm] -dependencies = [] - [[packages]] name = "typing-extensions" version = "4.14.1" @@ -1358,46 +1284,7 @@ wheels = [ {name = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f"}}, {name = "charset_normalizer-3.4.2-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl",hashes = {sha256 = "db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c"}}, {name = "charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl",hashes = {sha256 = "5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e"}}, - {name = "charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl",url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl",hashes = {sha256 = "be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2"}}, - {name = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645"}}, - {name = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd"}}, - {name = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8"}}, - {name = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f"}}, - {name = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7"}}, - {name = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9"}}, - {name = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl",hashes = {sha256 = "f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544"}}, - {name = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82"}}, - {name = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl",hashes = {sha256 = "0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0"}}, - {name = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5"}}, - {name = "charset_normalizer-3.4.2-cp311-cp311-win32.whl",url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl",hashes = {sha256 = "daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a"}}, - {name = "charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl",hashes = {sha256 = "e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28"}}, - {name = "charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl",url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl",hashes = {sha256 = "7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941"}}, - {name = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd"}}, - {name = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6"}}, - {name = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d"}}, - {name = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86"}}, - {name = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c"}}, - {name = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0"}}, - {name = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl",hashes = {sha256 = "b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef"}}, - {name = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6"}}, - {name = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl",hashes = {sha256 = "68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366"}}, - {name = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db"}}, - {name = "charset_normalizer-3.4.2-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl",hashes = {sha256 = "e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a"}}, - {name = "charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl",hashes = {sha256 = "f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509"}}, {name = "charset_normalizer-3.4.2-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl",hashes = {sha256 = "7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0"}}, - {name = "charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl",url = "https://files.pythonhosted.org/packages/28/f8/dfb01ff6cc9af38552c69c9027501ff5a5117c4cc18dcd27cb5259fa1888/charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl",hashes = {sha256 = "005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4"}}, - {name = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/32/fb/74e26ee556a9dbfe3bd264289b67be1e6d616329403036f6507bb9f3f29c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7"}}, - {name = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/ad/06/8499ee5aa7addc6f6d72e068691826ff093329fe59891e83b092ae4c851c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836"}}, - {name = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/f1/a2/5e4c187680728219254ef107a6949c60ee0e9a916a5dadb148c7ae82459c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597"}}, - {name = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/4c/fe/56aca740dda674f0cc1ba1418c4d84534be51f639b5f98f538b332dc9a95/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7"}}, - {name = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/53/13/db2e7779f892386b589173dd689c1b1e304621c5792046edd8a978cbf9e0/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f"}}, - {name = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/69/35/e52ab9a276186f729bce7a0638585d2982f50402046e4b0faa5d2c3ef2da/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl",hashes = {sha256 = "dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba"}}, - {name = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/a6/d8/af7333f732fc2e7635867d56cb7c349c28c7094910c72267586947561b4b/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl",hashes = {sha256 = "aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12"}}, - {name = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/7a/3d/a5b2e48acef264d71e036ff30bcc49e51bde80219bb628ba3e00cf59baac/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518"}}, - {name = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/85/d8/23e2c112532a29f3eef374375a8684a4f3b8e784f62b01da931186f43494/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl",hashes = {sha256 = "c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5"}}, - {name = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/c7/57/93e0169f08ecc20fe82d12254a200dfaceddc1c12a4077bf454ecc597e33/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl",hashes = {sha256 = "982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3"}}, - {name = "charset_normalizer-3.4.2-cp39-cp39-win32.whl",url = "https://files.pythonhosted.org/packages/2c/9d/9bf2b005138e7e060d7ebdec7503d0ef3240141587651f4b445bdf7286c2/charset_normalizer-3.4.2-cp39-cp39-win32.whl",hashes = {sha256 = "43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471"}}, - {name = "charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl",url = "https://files.pythonhosted.org/packages/6d/24/5849d46cf4311bbf21b424c443b09b459f5b436b1558c04e45dbb7cc478b/charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl",hashes = {sha256 = "d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e"}}, ] marker = "\"default\" in dependency_groups or \"dev\" in extras or \"recommended\" in extras" @@ -1496,57 +1383,6 @@ wheels = [ {name = "aiohttp-3.12.14-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/22/32/5501ab525a47ba23c20613e568174d6c63aa09e2caa22cded5c6ea8e3ada/aiohttp-3.12.14-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "2785b112346e435dd3a1a67f67713a3fe692d288542f1347ad255683f066d8e0"}}, {name = "aiohttp-3.12.14-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/06/af/28e24574801fcf1657945347ee10df3892311c2829b41232be6089e461e7/aiohttp-3.12.14-cp312-cp312-win32.whl",hashes = {sha256 = "15f5f4792c9c999a31d8decf444e79fcfd98497bf98e94284bf390a7bb8c1729"}}, {name = "aiohttp-3.12.14-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/98/d5/7ac2464aebd2eecac38dbe96148c9eb487679c512449ba5215d233755582/aiohttp-3.12.14-cp312-cp312-win_amd64.whl",hashes = {sha256 = "3b66e1a182879f579b105a80d5c4bd448b91a57e8933564bf41665064796a338"}}, - {name = "aiohttp-3.12.14-cp311-cp311-macosx_10_9_universal2.whl",url = "https://files.pythonhosted.org/packages/53/e1/8029b29316971c5fa89cec170274582619a01b3d82dd1036872acc9bc7e8/aiohttp-3.12.14-cp311-cp311-macosx_10_9_universal2.whl",hashes = {sha256 = "f4552ff7b18bcec18b60a90c6982049cdb9dac1dba48cf00b97934a06ce2e597"}}, - {name = "aiohttp-3.12.14-cp311-cp311-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/96/bd/4f204cf1e282041f7b7e8155f846583b19149e0872752711d0da5e9cc023/aiohttp-3.12.14-cp311-cp311-macosx_10_9_x86_64.whl",hashes = {sha256 = "8283f42181ff6ccbcf25acaae4e8ab2ff7e92b3ca4a4ced73b2c12d8cd971393"}}, - {name = "aiohttp-3.12.14-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/d6/0f/2a580fcdd113fe2197a3b9df30230c7e85bb10bf56f7915457c60e9addd9/aiohttp-3.12.14-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "040afa180ea514495aaff7ad34ec3d27826eaa5d19812730fe9e529b04bb2179"}}, - {name = "aiohttp-3.12.14-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/38/78/2c1089f6adca90c3dd74915bafed6d6d8a87df5e3da74200f6b3a8b8906f/aiohttp-3.12.14-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "b413c12f14c1149f0ffd890f4141a7471ba4b41234fe4fd4a0ff82b1dc299dbb"}}, - {name = "aiohttp-3.12.14-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl",url = "https://files.pythonhosted.org/packages/4a/c8/ce6c7a34d9c589f007cfe064da2d943b3dee5aabc64eaecd21faf927ab11/aiohttp-3.12.14-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl",hashes = {sha256 = "1d6f607ce2e1a93315414e3d448b831238f1874b9968e1195b06efaa5c87e245"}}, - {name = "aiohttp-3.12.14-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/18/10/431cd3d089de700756a56aa896faf3ea82bee39d22f89db7ddc957580308/aiohttp-3.12.14-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "565e70d03e924333004ed101599902bba09ebb14843c8ea39d657f037115201b"}}, - {name = "aiohttp-3.12.14-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/fa/b2/26f4524184e0f7ba46671c512d4b03022633bcf7d32fa0c6f1ef49d55800/aiohttp-3.12.14-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "4699979560728b168d5ab63c668a093c9570af2c7a78ea24ca5212c6cdc2b641"}}, - {name = "aiohttp-3.12.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/e0/30/aadcdf71b510a718e3d98a7bfeaea2396ac847f218b7e8edb241b09bd99a/aiohttp-3.12.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "ad5fdf6af93ec6c99bf800eba3af9a43d8bfd66dce920ac905c817ef4a712afe"}}, - {name = "aiohttp-3.12.14-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/67/7f/7ccf11756ae498fdedc3d689a0c36ace8fc82f9d52d3517da24adf6e9a74/aiohttp-3.12.14-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "4ac76627c0b7ee0e80e871bde0d376a057916cb008a8f3ffc889570a838f5cc7"}}, - {name = "aiohttp-3.12.14-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/6b/4d/35ebc170b1856dd020c92376dbfe4297217625ef4004d56587024dc2289c/aiohttp-3.12.14-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "798204af1180885651b77bf03adc903743a86a39c7392c472891649610844635"}}, - {name = "aiohttp-3.12.14-cp311-cp311-musllinux_1_2_armv7l.whl",url = "https://files.pythonhosted.org/packages/7b/24/46dc0380146f33e2e4aa088b92374b598f5bdcde1718c77e8d1a0094f1a4/aiohttp-3.12.14-cp311-cp311-musllinux_1_2_armv7l.whl",hashes = {sha256 = "4f1205f97de92c37dd71cf2d5bcfb65fdaed3c255d246172cce729a8d849b4da"}}, - {name = "aiohttp-3.12.14-cp311-cp311-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/2f/0a/46599d7d19b64f4d0fe1b57bdf96a9a40b5c125f0ae0d8899bc22e91fdce/aiohttp-3.12.14-cp311-cp311-musllinux_1_2_i686.whl",hashes = {sha256 = "76ae6f1dd041f85065d9df77c6bc9c9703da9b5c018479d20262acc3df97d419"}}, - {name = "aiohttp-3.12.14-cp311-cp311-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/08/86/b21b682e33d5ca317ef96bd21294984f72379454e689d7da584df1512a19/aiohttp-3.12.14-cp311-cp311-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "a194ace7bc43ce765338ca2dfb5661489317db216ea7ea700b0332878b392cab"}}, - {name = "aiohttp-3.12.14-cp311-cp311-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/4f/45/f639482530b1396c365f23c5e3b1ae51c9bc02ba2b2248ca0c855a730059/aiohttp-3.12.14-cp311-cp311-musllinux_1_2_s390x.whl",hashes = {sha256 = "16260e8e03744a6fe3fcb05259eeab8e08342c4c33decf96a9dad9f1187275d0"}}, - {name = "aiohttp-3.12.14-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/7e/e5/39635a9e06eed1d73671bd4079a3caf9cf09a49df08490686f45a710b80e/aiohttp-3.12.14-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "8c779e5ebbf0e2e15334ea404fcce54009dc069210164a244d2eac8352a44b28"}}, - {name = "aiohttp-3.12.14-cp311-cp311-win32.whl",url = "https://files.pythonhosted.org/packages/51/e1/7f1c77515d369b7419c5b501196526dad3e72800946c0099594c1f0c20b4/aiohttp-3.12.14-cp311-cp311-win32.whl",hashes = {sha256 = "a289f50bf1bd5be227376c067927f78079a7bdeccf8daa6a9e65c38bae14324b"}}, - {name = "aiohttp-3.12.14-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/06/24/a6bf915c85b7a5b07beba3d42b3282936b51e4578b64a51e8e875643c276/aiohttp-3.12.14-cp311-cp311-win_amd64.whl",hashes = {sha256 = "0b8a69acaf06b17e9c54151a6c956339cf46db4ff72b3ac28516d0f7068f4ced"}}, - {name = "aiohttp-3.12.14-cp310-cp310-macosx_10_9_universal2.whl",url = "https://files.pythonhosted.org/packages/0c/88/f161f429f9de391eee6a5c2cffa54e2ecd5b7122ae99df247f7734dfefcb/aiohttp-3.12.14-cp310-cp310-macosx_10_9_universal2.whl",hashes = {sha256 = "906d5075b5ba0dd1c66fcaaf60eb09926a9fef3ca92d912d2a0bbdbecf8b1248"}}, - {name = "aiohttp-3.12.14-cp310-cp310-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/fe/b5/24fa382a69a25d242e2baa3e56d5ea5227d1b68784521aaf3a1a8b34c9a4/aiohttp-3.12.14-cp310-cp310-macosx_10_9_x86_64.whl",hashes = {sha256 = "c875bf6fc2fd1a572aba0e02ef4e7a63694778c5646cdbda346ee24e630d30fb"}}, - {name = "aiohttp-3.12.14-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/09/67/fda1bc34adbfaa950d98d934a23900918f9d63594928c70e55045838c943/aiohttp-3.12.14-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "fbb284d15c6a45fab030740049d03c0ecd60edad9cd23b211d7e11d3be8d56fd"}}, - {name = "aiohttp-3.12.14-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/36/96/3ce1ea96d3cf6928b87cfb8cdd94650367f5c2f36e686a1f5568f0f13754/aiohttp-3.12.14-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "38e360381e02e1a05d36b223ecab7bc4a6e7b5ab15760022dc92589ee1d4238c"}}, - {name = "aiohttp-3.12.14-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl",url = "https://files.pythonhosted.org/packages/be/04/ddea06cb4bc7d8db3745cf95e2c42f310aad485ca075bd685f0e4f0f6b65/aiohttp-3.12.14-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl",hashes = {sha256 = "aaf90137b5e5d84a53632ad95ebee5c9e3e7468f0aab92ba3f608adcb914fa95"}}, - {name = "aiohttp-3.12.14-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/73/66/63942f104d33ce6ca7871ac6c1e2ebab48b88f78b2b7680c37de60f5e8cd/aiohttp-3.12.14-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "e532a25e4a0a2685fa295a31acf65e027fbe2bea7a4b02cdfbbba8a064577663"}}, - {name = "aiohttp-3.12.14-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/20/00/aab615742b953f04b48cb378ee72ada88555b47b860b98c21c458c030a23/aiohttp-3.12.14-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "eab9762c4d1b08ae04a6c77474e6136da722e34fdc0e6d6eab5ee93ac29f35d1"}}, - {name = "aiohttp-3.12.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/d6/4f/ef6d9f77225cf27747368c37b3d69fac1f8d6f9d3d5de2d410d155639524/aiohttp-3.12.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "abe53c3812b2899889a7fca763cdfaeee725f5be68ea89905e4275476ffd7e61"}}, - {name = "aiohttp-3.12.14-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/37/e1/e98a43c15aa52e9219a842f18c59cbae8bbe2d50c08d298f17e9e8bafa38/aiohttp-3.12.14-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "5760909b7080aa2ec1d320baee90d03b21745573780a072b66ce633eb77a8656"}}, - {name = "aiohttp-3.12.14-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/71/5c/29c6dfb49323bcdb0239bf3fc97ffcf0eaf86d3a60426a3287ec75d67721/aiohttp-3.12.14-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "02fcd3f69051467bbaa7f84d7ec3267478c7df18d68b2e28279116e29d18d4f3"}}, - {name = "aiohttp-3.12.14-cp310-cp310-musllinux_1_2_armv7l.whl",url = "https://files.pythonhosted.org/packages/79/60/ec90782084090c4a6b459790cfd8d17be2c5662c9c4b2d21408b2f2dc36c/aiohttp-3.12.14-cp310-cp310-musllinux_1_2_armv7l.whl",hashes = {sha256 = "4dcd1172cd6794884c33e504d3da3c35648b8be9bfa946942d353b939d5f1288"}}, - {name = "aiohttp-3.12.14-cp310-cp310-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/22/89/205d3ad30865c32bc472ac13f94374210745b05bd0f2856996cb34d53396/aiohttp-3.12.14-cp310-cp310-musllinux_1_2_i686.whl",hashes = {sha256 = "224d0da41355b942b43ad08101b1b41ce633a654128ee07e36d75133443adcda"}}, - {name = "aiohttp-3.12.14-cp310-cp310-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/48/ae/2f66edaa8bd6db2a4cba0386881eb92002cdc70834e2a93d1d5607132c7e/aiohttp-3.12.14-cp310-cp310-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "e387668724f4d734e865c1776d841ed75b300ee61059aca0b05bce67061dcacc"}}, - {name = "aiohttp-3.12.14-cp310-cp310-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/08/3a/fa73bfc6e21407ea57f7906a816f0dc73663d9549da703be05dbd76d2dc3/aiohttp-3.12.14-cp310-cp310-musllinux_1_2_s390x.whl",hashes = {sha256 = "dec9cde5b5a24171e0b0a4ca064b1414950904053fb77c707efd876a2da525d8"}}, - {name = "aiohttp-3.12.14-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/e3/b3/751124b8ceb0831c17960d06ee31a4732cb4a6a006fdbfa1153d07c52226/aiohttp-3.12.14-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "bbad68a2af4877cc103cd94af9160e45676fc6f0c14abb88e6e092b945c2c8e3"}}, - {name = "aiohttp-3.12.14-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/81/3c/72477a1d34edb8ab8ce8013086a41526d48b64f77e381c8908d24e1c18f5/aiohttp-3.12.14-cp310-cp310-win32.whl",hashes = {sha256 = "ee580cb7c00bd857b3039ebca03c4448e84700dc1322f860cf7a500a6f62630c"}}, - {name = "aiohttp-3.12.14-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/a2/c4/8aec4ccf1b822ec78e7982bd5cf971113ecce5f773f04039c76a083116fc/aiohttp-3.12.14-cp310-cp310-win_amd64.whl",hashes = {sha256 = "cf4f05b8cea571e2ccc3ca744e35ead24992d90a72ca2cf7ab7a2efbac6716db"}}, - {name = "aiohttp-3.12.14-cp39-cp39-macosx_10_9_universal2.whl",url = "https://files.pythonhosted.org/packages/cf/54/8a65095784f5c8b2a60a8baa2baabb15b8d507efb0911d59f94af04ba908/aiohttp-3.12.14-cp39-cp39-macosx_10_9_universal2.whl",hashes = {sha256 = "b8cc6b05e94d837bcd71c6531e2344e1ff0fb87abe4ad78a9261d67ef5d83eae"}}, - {name = "aiohttp-3.12.14-cp39-cp39-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/d0/23/65a82d33841c790178aed8aa6b5e720e37f08bdf7256936fa3bc86f03257/aiohttp-3.12.14-cp39-cp39-macosx_10_9_x86_64.whl",hashes = {sha256 = "d1dcb015ac6a3b8facd3677597edd5ff39d11d937456702f0bb2b762e390a21b"}}, - {name = "aiohttp-3.12.14-cp39-cp39-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/10/66/9d51ec40613aca2f38d6ac527b592686a302197109aa1c0fe045040835ec/aiohttp-3.12.14-cp39-cp39-macosx_11_0_arm64.whl",hashes = {sha256 = "3779ed96105cd70ee5e85ca4f457adbce3d9ff33ec3d0ebcdf6c5727f26b21b3"}}, - {name = "aiohttp-3.12.14-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/48/9e/2f14e4780a461351325d7821fb64e9107189315dd8f6e8a67e7afdbf875c/aiohttp-3.12.14-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "717a0680729b4ebd7569c1dcd718c46b09b360745fd8eb12317abc74b14d14d0"}}, - {name = "aiohttp-3.12.14-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl",url = "https://files.pythonhosted.org/packages/b8/26/26ef03e6cc4b7fb275eaa76b33c128f72729e8833e512b6770f877560b6e/aiohttp-3.12.14-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl",hashes = {sha256 = "b5dd3a2ef7c7e968dbbac8f5574ebeac4d2b813b247e8cec28174a2ba3627170"}}, - {name = "aiohttp-3.12.14-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/68/cf/fffc2a9edacbd475cfb508075bad052426ce0b9100f1045536ee1b683872/aiohttp-3.12.14-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "4710f77598c0092239bc12c1fcc278a444e16c7032d91babf5abbf7166463f7b"}}, - {name = "aiohttp-3.12.14-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/0b/c5/bb8b29ef079d3ecb5960ec1b547b56bc52ee5ffc43c8a30ef21f9afeb67b/aiohttp-3.12.14-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "f3e9f75ae842a6c22a195d4a127263dbf87cbab729829e0bd7857fb1672400b2"}}, - {name = "aiohttp-3.12.14-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/09/0d/d18e2d2754497bf91b9559425e8c4286af61bdbe42d49c43d955c7269680/aiohttp-3.12.14-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "5f9c8d55d6802086edd188e3a7d85a77787e50d56ce3eb4757a3205fa4657922"}}, - {name = "aiohttp-3.12.14-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/33/c8/2c32cd25deb9f590cb8d50ff33fb3bb2cc8d1761958989f6f64cf00ef1cb/aiohttp-3.12.14-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "79b29053ff3ad307880d94562cca80693c62062a098a5776ea8ef5ef4b28d140"}}, - {name = "aiohttp-3.12.14-cp39-cp39-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/0f/36/1b36ae47b9d6afdd39072373bb7157b464996376d562d3c50950ddf6d10e/aiohttp-3.12.14-cp39-cp39-musllinux_1_2_aarch64.whl",hashes = {sha256 = "23e1332fff36bebd3183db0c7a547a1da9d3b4091509f6d818e098855f2f27d3"}}, - {name = "aiohttp-3.12.14-cp39-cp39-musllinux_1_2_armv7l.whl",url = "https://files.pythonhosted.org/packages/2b/e8/6864b7812351821168e80ca102d7fa244a78fefe9690995a40e8b5c19f4b/aiohttp-3.12.14-cp39-cp39-musllinux_1_2_armv7l.whl",hashes = {sha256 = "a564188ce831fd110ea76bcc97085dd6c625b427db3f1dbb14ca4baa1447dcbc"}}, - {name = "aiohttp-3.12.14-cp39-cp39-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/9b/55/f90e3eb25330f8a564a6e6b4d3cc15d3630bd28b0795a025e397e3279411/aiohttp-3.12.14-cp39-cp39-musllinux_1_2_i686.whl",hashes = {sha256 = "a7a1b4302f70bb3ec40ca86de82def532c97a80db49cac6a6700af0de41af5ee"}}, - {name = "aiohttp-3.12.14-cp39-cp39-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/1b/f7/39c3570434bb7e81601155ba71327735b26548473cca2d5c7f5badabb140/aiohttp-3.12.14-cp39-cp39-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "1b07ccef62950a2519f9bfc1e5b294de5dd84329f444ca0b329605ea787a3de5"}}, - {name = "aiohttp-3.12.14-cp39-cp39-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/46/0d/caee8733fbe511c34a54e93ee26c4b8d505e12785444d31f772a610df7ab/aiohttp-3.12.14-cp39-cp39-musllinux_1_2_s390x.whl",hashes = {sha256 = "938bd3ca6259e7e48b38d84f753d548bd863e0c222ed6ee6ace3fd6752768a84"}}, - {name = "aiohttp-3.12.14-cp39-cp39-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/24/f3/5d21196abf74dee66c5809e764cc27a2275e54c9355019c21be3bf77dd77/aiohttp-3.12.14-cp39-cp39-musllinux_1_2_x86_64.whl",hashes = {sha256 = "8bc784302b6b9f163b54c4e93d7a6f09563bd01ff2b841b29ed3ac126e5040bf"}}, - {name = "aiohttp-3.12.14-cp39-cp39-win32.whl",url = "https://files.pythonhosted.org/packages/54/bb/b4226f4fd0597d5245f284d10be48bf1ef610ab4f57d4239686fb03d1814/aiohttp-3.12.14-cp39-cp39-win32.whl",hashes = {sha256 = "a3416f95961dd7d5393ecff99e3f41dc990fb72eda86c11f2a60308ac6dcd7a0"}}, - {name = "aiohttp-3.12.14-cp39-cp39-win_amd64.whl",url = "https://files.pythonhosted.org/packages/a0/c0/2f1cefb7b077bf5c19f01bdf0d82b89de0bf2801b441eda23ada0b8966ac/aiohttp-3.12.14-cp39-cp39-win_amd64.whl",hashes = {sha256 = "196858b8820d7f60578f8b47e5669b3195c21d8ab261e39b1d705346458f445f"}}, ] marker = "\"default\" in dependency_groups or \"dev\" in extras" @@ -1563,17 +1399,73 @@ dependencies = [ ] [[packages]] -name = "async-timeout" -version = "5.0.1" -requires-python = ">=3.8" -sdist = {name = "async_timeout-5.0.1.tar.gz", url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hashes = {sha256 = "d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}} +name = "multidict" +version = "6.6.3" +requires-python = ">=3.9" +sdist = {name = "multidict-6.6.3.tar.gz", url = "https://files.pythonhosted.org/packages/3d/2c/5dad12e82fbdf7470f29bff2171484bf07cb3b16ada60a6589af8f376440/multidict-6.6.3.tar.gz", hashes = {sha256 = "798a9eb12dab0a6c2e29c1de6f3468af5cb2da6053a20dfa3344907eed0937cc"}} wheels = [ - {name = "async_timeout-5.0.1-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl",hashes = {sha256 = "39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}}, + {name = "multidict-6.6.3-cp313-cp313-macosx_10_13_universal2.whl",url = "https://files.pythonhosted.org/packages/52/1d/0bebcbbb4f000751fbd09957257903d6e002943fc668d841a4cf2fb7f872/multidict-6.6.3-cp313-cp313-macosx_10_13_universal2.whl",hashes = {sha256 = "540d3c06d48507357a7d57721e5094b4f7093399a0106c211f33540fdc374d55"}}, + {name = "multidict-6.6.3-cp313-cp313-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/07/8f/cbe241b0434cfe257f65c2b1bcf9e8d5fb52bc708c5061fb29b0fed22bdf/multidict-6.6.3-cp313-cp313-macosx_10_13_x86_64.whl",hashes = {sha256 = "9c19cea2a690f04247d43f366d03e4eb110a0dc4cd1bbeee4d445435428ed35b"}}, + {name = "multidict-6.6.3-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/32/d2/0b3b23f9dbad5b270b22a3ac3ea73ed0a50ef2d9a390447061178ed6bdb8/multidict-6.6.3-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "7af039820cfd00effec86bda5d8debef711a3e86a1d3772e85bea0f243a4bd65"}}, + {name = "multidict-6.6.3-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/fd/fe/6eb68927e823999e3683bc49678eb20374ba9615097d085298fd5b386564/multidict-6.6.3-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "500b84f51654fdc3944e936f2922114349bf8fdcac77c3092b03449f0e5bc2b3"}}, + {name = "multidict-6.6.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/e7/ab/320d8507e7726c460cb77117848b3834ea0d59e769f36fdae495f7669929/multidict-6.6.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "f3fc723ab8a5c5ed6c50418e9bfcd8e6dceba6c271cee6728a10a4ed8561520c"}}, + {name = "multidict-6.6.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl",url = "https://files.pythonhosted.org/packages/76/60/38ee422db515ac69834e60142a1a69111ac96026e76e8e9aa347fd2e4591/multidict-6.6.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl",hashes = {sha256 = "94c47ea3ade005b5976789baaed66d4de4480d0a0bf31cef6edaa41c1e7b56a6"}}, + {name = "multidict-6.6.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",url = "https://files.pythonhosted.org/packages/27/fb/905224fde2dff042b030c27ad95a7ae744325cf54b890b443d30a789b80e/multidict-6.6.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",hashes = {sha256 = "dbc7cf464cc6d67e83e136c9f55726da3a30176f020a36ead246eceed87f1cd8"}}, + {name = "multidict-6.6.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/76/35/dc38ab361051beae08d1a53965e3e1a418752fc5be4d3fb983c5582d8784/multidict-6.6.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "900eb9f9da25ada070f8ee4a23f884e0ee66fe4e1a38c3af644256a508ad81ca"}}, + {name = "multidict-6.6.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/1f/a3/0a485b7f36e422421b17e2bbb5a81c1af10eac1d4476f2ff92927c730479/multidict-6.6.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "7c6df517cf177da5d47ab15407143a89cd1a23f8b335f3a28d57e8b0a3dbb884"}}, + {name = "multidict-6.6.3-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/b4/59/bcdd52c1dab7c0e0d75ff19cac751fbd5f850d1fc39172ce809a74aa9ea4/multidict-6.6.3-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "4ef421045f13879e21c994b36e728d8e7d126c91a64b9185810ab51d474f27e7"}}, + {name = "multidict-6.6.3-cp313-cp313-musllinux_1_2_armv7l.whl",url = "https://files.pythonhosted.org/packages/bb/a4/2d96aaa6eae8067ce108d4acee6f45ced5728beda55c0f02ae1072c730d1/multidict-6.6.3-cp313-cp313-musllinux_1_2_armv7l.whl",hashes = {sha256 = "6c1e61bb4f80895c081790b6b09fa49e13566df8fbff817da3f85b3a8192e36b"}}, + {name = "multidict-6.6.3-cp313-cp313-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/25/d2/ed9f847fa5c7d0677d4f02ea2c163d5e48573de3f57bacf5670e43a5ffaa/multidict-6.6.3-cp313-cp313-musllinux_1_2_i686.whl",hashes = {sha256 = "e5e8523bb12d7623cd8300dbd91b9e439a46a028cd078ca695eb66ba31adee3c"}}, + {name = "multidict-6.6.3-cp313-cp313-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/1f/af/9155850372563fc550803d3f25373308aa70f59b52cff25854086ecb4a79/multidict-6.6.3-cp313-cp313-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "ef58340cc896219e4e653dade08fea5c55c6df41bcc68122e3be3e9d873d9a7b"}}, + {name = "multidict-6.6.3-cp313-cp313-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/36/2f/c6a728f699896252cf309769089568a33c6439626648843f78743660709d/multidict-6.6.3-cp313-cp313-musllinux_1_2_s390x.whl",hashes = {sha256 = "fc9dc435ec8699e7b602b94fe0cd4703e69273a01cbc34409af29e7820f777f1"}}, + {name = "multidict-6.6.3-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/d0/60/689880776d6b18fa2b70f6cc74ff87dd6c6b9b47bd9cf74c16fecfaa6ad9/multidict-6.6.3-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "9e864486ef4ab07db5e9cb997bad2b681514158d6954dd1958dfb163b83d53e6"}}, + {name = "multidict-6.6.3-cp313-cp313-win32.whl",url = "https://files.pythonhosted.org/packages/75/5e/325b11f2222a549019cf2ef879c1f81f94a0d40ace3ef55cf529915ba6cc/multidict-6.6.3-cp313-cp313-win32.whl",hashes = {sha256 = "5633a82fba8e841bc5c5c06b16e21529573cd654f67fd833650a215520a6210e"}}, + {name = "multidict-6.6.3-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/b1/ad/cf46e73f5d6e3c775cabd2a05976547f3f18b39bee06260369a42501f053/multidict-6.6.3-cp313-cp313-win_amd64.whl",hashes = {sha256 = "e93089c1570a4ad54c3714a12c2cef549dc9d58e97bcded193d928649cab78e9"}}, + {name = "multidict-6.6.3-cp313-cp313-win_arm64.whl",url = "https://files.pythonhosted.org/packages/c5/c9/2e3fe950db28fb7c62e1a5f46e1e38759b072e2089209bc033c2798bb5ec/multidict-6.6.3-cp313-cp313-win_arm64.whl",hashes = {sha256 = "c60b401f192e79caec61f166da9c924e9f8bc65548d4246842df91651e83d600"}}, + {name = "multidict-6.6.3-cp313-cp313t-macosx_10_13_universal2.whl",url = "https://files.pythonhosted.org/packages/3a/58/aaf8114cf34966e084a8cc9517771288adb53465188843d5a19862cb6dc3/multidict-6.6.3-cp313-cp313t-macosx_10_13_universal2.whl",hashes = {sha256 = "02fd8f32d403a6ff13864b0851f1f523d4c988051eea0471d4f1fd8010f11134"}}, + {name = "multidict-6.6.3-cp313-cp313t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/71/af/5402e7b58a1f5b987a07ad98f2501fdba2a4f4b4c30cf114e3ce8db64c87/multidict-6.6.3-cp313-cp313t-macosx_10_13_x86_64.whl",hashes = {sha256 = "f3aa090106b1543f3f87b2041eef3c156c8da2aed90c63a2fbed62d875c49c37"}}, + {name = "multidict-6.6.3-cp313-cp313t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/39/65/ab3c8cafe21adb45b24a50266fd747147dec7847425bc2a0f6934b3ae9ce/multidict-6.6.3-cp313-cp313t-macosx_11_0_arm64.whl",hashes = {sha256 = "e924fb978615a5e33ff644cc42e6aa241effcf4f3322c09d4f8cebde95aff5f8"}}, + {name = "multidict-6.6.3-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/49/ba/9fcc1b332f67cc0c0c8079e263bfab6660f87fe4e28a35921771ff3eea0d/multidict-6.6.3-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "b9fe5a0e57c6dbd0e2ce81ca66272282c32cd11d31658ee9553849d91289e1c1"}}, + {name = "multidict-6.6.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/a4/14/0145a251f555f7c754ce2dcbcd012939bbd1f34f066fa5d28a50e722a054/multidict-6.6.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "b24576f208793ebae00280c59927c3b7c2a3b1655e443a25f753c4611bc1c373"}}, + {name = "multidict-6.6.3-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl",url = "https://files.pythonhosted.org/packages/9e/d4/d5c0bd2bbb173b586c249a151a26d2fb3ec7d53c96e42091c9fef4e1f10c/multidict-6.6.3-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl",hashes = {sha256 = "135631cb6c58eac37d7ac0df380294fecdc026b28837fa07c02e459c7fb9c54e"}}, + {name = "multidict-6.6.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",url = "https://files.pythonhosted.org/packages/21/32/c9a2d8444a50ec48c4733ccc67254100c10e1c8ae8e40c7a2d2183b59b97/multidict-6.6.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",hashes = {sha256 = "274d416b0df887aef98f19f21578653982cfb8a05b4e187d4a17103322eeaf8f"}}, + {name = "multidict-6.6.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/68/d0/14fa1699f4ef629eae08ad6201c6b476098f5efb051b296f4c26be7a9fdf/multidict-6.6.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "e252017a817fad7ce05cafbe5711ed40faeb580e63b16755a3a24e66fa1d87c0"}}, + {name = "multidict-6.6.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/da/88/84a27570fbe303c65607d517a5f147cd2fc046c2d1da02b84b17b9bdc2aa/multidict-6.6.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "2e4cc8d848cd4fe1cdee28c13ea79ab0ed37fc2e89dd77bac86a2e7959a8c3bc"}}, + {name = "multidict-6.6.3-cp313-cp313t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/1c/60/dca352a0c999ce96a5d8b8ee0b2b9f729dcad2e0b0c195f8286269a2074c/multidict-6.6.3-cp313-cp313t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "9e236a7094b9c4c1b7585f6b9cca34b9d833cf079f7e4c49e6a4a6ec9bfdc68f"}}, + {name = "multidict-6.6.3-cp313-cp313t-musllinux_1_2_armv7l.whl",url = "https://files.pythonhosted.org/packages/50/ef/433fa3ed06028f03946f3993223dada70fb700f763f70c00079533c34578/multidict-6.6.3-cp313-cp313t-musllinux_1_2_armv7l.whl",hashes = {sha256 = "e0cb0ab69915c55627c933f0b555a943d98ba71b4d1c57bc0d0a66e2567c7471"}}, + {name = "multidict-6.6.3-cp313-cp313t-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/1b/1f/487612ab56fbe35715320905215a57fede20de7db40a261759690dc80471/multidict-6.6.3-cp313-cp313t-musllinux_1_2_i686.whl",hashes = {sha256 = "81ef2f64593aba09c5212a3d0f8c906a0d38d710a011f2f42759704d4557d3f2"}}, + {name = "multidict-6.6.3-cp313-cp313t-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/da/6f/ce8b79de16cd885c6f9052c96a3671373d00c59b3ee635ea93e6e81b8ccf/multidict-6.6.3-cp313-cp313t-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "b9cbc60010de3562545fa198bfc6d3825df430ea96d2cc509c39bd71e2e7d648"}}, + {name = "multidict-6.6.3-cp313-cp313t-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/bb/fe/a2514a6aba78e5abefa1624ca85ae18f542d95ac5cde2e3815a9fbf369aa/multidict-6.6.3-cp313-cp313t-musllinux_1_2_s390x.whl",hashes = {sha256 = "70d974eaaa37211390cd02ef93b7e938de564bbffa866f0b08d07e5e65da783d"}}, + {name = "multidict-6.6.3-cp313-cp313t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/8c/22/b788718d63bb3cce752d107a57c85fcd1a212c6c778628567c9713f9345a/multidict-6.6.3-cp313-cp313t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "3713303e4a6663c6d01d648a68f2848701001f3390a030edaaf3fc949c90bf7c"}}, + {name = "multidict-6.6.3-cp313-cp313t-win32.whl",url = "https://files.pythonhosted.org/packages/22/d6/fdb3d0670819f2228f3f7d9af613d5e652c15d170c83e5f1c94fbc55a25b/multidict-6.6.3-cp313-cp313t-win32.whl",hashes = {sha256 = "639ecc9fe7cd73f2495f62c213e964843826f44505a3e5d82805aa85cac6f89e"}}, + {name = "multidict-6.6.3-cp313-cp313t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/b6/d6/a9d2c808f2c489ad199723197419207ecbfbc1776f6e155e1ecea9c883aa/multidict-6.6.3-cp313-cp313t-win_amd64.whl",hashes = {sha256 = "9f97e181f344a0ef3881b573d31de8542cc0dbc559ec68c8f8b5ce2c2e91646d"}}, + {name = "multidict-6.6.3-cp313-cp313t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/f2/40/b68001cba8188dd267590a111f9661b6256debc327137667e832bf5d66e8/multidict-6.6.3-cp313-cp313t-win_arm64.whl",hashes = {sha256 = "ce8b7693da41a3c4fde5871c738a81490cea5496c671d74374c8ab889e1834fb"}}, + {name = "multidict-6.6.3-cp312-cp312-macosx_10_13_universal2.whl",url = "https://files.pythonhosted.org/packages/0e/a0/6b57988ea102da0623ea814160ed78d45a2645e4bbb499c2896d12833a70/multidict-6.6.3-cp312-cp312-macosx_10_13_universal2.whl",hashes = {sha256 = "056bebbeda16b2e38642d75e9e5310c484b7c24e3841dc0fb943206a72ec89d6"}}, + {name = "multidict-6.6.3-cp312-cp312-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/07/7a/d1e92665b0850c6c0508f101f9cf0410c1afa24973e1115fe9c6a185ebf7/multidict-6.6.3-cp312-cp312-macosx_10_13_x86_64.whl",hashes = {sha256 = "e5f481cccb3c5c5e5de5d00b5141dc589c1047e60d07e85bbd7dea3d4580d63f"}}, + {name = "multidict-6.6.3-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/52/6f/dd104490e01be6ef8bf9573705d8572f8c2d2c561f06e3826b081d9e6591/multidict-6.6.3-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "10bea2ee839a759ee368b5a6e47787f399b41e70cf0c20d90dfaf4158dfb4e55"}}, + {name = "multidict-6.6.3-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/44/fe/06e0e01b1b0611e6581b7fd5a85b43dacc08b6cea3034f902f383b0873e5/multidict-6.6.3-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "2334cfb0fa9549d6ce2c21af2bfbcd3ac4ec3646b1b1581c88e3e2b1779ec92b"}}, + {name = "multidict-6.6.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/ce/71/4f0e558fb77696b89c233c1ee2d92f3e1d5459070a0e89153c9e9e804186/multidict-6.6.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "b8fee016722550a2276ca2cb5bb624480e0ed2bd49125b2b73b7010b9090e888"}}, + {name = "multidict-6.6.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl",url = "https://files.pythonhosted.org/packages/e3/25/cca0e68228addad24903801ed1ab42e21307a1b4b6dd2cf63da5d3ae082a/multidict-6.6.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl",hashes = {sha256 = "e5511cb35f5c50a2db21047c875eb42f308c5583edf96bd8ebf7d770a9d68f6d"}}, + {name = "multidict-6.6.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",url = "https://files.pythonhosted.org/packages/6e/a3/46f2d420d86bbcb8fe660b26a10a219871a0fbf4d43cb846a4031533f3e0/multidict-6.6.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",hashes = {sha256 = "712b348f7f449948e0a6c4564a21c7db965af900973a67db432d724619b3c680"}}, + {name = "multidict-6.6.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/9e/73/1c743542fe00794a2ec7466abd3f312ccb8fad8dff9f36d42e18fb1ec33e/multidict-6.6.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "e4e15d2138ee2694e038e33b7c3da70e6b0ad8868b9f8094a72e1414aeda9c1a"}}, + {name = "multidict-6.6.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/a4/11/6ec9dcbe2264b92778eeb85407d1df18812248bf3506a5a1754bc035db0c/multidict-6.6.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "8df25594989aebff8a130f7899fa03cbfcc5d2b5f4a461cf2518236fe6f15961"}}, + {name = "multidict-6.6.3-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/9b/2b/631b1e2afeb5f1696846d747d36cda075bfdc0bc7245d6ba5c319278d6c4/multidict-6.6.3-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "159ca68bfd284a8860f8d8112cf0521113bffd9c17568579e4d13d1f1dc76b65"}}, + {name = "multidict-6.6.3-cp312-cp312-musllinux_1_2_armv7l.whl",url = "https://files.pythonhosted.org/packages/bf/0e/7e3b93f79efeb6111d3bf9a1a69e555ba1d07ad1c11bceb56b7310d0d7ee/multidict-6.6.3-cp312-cp312-musllinux_1_2_armv7l.whl",hashes = {sha256 = "e098c17856a8c9ade81b4810888c5ad1914099657226283cab3062c0540b0643"}}, + {name = "multidict-6.6.3-cp312-cp312-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/ad/9e/086846c1d6601948e7de556ee464a2d4c85e33883e749f46b9547d7b0704/multidict-6.6.3-cp312-cp312-musllinux_1_2_i686.whl",hashes = {sha256 = "67c92ed673049dec52d7ed39f8cf9ebbadf5032c774058b4406d18c8f8fe7063"}}, + {name = "multidict-6.6.3-cp312-cp312-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/8c/7b/86ec260118e522f1a31550e87b23542294880c97cfbf6fb18cc67b044c66/multidict-6.6.3-cp312-cp312-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "bd0578596e3a835ef451784053cfd327d607fc39ea1a14812139339a18a0dbc3"}}, + {name = "multidict-6.6.3-cp312-cp312-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/8c/bd/22ce8f47abb0be04692c9fc4638508b8340987b18691aa7775d927b73f72/multidict-6.6.3-cp312-cp312-musllinux_1_2_s390x.whl",hashes = {sha256 = "346055630a2df2115cd23ae271910b4cae40f4e336773550dca4889b12916e75"}}, + {name = "multidict-6.6.3-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/07/9c/91b7ac1691be95cd1f4a26e36a74b97cda6aa9820632d31aab4410f46ebd/multidict-6.6.3-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "555ff55a359302b79de97e0468e9ee80637b0de1fce77721639f7cd9440b3a10"}}, + {name = "multidict-6.6.3-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/6f/5c/4d7adc739884f7a9fbe00d1eac8c034023ef8bad71f2ebe12823ca2e3649/multidict-6.6.3-cp312-cp312-win32.whl",hashes = {sha256 = "73ab034fb8d58ff85c2bcbadc470efc3fafeea8affcf8722855fb94557f14cc5"}}, + {name = "multidict-6.6.3-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/6a/a3/0fbc7afdf7cb1aa12a086b02959307848eb6bcc8f66fcb66c0cb57e2a2c1/multidict-6.6.3-cp312-cp312-win_amd64.whl",hashes = {sha256 = "04cbcce84f63b9af41bad04a54d4cc4e60e90c35b9e6ccb130be2d75b71f8c17"}}, + {name = "multidict-6.6.3-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/b8/95/8c825bd70ff9b02462dc18d1295dd08d3e9e4eb66856d292ffa62cfe1920/multidict-6.6.3-cp312-cp312-win_arm64.whl",hashes = {sha256 = "0f1130b896ecb52d2a1e615260f3ea2af55fa7dc3d7c3003ba0c3121a759b18b"}}, + {name = "multidict-6.6.3-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/d8/30/9aec301e9772b098c1f5c0ca0279237c9766d94b97802e9888010c64b0ed/multidict-6.6.3-py3-none-any.whl",hashes = {sha256 = "8db10f29c7541fc5da4defd8cd697e1ca429db743fa716325f236079b96f775a"}}, ] -marker = "python_version < \"3.11\" and python_version >= \"3.9\" and \"default\" in dependency_groups or python_version < \"3.11\" and python_version >= \"3.9\" and \"dev\" in extras" +marker = "\"default\" in dependency_groups or \"dev\" in extras" [packages.tool.pdm] -dependencies = [] +dependencies = [ + "typing-extensions>=4.1.0; python_version < \"3.11\"", +] [[packages]] name = "h2" @@ -1664,129 +1556,6 @@ dependencies = [ "markdown-it-py<4.0.0,>=1.0.0", ] -[[packages]] -name = "multidict" -version = "6.6.3" -requires-python = ">=3.9" -sdist = {name = "multidict-6.6.3.tar.gz", url = "https://files.pythonhosted.org/packages/3d/2c/5dad12e82fbdf7470f29bff2171484bf07cb3b16ada60a6589af8f376440/multidict-6.6.3.tar.gz", hashes = {sha256 = "798a9eb12dab0a6c2e29c1de6f3468af5cb2da6053a20dfa3344907eed0937cc"}} -wheels = [ - {name = "multidict-6.6.3-cp313-cp313-macosx_10_13_universal2.whl",url = "https://files.pythonhosted.org/packages/52/1d/0bebcbbb4f000751fbd09957257903d6e002943fc668d841a4cf2fb7f872/multidict-6.6.3-cp313-cp313-macosx_10_13_universal2.whl",hashes = {sha256 = "540d3c06d48507357a7d57721e5094b4f7093399a0106c211f33540fdc374d55"}}, - {name = "multidict-6.6.3-cp313-cp313-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/07/8f/cbe241b0434cfe257f65c2b1bcf9e8d5fb52bc708c5061fb29b0fed22bdf/multidict-6.6.3-cp313-cp313-macosx_10_13_x86_64.whl",hashes = {sha256 = "9c19cea2a690f04247d43f366d03e4eb110a0dc4cd1bbeee4d445435428ed35b"}}, - {name = "multidict-6.6.3-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/32/d2/0b3b23f9dbad5b270b22a3ac3ea73ed0a50ef2d9a390447061178ed6bdb8/multidict-6.6.3-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "7af039820cfd00effec86bda5d8debef711a3e86a1d3772e85bea0f243a4bd65"}}, - {name = "multidict-6.6.3-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/fd/fe/6eb68927e823999e3683bc49678eb20374ba9615097d085298fd5b386564/multidict-6.6.3-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "500b84f51654fdc3944e936f2922114349bf8fdcac77c3092b03449f0e5bc2b3"}}, - {name = "multidict-6.6.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/e7/ab/320d8507e7726c460cb77117848b3834ea0d59e769f36fdae495f7669929/multidict-6.6.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "f3fc723ab8a5c5ed6c50418e9bfcd8e6dceba6c271cee6728a10a4ed8561520c"}}, - {name = "multidict-6.6.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl",url = "https://files.pythonhosted.org/packages/76/60/38ee422db515ac69834e60142a1a69111ac96026e76e8e9aa347fd2e4591/multidict-6.6.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl",hashes = {sha256 = "94c47ea3ade005b5976789baaed66d4de4480d0a0bf31cef6edaa41c1e7b56a6"}}, - {name = "multidict-6.6.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",url = "https://files.pythonhosted.org/packages/27/fb/905224fde2dff042b030c27ad95a7ae744325cf54b890b443d30a789b80e/multidict-6.6.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",hashes = {sha256 = "dbc7cf464cc6d67e83e136c9f55726da3a30176f020a36ead246eceed87f1cd8"}}, - {name = "multidict-6.6.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/76/35/dc38ab361051beae08d1a53965e3e1a418752fc5be4d3fb983c5582d8784/multidict-6.6.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "900eb9f9da25ada070f8ee4a23f884e0ee66fe4e1a38c3af644256a508ad81ca"}}, - {name = "multidict-6.6.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/1f/a3/0a485b7f36e422421b17e2bbb5a81c1af10eac1d4476f2ff92927c730479/multidict-6.6.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "7c6df517cf177da5d47ab15407143a89cd1a23f8b335f3a28d57e8b0a3dbb884"}}, - {name = "multidict-6.6.3-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/b4/59/bcdd52c1dab7c0e0d75ff19cac751fbd5f850d1fc39172ce809a74aa9ea4/multidict-6.6.3-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "4ef421045f13879e21c994b36e728d8e7d126c91a64b9185810ab51d474f27e7"}}, - {name = "multidict-6.6.3-cp313-cp313-musllinux_1_2_armv7l.whl",url = "https://files.pythonhosted.org/packages/bb/a4/2d96aaa6eae8067ce108d4acee6f45ced5728beda55c0f02ae1072c730d1/multidict-6.6.3-cp313-cp313-musllinux_1_2_armv7l.whl",hashes = {sha256 = "6c1e61bb4f80895c081790b6b09fa49e13566df8fbff817da3f85b3a8192e36b"}}, - {name = "multidict-6.6.3-cp313-cp313-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/25/d2/ed9f847fa5c7d0677d4f02ea2c163d5e48573de3f57bacf5670e43a5ffaa/multidict-6.6.3-cp313-cp313-musllinux_1_2_i686.whl",hashes = {sha256 = "e5e8523bb12d7623cd8300dbd91b9e439a46a028cd078ca695eb66ba31adee3c"}}, - {name = "multidict-6.6.3-cp313-cp313-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/1f/af/9155850372563fc550803d3f25373308aa70f59b52cff25854086ecb4a79/multidict-6.6.3-cp313-cp313-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "ef58340cc896219e4e653dade08fea5c55c6df41bcc68122e3be3e9d873d9a7b"}}, - {name = "multidict-6.6.3-cp313-cp313-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/36/2f/c6a728f699896252cf309769089568a33c6439626648843f78743660709d/multidict-6.6.3-cp313-cp313-musllinux_1_2_s390x.whl",hashes = {sha256 = "fc9dc435ec8699e7b602b94fe0cd4703e69273a01cbc34409af29e7820f777f1"}}, - {name = "multidict-6.6.3-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/d0/60/689880776d6b18fa2b70f6cc74ff87dd6c6b9b47bd9cf74c16fecfaa6ad9/multidict-6.6.3-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "9e864486ef4ab07db5e9cb997bad2b681514158d6954dd1958dfb163b83d53e6"}}, - {name = "multidict-6.6.3-cp313-cp313-win32.whl",url = "https://files.pythonhosted.org/packages/75/5e/325b11f2222a549019cf2ef879c1f81f94a0d40ace3ef55cf529915ba6cc/multidict-6.6.3-cp313-cp313-win32.whl",hashes = {sha256 = "5633a82fba8e841bc5c5c06b16e21529573cd654f67fd833650a215520a6210e"}}, - {name = "multidict-6.6.3-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/b1/ad/cf46e73f5d6e3c775cabd2a05976547f3f18b39bee06260369a42501f053/multidict-6.6.3-cp313-cp313-win_amd64.whl",hashes = {sha256 = "e93089c1570a4ad54c3714a12c2cef549dc9d58e97bcded193d928649cab78e9"}}, - {name = "multidict-6.6.3-cp313-cp313-win_arm64.whl",url = "https://files.pythonhosted.org/packages/c5/c9/2e3fe950db28fb7c62e1a5f46e1e38759b072e2089209bc033c2798bb5ec/multidict-6.6.3-cp313-cp313-win_arm64.whl",hashes = {sha256 = "c60b401f192e79caec61f166da9c924e9f8bc65548d4246842df91651e83d600"}}, - {name = "multidict-6.6.3-cp313-cp313t-macosx_10_13_universal2.whl",url = "https://files.pythonhosted.org/packages/3a/58/aaf8114cf34966e084a8cc9517771288adb53465188843d5a19862cb6dc3/multidict-6.6.3-cp313-cp313t-macosx_10_13_universal2.whl",hashes = {sha256 = "02fd8f32d403a6ff13864b0851f1f523d4c988051eea0471d4f1fd8010f11134"}}, - {name = "multidict-6.6.3-cp313-cp313t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/71/af/5402e7b58a1f5b987a07ad98f2501fdba2a4f4b4c30cf114e3ce8db64c87/multidict-6.6.3-cp313-cp313t-macosx_10_13_x86_64.whl",hashes = {sha256 = "f3aa090106b1543f3f87b2041eef3c156c8da2aed90c63a2fbed62d875c49c37"}}, - {name = "multidict-6.6.3-cp313-cp313t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/39/65/ab3c8cafe21adb45b24a50266fd747147dec7847425bc2a0f6934b3ae9ce/multidict-6.6.3-cp313-cp313t-macosx_11_0_arm64.whl",hashes = {sha256 = "e924fb978615a5e33ff644cc42e6aa241effcf4f3322c09d4f8cebde95aff5f8"}}, - {name = "multidict-6.6.3-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/49/ba/9fcc1b332f67cc0c0c8079e263bfab6660f87fe4e28a35921771ff3eea0d/multidict-6.6.3-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "b9fe5a0e57c6dbd0e2ce81ca66272282c32cd11d31658ee9553849d91289e1c1"}}, - {name = "multidict-6.6.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/a4/14/0145a251f555f7c754ce2dcbcd012939bbd1f34f066fa5d28a50e722a054/multidict-6.6.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "b24576f208793ebae00280c59927c3b7c2a3b1655e443a25f753c4611bc1c373"}}, - {name = "multidict-6.6.3-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl",url = "https://files.pythonhosted.org/packages/9e/d4/d5c0bd2bbb173b586c249a151a26d2fb3ec7d53c96e42091c9fef4e1f10c/multidict-6.6.3-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl",hashes = {sha256 = "135631cb6c58eac37d7ac0df380294fecdc026b28837fa07c02e459c7fb9c54e"}}, - {name = "multidict-6.6.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",url = "https://files.pythonhosted.org/packages/21/32/c9a2d8444a50ec48c4733ccc67254100c10e1c8ae8e40c7a2d2183b59b97/multidict-6.6.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",hashes = {sha256 = "274d416b0df887aef98f19f21578653982cfb8a05b4e187d4a17103322eeaf8f"}}, - {name = "multidict-6.6.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/68/d0/14fa1699f4ef629eae08ad6201c6b476098f5efb051b296f4c26be7a9fdf/multidict-6.6.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "e252017a817fad7ce05cafbe5711ed40faeb580e63b16755a3a24e66fa1d87c0"}}, - {name = "multidict-6.6.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/da/88/84a27570fbe303c65607d517a5f147cd2fc046c2d1da02b84b17b9bdc2aa/multidict-6.6.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "2e4cc8d848cd4fe1cdee28c13ea79ab0ed37fc2e89dd77bac86a2e7959a8c3bc"}}, - {name = "multidict-6.6.3-cp313-cp313t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/1c/60/dca352a0c999ce96a5d8b8ee0b2b9f729dcad2e0b0c195f8286269a2074c/multidict-6.6.3-cp313-cp313t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "9e236a7094b9c4c1b7585f6b9cca34b9d833cf079f7e4c49e6a4a6ec9bfdc68f"}}, - {name = "multidict-6.6.3-cp313-cp313t-musllinux_1_2_armv7l.whl",url = "https://files.pythonhosted.org/packages/50/ef/433fa3ed06028f03946f3993223dada70fb700f763f70c00079533c34578/multidict-6.6.3-cp313-cp313t-musllinux_1_2_armv7l.whl",hashes = {sha256 = "e0cb0ab69915c55627c933f0b555a943d98ba71b4d1c57bc0d0a66e2567c7471"}}, - {name = "multidict-6.6.3-cp313-cp313t-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/1b/1f/487612ab56fbe35715320905215a57fede20de7db40a261759690dc80471/multidict-6.6.3-cp313-cp313t-musllinux_1_2_i686.whl",hashes = {sha256 = "81ef2f64593aba09c5212a3d0f8c906a0d38d710a011f2f42759704d4557d3f2"}}, - {name = "multidict-6.6.3-cp313-cp313t-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/da/6f/ce8b79de16cd885c6f9052c96a3671373d00c59b3ee635ea93e6e81b8ccf/multidict-6.6.3-cp313-cp313t-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "b9cbc60010de3562545fa198bfc6d3825df430ea96d2cc509c39bd71e2e7d648"}}, - {name = "multidict-6.6.3-cp313-cp313t-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/bb/fe/a2514a6aba78e5abefa1624ca85ae18f542d95ac5cde2e3815a9fbf369aa/multidict-6.6.3-cp313-cp313t-musllinux_1_2_s390x.whl",hashes = {sha256 = "70d974eaaa37211390cd02ef93b7e938de564bbffa866f0b08d07e5e65da783d"}}, - {name = "multidict-6.6.3-cp313-cp313t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/8c/22/b788718d63bb3cce752d107a57c85fcd1a212c6c778628567c9713f9345a/multidict-6.6.3-cp313-cp313t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "3713303e4a6663c6d01d648a68f2848701001f3390a030edaaf3fc949c90bf7c"}}, - {name = "multidict-6.6.3-cp313-cp313t-win32.whl",url = "https://files.pythonhosted.org/packages/22/d6/fdb3d0670819f2228f3f7d9af613d5e652c15d170c83e5f1c94fbc55a25b/multidict-6.6.3-cp313-cp313t-win32.whl",hashes = {sha256 = "639ecc9fe7cd73f2495f62c213e964843826f44505a3e5d82805aa85cac6f89e"}}, - {name = "multidict-6.6.3-cp313-cp313t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/b6/d6/a9d2c808f2c489ad199723197419207ecbfbc1776f6e155e1ecea9c883aa/multidict-6.6.3-cp313-cp313t-win_amd64.whl",hashes = {sha256 = "9f97e181f344a0ef3881b573d31de8542cc0dbc559ec68c8f8b5ce2c2e91646d"}}, - {name = "multidict-6.6.3-cp313-cp313t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/f2/40/b68001cba8188dd267590a111f9661b6256debc327137667e832bf5d66e8/multidict-6.6.3-cp313-cp313t-win_arm64.whl",hashes = {sha256 = "ce8b7693da41a3c4fde5871c738a81490cea5496c671d74374c8ab889e1834fb"}}, - {name = "multidict-6.6.3-cp312-cp312-macosx_10_13_universal2.whl",url = "https://files.pythonhosted.org/packages/0e/a0/6b57988ea102da0623ea814160ed78d45a2645e4bbb499c2896d12833a70/multidict-6.6.3-cp312-cp312-macosx_10_13_universal2.whl",hashes = {sha256 = "056bebbeda16b2e38642d75e9e5310c484b7c24e3841dc0fb943206a72ec89d6"}}, - {name = "multidict-6.6.3-cp312-cp312-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/07/7a/d1e92665b0850c6c0508f101f9cf0410c1afa24973e1115fe9c6a185ebf7/multidict-6.6.3-cp312-cp312-macosx_10_13_x86_64.whl",hashes = {sha256 = "e5f481cccb3c5c5e5de5d00b5141dc589c1047e60d07e85bbd7dea3d4580d63f"}}, - {name = "multidict-6.6.3-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/52/6f/dd104490e01be6ef8bf9573705d8572f8c2d2c561f06e3826b081d9e6591/multidict-6.6.3-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "10bea2ee839a759ee368b5a6e47787f399b41e70cf0c20d90dfaf4158dfb4e55"}}, - {name = "multidict-6.6.3-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/44/fe/06e0e01b1b0611e6581b7fd5a85b43dacc08b6cea3034f902f383b0873e5/multidict-6.6.3-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "2334cfb0fa9549d6ce2c21af2bfbcd3ac4ec3646b1b1581c88e3e2b1779ec92b"}}, - {name = "multidict-6.6.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/ce/71/4f0e558fb77696b89c233c1ee2d92f3e1d5459070a0e89153c9e9e804186/multidict-6.6.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "b8fee016722550a2276ca2cb5bb624480e0ed2bd49125b2b73b7010b9090e888"}}, - {name = "multidict-6.6.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl",url = "https://files.pythonhosted.org/packages/e3/25/cca0e68228addad24903801ed1ab42e21307a1b4b6dd2cf63da5d3ae082a/multidict-6.6.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl",hashes = {sha256 = "e5511cb35f5c50a2db21047c875eb42f308c5583edf96bd8ebf7d770a9d68f6d"}}, - {name = "multidict-6.6.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",url = "https://files.pythonhosted.org/packages/6e/a3/46f2d420d86bbcb8fe660b26a10a219871a0fbf4d43cb846a4031533f3e0/multidict-6.6.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",hashes = {sha256 = "712b348f7f449948e0a6c4564a21c7db965af900973a67db432d724619b3c680"}}, - {name = "multidict-6.6.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/9e/73/1c743542fe00794a2ec7466abd3f312ccb8fad8dff9f36d42e18fb1ec33e/multidict-6.6.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "e4e15d2138ee2694e038e33b7c3da70e6b0ad8868b9f8094a72e1414aeda9c1a"}}, - {name = "multidict-6.6.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/a4/11/6ec9dcbe2264b92778eeb85407d1df18812248bf3506a5a1754bc035db0c/multidict-6.6.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "8df25594989aebff8a130f7899fa03cbfcc5d2b5f4a461cf2518236fe6f15961"}}, - {name = "multidict-6.6.3-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/9b/2b/631b1e2afeb5f1696846d747d36cda075bfdc0bc7245d6ba5c319278d6c4/multidict-6.6.3-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "159ca68bfd284a8860f8d8112cf0521113bffd9c17568579e4d13d1f1dc76b65"}}, - {name = "multidict-6.6.3-cp312-cp312-musllinux_1_2_armv7l.whl",url = "https://files.pythonhosted.org/packages/bf/0e/7e3b93f79efeb6111d3bf9a1a69e555ba1d07ad1c11bceb56b7310d0d7ee/multidict-6.6.3-cp312-cp312-musllinux_1_2_armv7l.whl",hashes = {sha256 = "e098c17856a8c9ade81b4810888c5ad1914099657226283cab3062c0540b0643"}}, - {name = "multidict-6.6.3-cp312-cp312-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/ad/9e/086846c1d6601948e7de556ee464a2d4c85e33883e749f46b9547d7b0704/multidict-6.6.3-cp312-cp312-musllinux_1_2_i686.whl",hashes = {sha256 = "67c92ed673049dec52d7ed39f8cf9ebbadf5032c774058b4406d18c8f8fe7063"}}, - {name = "multidict-6.6.3-cp312-cp312-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/8c/7b/86ec260118e522f1a31550e87b23542294880c97cfbf6fb18cc67b044c66/multidict-6.6.3-cp312-cp312-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "bd0578596e3a835ef451784053cfd327d607fc39ea1a14812139339a18a0dbc3"}}, - {name = "multidict-6.6.3-cp312-cp312-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/8c/bd/22ce8f47abb0be04692c9fc4638508b8340987b18691aa7775d927b73f72/multidict-6.6.3-cp312-cp312-musllinux_1_2_s390x.whl",hashes = {sha256 = "346055630a2df2115cd23ae271910b4cae40f4e336773550dca4889b12916e75"}}, - {name = "multidict-6.6.3-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/07/9c/91b7ac1691be95cd1f4a26e36a74b97cda6aa9820632d31aab4410f46ebd/multidict-6.6.3-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "555ff55a359302b79de97e0468e9ee80637b0de1fce77721639f7cd9440b3a10"}}, - {name = "multidict-6.6.3-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/6f/5c/4d7adc739884f7a9fbe00d1eac8c034023ef8bad71f2ebe12823ca2e3649/multidict-6.6.3-cp312-cp312-win32.whl",hashes = {sha256 = "73ab034fb8d58ff85c2bcbadc470efc3fafeea8affcf8722855fb94557f14cc5"}}, - {name = "multidict-6.6.3-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/6a/a3/0fbc7afdf7cb1aa12a086b02959307848eb6bcc8f66fcb66c0cb57e2a2c1/multidict-6.6.3-cp312-cp312-win_amd64.whl",hashes = {sha256 = "04cbcce84f63b9af41bad04a54d4cc4e60e90c35b9e6ccb130be2d75b71f8c17"}}, - {name = "multidict-6.6.3-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/b8/95/8c825bd70ff9b02462dc18d1295dd08d3e9e4eb66856d292ffa62cfe1920/multidict-6.6.3-cp312-cp312-win_arm64.whl",hashes = {sha256 = "0f1130b896ecb52d2a1e615260f3ea2af55fa7dc3d7c3003ba0c3121a759b18b"}}, - {name = "multidict-6.6.3-cp311-cp311-macosx_10_9_universal2.whl",url = "https://files.pythonhosted.org/packages/08/f0/1a39863ced51f639c81a5463fbfa9eb4df59c20d1a8769ab9ef4ca57ae04/multidict-6.6.3-cp311-cp311-macosx_10_9_universal2.whl",hashes = {sha256 = "18f4eba0cbac3546b8ae31e0bbc55b02c801ae3cbaf80c247fcdd89b456ff58c"}}, - {name = "multidict-6.6.3-cp311-cp311-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/c9/0e/a7cfa451c7b0365cd844e90b41e21fab32edaa1e42fc0c9f68461ce44ed7/multidict-6.6.3-cp311-cp311-macosx_10_9_x86_64.whl",hashes = {sha256 = "ef43b5dd842382329e4797c46f10748d8c2b6e0614f46b4afe4aee9ac33159df"}}, - {name = "multidict-6.6.3-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/c6/bb/a14a4efc5ee748cc1904b0748be278c31b9295ce5f4d2ef66526f410b94d/multidict-6.6.3-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "bf9bd1fd5eec01494e0f2e8e446a74a85d5e49afb63d75a9934e4a5423dba21d"}}, - {name = "multidict-6.6.3-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/c2/f8/410677d563c2d55e063ef74fe578f9d53fe6b0a51649597a5861f83ffa15/multidict-6.6.3-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "5bd8d6f793a787153956cd35e24f60485bf0651c238e207b9a54f7458b16d539"}}, - {name = "multidict-6.6.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/fd/df/2b787f80059314a98e1ec6a4cc7576244986df3e56b3c755e6fc7c99e038/multidict-6.6.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "1bf99b4daf908c73856bd87ee0a2499c3c9a3d19bb04b9c6025e66af3fd07462"}}, - {name = "multidict-6.6.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl",url = "https://files.pythonhosted.org/packages/05/f2/f9117089151b9a8ab39f9019620d10d9718eec2ac89e7ca9d30f3ec78e96/multidict-6.6.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl",hashes = {sha256 = "0b9e59946b49dafaf990fd9c17ceafa62976e8471a14952163d10a7a630413a9"}}, - {name = "multidict-6.6.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",url = "https://files.pythonhosted.org/packages/93/2d/7115300ec5b699faa152c56799b089a53ed69e399c3c2d528251f0aeda1a/multidict-6.6.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",hashes = {sha256 = "e2db616467070d0533832d204c54eea6836a5e628f2cb1e6dfd8cd6ba7277cb7"}}, - {name = "multidict-6.6.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/15/ea/ff4bab367623e39c20d3b07637225c7688d79e4f3cc1f3b9f89867677f9a/multidict-6.6.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "7394888236621f61dcdd25189b2768ae5cc280f041029a5bcf1122ac63df79f9"}}, - {name = "multidict-6.6.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/74/07/2c9246cda322dfe08be85f1b8739646f2c4c5113a1422d7a407763422ec4/multidict-6.6.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "f114d8478733ca7388e7c7e0ab34b72547476b97009d643644ac33d4d3fe1821"}}, - {name = "multidict-6.6.3-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/a8/62/279c13d584207d5697a752a66ffc9bb19355a95f7659140cb1b3cf82180e/multidict-6.6.3-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "cdf22e4db76d323bcdc733514bf732e9fb349707c98d341d40ebcc6e9318ef3d"}}, - {name = "multidict-6.6.3-cp311-cp311-musllinux_1_2_armv7l.whl",url = "https://files.pythonhosted.org/packages/69/cc/e06636f48c6d51e724a8bc8d9e1db5f136fe1df066d7cafe37ef4000f86a/multidict-6.6.3-cp311-cp311-musllinux_1_2_armv7l.whl",hashes = {sha256 = "e995a34c3d44ab511bfc11aa26869b9d66c2d8c799fa0e74b28a473a692532d6"}}, - {name = "multidict-6.6.3-cp311-cp311-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/89/a4/66c9d8fb9acf3b226cdd468ed009537ac65b520aebdc1703dd6908b19d33/multidict-6.6.3-cp311-cp311-musllinux_1_2_i686.whl",hashes = {sha256 = "766a4a5996f54361d8d5a9050140aa5362fe48ce51c755a50c0bc3706460c430"}}, - {name = "multidict-6.6.3-cp311-cp311-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/cf/01/c69e0317be556e46257826d5449feb4e6aa0d18573e567a48a2c14156f1f/multidict-6.6.3-cp311-cp311-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "3893a0d7d28a7fe6ca7a1f760593bc13038d1d35daf52199d431b61d2660602b"}}, - {name = "multidict-6.6.3-cp311-cp311-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/c0/da/9cc1da0299762d20e626fe0042e71b5694f9f72d7d3f9678397cbaa71b2b/multidict-6.6.3-cp311-cp311-musllinux_1_2_s390x.whl",hashes = {sha256 = "934796c81ea996e61914ba58064920d6cad5d99140ac3167901eb932150e2e56"}}, - {name = "multidict-6.6.3-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/e6/91/b22756afec99cc31105ddd4a52f95ab32b1a4a58f4d417979c570c4a922e/multidict-6.6.3-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "9ed948328aec2072bc00f05d961ceadfd3e9bfc2966c1319aeaf7b7c21219183"}}, - {name = "multidict-6.6.3-cp311-cp311-win32.whl",url = "https://files.pythonhosted.org/packages/be/f1/adcc185b878036a20399d5be5228f3cbe7f823d78985d101d425af35c800/multidict-6.6.3-cp311-cp311-win32.whl",hashes = {sha256 = "9f5b28c074c76afc3e4c610c488e3493976fe0e596dd3db6c8ddfbb0134dcac5"}}, - {name = "multidict-6.6.3-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/e0/d4/27652c1c6526ea6b4f5ddd397e93f4232ff5de42bea71d339bc6a6cc497f/multidict-6.6.3-cp311-cp311-win_amd64.whl",hashes = {sha256 = "bc7f6fbc61b1c16050a389c630da0b32fc6d4a3d191394ab78972bf5edc568c2"}}, - {name = "multidict-6.6.3-cp311-cp311-win_arm64.whl",url = "https://files.pythonhosted.org/packages/16/18/23f4932019804e56d3c2413e237f866444b774b0263bcb81df2fdecaf593/multidict-6.6.3-cp311-cp311-win_arm64.whl",hashes = {sha256 = "d4e47d8faffaae822fb5cba20937c048d4f734f43572e7079298a6c39fb172cb"}}, - {name = "multidict-6.6.3-cp310-cp310-macosx_10_9_universal2.whl",url = "https://files.pythonhosted.org/packages/0b/67/414933982bce2efce7cbcb3169eaaf901e0f25baec69432b4874dfb1f297/multidict-6.6.3-cp310-cp310-macosx_10_9_universal2.whl",hashes = {sha256 = "a2be5b7b35271f7fff1397204ba6708365e3d773579fe2a30625e16c4b4ce817"}}, - {name = "multidict-6.6.3-cp310-cp310-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/8a/fe/d8a3ee1fad37dc2ef4f75488b0d9d4f25bf204aad8306cbab63d97bff64a/multidict-6.6.3-cp310-cp310-macosx_10_9_x86_64.whl",hashes = {sha256 = "12f4581d2930840295c461764b9a65732ec01250b46c6b2c510d7ee68872b140"}}, - {name = "multidict-6.6.3-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/1f/e0/265d89af8c98240265d82b8cbcf35897f83b76cd59ee3ab3879050fd8c45/multidict-6.6.3-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "dd7793bab517e706c9ed9d7310b06c8672fd0aeee5781bfad612f56b8e0f7d14"}}, - {name = "multidict-6.6.3-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/e6/05/6b759379f7e8e04ccc97cfb2a5dcc5cdbd44a97f072b2272dc51281e6a40/multidict-6.6.3-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "72d8815f2cd3cf3df0f83cac3f3ef801d908b2d90409ae28102e0553af85545a"}}, - {name = "multidict-6.6.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/4e/f5/8d5a15488edd9a91fa4aad97228d785df208ed6298580883aa3d9def1959/multidict-6.6.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "531e331a2ee53543ab32b16334e2deb26f4e6b9b28e41f8e0c87e99a6c8e2d69"}}, - {name = "multidict-6.6.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl",url = "https://files.pythonhosted.org/packages/6e/b5/a8f317d47d0ac5bb746d6d8325885c8967c2a8ce0bb57be5399e3642cccb/multidict-6.6.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl",hashes = {sha256 = "42ca5aa9329a63be8dc49040f63817d1ac980e02eeddba763a9ae5b4027b9c9c"}}, - {name = "multidict-6.6.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",url = "https://files.pythonhosted.org/packages/76/88/18b2a0d5e80515fa22716556061189c2853ecf2aa2133081ebbe85ebea38/multidict-6.6.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",hashes = {sha256 = "208b9b9757060b9faa6f11ab4bc52846e4f3c2fb8b14d5680c8aac80af3dc751"}}, - {name = "multidict-6.6.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/62/bf/ebfcfd6b55a1b05ef16d0775ae34c0fe15e8dab570d69ca9941073b969e7/multidict-6.6.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "acf6b97bd0884891af6a8b43d0f586ab2fcf8e717cbd47ab4bdddc09e20652d8"}}, - {name = "multidict-6.6.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/44/11/780615a98fd3775fc309d0234d563941af69ade2df0bb82c91dda6ddaea1/multidict-6.6.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "68e9e12ed00e2089725669bdc88602b0b6f8d23c0c95e52b95f0bc69f7fe9b55"}}, - {name = "multidict-6.6.3-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/28/3d/35f33045e21034b388686213752cabc3a1b9d03e20969e6fa8f1b1d82db1/multidict-6.6.3-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "05db2f66c9addb10cfa226e1acb363450fab2ff8a6df73c622fefe2f5af6d4e7"}}, - {name = "multidict-6.6.3-cp310-cp310-musllinux_1_2_armv7l.whl",url = "https://files.pythonhosted.org/packages/6e/cc/ff84c03b95b430015d2166d9aae775a3985d757b94f6635010d0038d9241/multidict-6.6.3-cp310-cp310-musllinux_1_2_armv7l.whl",hashes = {sha256 = "0db58da8eafb514db832a1b44f8fa7906fdd102f7d982025f816a93ba45e3dcb"}}, - {name = "multidict-6.6.3-cp310-cp310-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/2e/f0/8cd49a0b37bdea673a4b793c2093f2f4ba8e7c9d6d7c9bd672fd6d38cd11/multidict-6.6.3-cp310-cp310-musllinux_1_2_i686.whl",hashes = {sha256 = "14117a41c8fdb3ee19c743b1c027da0736fdb79584d61a766da53d399b71176c"}}, - {name = "multidict-6.6.3-cp310-cp310-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/96/19/5d9a0cfdafe65d82b616a45ae950975820289069f885328e8185e64283c2/multidict-6.6.3-cp310-cp310-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "877443eaaabcd0b74ff32ebeed6f6176c71850feb7d6a1d2db65945256ea535c"}}, - {name = "multidict-6.6.3-cp310-cp310-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/e6/dc/c90066151da87d1e489f147b9b4327927241e65f1876702fafec6729c014/multidict-6.6.3-cp310-cp310-musllinux_1_2_s390x.whl",hashes = {sha256 = "70b72e749a4f6e7ed8fb334fa8d8496384840319512746a5f42fa0aec79f4d61"}}, - {name = "multidict-6.6.3-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/ec/39/458afb0cccbb0ee9164365273be3e039efddcfcb94ef35924b7dbdb05db0/multidict-6.6.3-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "43571f785b86afd02b3855c5ac8e86ec921b760298d6f82ff2a61daf5a35330b"}}, - {name = "multidict-6.6.3-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/35/38/0016adac3990426610a081787011177e661875546b434f50a26319dc8372/multidict-6.6.3-cp310-cp310-win32.whl",hashes = {sha256 = "20c5a0c3c13a15fd5ea86c42311859f970070e4e24de5a550e99d7c271d76318"}}, - {name = "multidict-6.6.3-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/f3/d2/17897a8f3f2c5363d969b4c635aa40375fe1f09168dc09a7826780bfb2a4/multidict-6.6.3-cp310-cp310-win_amd64.whl",hashes = {sha256 = "ab0a34a007704c625e25a9116c6770b4d3617a071c8a7c30cd338dfbadfe6485"}}, - {name = "multidict-6.6.3-cp310-cp310-win_arm64.whl",url = "https://files.pythonhosted.org/packages/2d/5f/d4a717c1e457fe44072e33fa400d2b93eb0f2819c4d669381f925b7cba1f/multidict-6.6.3-cp310-cp310-win_arm64.whl",hashes = {sha256 = "769841d70ca8bdd140a715746199fc6473414bd02efd678d75681d2d6a8986c5"}}, - {name = "multidict-6.6.3-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/d8/30/9aec301e9772b098c1f5c0ca0279237c9766d94b97802e9888010c64b0ed/multidict-6.6.3-py3-none-any.whl",hashes = {sha256 = "8db10f29c7541fc5da4defd8cd697e1ca429db743fa716325f236079b96f775a"}}, - {name = "multidict-6.6.3-cp39-cp39-macosx_10_9_universal2.whl",url = "https://files.pythonhosted.org/packages/d2/64/ba29bd6dfc895e592b2f20f92378e692ac306cf25dd0be2f8e0a0f898edb/multidict-6.6.3-cp39-cp39-macosx_10_9_universal2.whl",hashes = {sha256 = "c8161b5a7778d3137ea2ee7ae8a08cce0010de3b00ac671c5ebddeaa17cefd22"}}, - {name = "multidict-6.6.3-cp39-cp39-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/ca/cd/872ae4c134257dacebff59834983c1615d6ec863b6e3d360f3203aad8400/multidict-6.6.3-cp39-cp39-macosx_10_9_x86_64.whl",hashes = {sha256 = "1328201ee930f069961ae707d59c6627ac92e351ed5b92397cf534d1336ce557"}}, - {name = "multidict-6.6.3-cp39-cp39-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/15/35/d417d8f62f2886784b76df60522d608aba39dfc83dd53b230ca71f2d4c53/multidict-6.6.3-cp39-cp39-macosx_11_0_arm64.whl",hashes = {sha256 = "b1db4d2093d6b235de76932febf9d50766cf49a5692277b2c28a501c9637f616"}}, - {name = "multidict-6.6.3-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/85/59/25cddf781f12cddb2386baa29744a3fdd160eb705539b48065f0cffd86d5/multidict-6.6.3-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "53becb01dd8ebd19d1724bebe369cfa87e4e7f29abbbe5c14c98ce4c383e16cd"}}, - {name = "multidict-6.6.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/c4/21/4055b6a527954c572498a8068c26bd3b75f2b959080e17e12104b592273c/multidict-6.6.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "41bb9d1d4c303886e2d85bade86e59885112a7f4277af5ad47ab919a2251f306"}}, - {name = "multidict-6.6.3-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl",url = "https://files.pythonhosted.org/packages/58/98/17f1f80bdba0b2fef49cf4ba59cebf8a81797f745f547abb5c9a4039df62/multidict-6.6.3-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl",hashes = {sha256 = "775b464d31dac90f23192af9c291dc9f423101857e33e9ebf0020a10bfcf4144"}}, - {name = "multidict-6.6.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",url = "https://files.pythonhosted.org/packages/f8/0e/a5e595fdd0820069f0c29911d5dc9dc3a75ec755ae733ce59a4e6962ae42/multidict-6.6.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",hashes = {sha256 = "d04d01f0a913202205a598246cf77826fe3baa5a63e9f6ccf1ab0601cf56eca0"}}, - {name = "multidict-6.6.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/66/9e/0f51e4cffea2daf24c137feabc9ec848ce50f8379c9badcbac00b41ab55e/multidict-6.6.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "d25594d3b38a2e6cabfdcafef339f754ca6e81fbbdb6650ad773ea9775af35ab"}}, - {name = "multidict-6.6.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/49/a0/a7cfc13c9a71ceb8c1c55457820733af9ce01e121139271f7b13e30c29d2/multidict-6.6.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "35712f1748d409e0707b165bf49f9f17f9e28ae85470c41615778f8d4f7d9609"}}, - {name = "multidict-6.6.3-cp39-cp39-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/c7/50/7ae0d1149ac71cab6e20bb7faf2a1868435974994595dadfdb7377f7140f/multidict-6.6.3-cp39-cp39-musllinux_1_2_aarch64.whl",hashes = {sha256 = "1c8082e5814b662de8589d6a06c17e77940d5539080cbab9fe6794b5241b76d9"}}, - {name = "multidict-6.6.3-cp39-cp39-musllinux_1_2_armv7l.whl",url = "https://files.pythonhosted.org/packages/b4/ac/2d0bf836c9c63a57360d57b773359043b371115e1c78ff648993bf19abd0/multidict-6.6.3-cp39-cp39-musllinux_1_2_armv7l.whl",hashes = {sha256 = "61af8a4b771f1d4d000b3168c12c3120ccf7284502a94aa58c68a81f5afac090"}}, - {name = "multidict-6.6.3-cp39-cp39-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/85/e1/68a65f069df298615591e70e48bfd379c27d4ecb252117c18bf52eebc237/multidict-6.6.3-cp39-cp39-musllinux_1_2_i686.whl",hashes = {sha256 = "448e4a9afccbf297577f2eaa586f07067441e7b63c8362a3540ba5a38dc0f14a"}}, - {name = "multidict-6.6.3-cp39-cp39-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/ae/ab/702f1baca649f88ea1dc6259fc2aa4509f4ad160ba48c8e61fbdb4a5a365/multidict-6.6.3-cp39-cp39-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "233ad16999afc2bbd3e534ad8dbe685ef8ee49a37dbc2cdc9514e57b6d589ced"}}, - {name = "multidict-6.6.3-cp39-cp39-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/5e/0b/726e690bfbf887985a8710ef2f25f1d6dd184a35bd3b36429814f810a2fc/multidict-6.6.3-cp39-cp39-musllinux_1_2_s390x.whl",hashes = {sha256 = "bb933c891cd4da6bdcc9733d048e994e22e1883287ff7540c2a0f3b117605092"}}, - {name = "multidict-6.6.3-cp39-cp39-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/73/bb/839486b27bcbcc2e0d875fb9d4012b4b6aa99639137343106aa7210e047a/multidict-6.6.3-cp39-cp39-musllinux_1_2_x86_64.whl",hashes = {sha256 = "37b09ca60998e87734699e88c2363abfd457ed18cfbf88e4009a4e83788e63ed"}}, - {name = "multidict-6.6.3-cp39-cp39-win32.whl",url = "https://files.pythonhosted.org/packages/e3/46/574d75ab7b9ae8690fe27e89f5fcd0121633112b438edfb9ed2be8be096b/multidict-6.6.3-cp39-cp39-win32.whl",hashes = {sha256 = "f54cb79d26d0cd420637d184af38f0668558f3c4bbe22ab7ad830e67249f2e0b"}}, - {name = "multidict-6.6.3-cp39-cp39-win_amd64.whl",url = "https://files.pythonhosted.org/packages/78/c3/8b3bc755508b777868349f4bfa844d3d31832f075ee800a3d6f1807338c5/multidict-6.6.3-cp39-cp39-win_amd64.whl",hashes = {sha256 = "295adc9c0551e5d5214b45cf29ca23dbc28c2d197a9c30d51aed9e037cb7c578"}}, - {name = "multidict-6.6.3-cp39-cp39-win_arm64.whl",url = "https://files.pythonhosted.org/packages/b2/30/5a66e7e4550e80975faee5b5dd9e9bd09194d2fd8f62363119b9e46e204b/multidict-6.6.3-cp39-cp39-win_arm64.whl",hashes = {sha256 = "15332783596f227db50fb261c2c251a58ac3873c457f3a550a95d5c0aa3c770d"}}, -] -marker = "\"default\" in dependency_groups or \"dev\" in extras" - -[packages.tool.pdm] -dependencies = [ - "typing-extensions>=4.1.0; python_version < \"3.11\"", -] - [[packages]] name = "regex" version = "2024.11.6" @@ -1823,53 +1592,6 @@ wheels = [ {name = "regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/93/2d/dd56bb76bd8e95bbce684326302f287455b56242a4f9c61f1bc76e28360e/regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad"}}, {name = "regex-2024.11.6-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/0b/55/31877a249ab7a5156758246b9c59539abbeba22461b7d8adc9e8475ff73e/regex-2024.11.6-cp312-cp312-win32.whl",hashes = {sha256 = "32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54"}}, {name = "regex-2024.11.6-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/38/ec/ad2d7de49a600cdb8dd78434a1aeffe28b9d6fc42eb36afab4a27ad23384/regex-2024.11.6-cp312-cp312-win_amd64.whl",hashes = {sha256 = "a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b"}}, - {name = "regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl",url = "https://files.pythonhosted.org/packages/58/58/7e4d9493a66c88a7da6d205768119f51af0f684fe7be7bac8328e217a52c/regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl",hashes = {sha256 = "5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638"}}, - {name = "regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/34/4c/8f8e631fcdc2ff978609eaeef1d6994bf2f028b59d9ac67640ed051f1218/regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl",hashes = {sha256 = "2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7"}}, - {name = "regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/c5/1b/f0e4d13e6adf866ce9b069e191f303a30ab1277e037037a365c3aad5cc9c/regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20"}}, - {name = "regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/25/4d/ab21047f446693887f25510887e6820b93f791992994f6498b0318904d4a/regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114"}}, - {name = "regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/45/ee/c867e15cd894985cb32b731d89576c41a4642a57850c162490ea34b78c3b/regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3"}}, - {name = "regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/b3/12/b0f480726cf1c60f6536fa5e1c95275a77624f3ac8fdccf79e6727499e28/regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f"}}, - {name = "regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/bf/ce/0d0e61429f603bac433910d99ef1a02ce45a8967ffbe3cbee48599e62d88/regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0"}}, - {name = "regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/e4/c1/243c83c53d4a419c1556f43777ccb552bccdf79d08fda3980e4e77dd9137/regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55"}}, - {name = "regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/c5/f4/75eb0dd4ce4b37f04928987f1d22547ddaf6c4bae697623c1b05da67a8aa/regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89"}}, - {name = "regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/16/5d/95c568574e630e141a69ff8a254c2f188b4398e813c40d49228c9bbd9875/regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl",hashes = {sha256 = "202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d"}}, - {name = "regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/8e/b5/f8495c7917f15cc6fee1e7f395e324ec3e00ab3c665a7dc9d27562fd5290/regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34"}}, - {name = "regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/1c/80/6dd7118e8cb212c3c60b191b932dc57db93fb2e36fb9e0e92f72a5909af9/regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl",hashes = {sha256 = "068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d"}}, - {name = "regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/11/9b/5a05d2040297d2d254baf95eeeb6df83554e5e1df03bc1a6687fc4ba1f66/regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45"}}, - {name = "regex-2024.11.6-cp311-cp311-win32.whl",url = "https://files.pythonhosted.org/packages/26/b7/b14e2440156ab39e0177506c08c18accaf2b8932e39fb092074de733d868/regex-2024.11.6-cp311-cp311-win32.whl",hashes = {sha256 = "c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9"}}, - {name = "regex-2024.11.6-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/80/32/763a6cc01d21fb3819227a1cc3f60fd251c13c37c27a73b8ff4315433a8e/regex-2024.11.6-cp311-cp311-win_amd64.whl",hashes = {sha256 = "02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60"}}, - {name = "regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl",url = "https://files.pythonhosted.org/packages/95/3c/4651f6b130c6842a8f3df82461a8950f923925db8b6961063e82744bddcc/regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl",hashes = {sha256 = "ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91"}}, - {name = "regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/15/51/9f35d12da8434b489c7b7bffc205c474a0a9432a889457026e9bc06a297a/regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl",hashes = {sha256 = "658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0"}}, - {name = "regex-2024.11.6-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/bd/18/b731f5510d1b8fb63c6b6d3484bfa9a59b84cc578ac8b5172970e05ae07c/regex-2024.11.6-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e"}}, - {name = "regex-2024.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/78/a2/6dd36e16341ab95e4c6073426561b9bfdeb1a9c9b63ab1b579c2e96cb105/regex-2024.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde"}}, - {name = "regex-2024.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/1b/2b/323e72d5d2fd8de0d9baa443e1ed70363ed7e7b2fb526f5950c5cb99c364/regex-2024.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e"}}, - {name = "regex-2024.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/90/30/63373b9ea468fbef8a907fd273e5c329b8c9535fee36fc8dba5fecac475d/regex-2024.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2"}}, - {name = "regex-2024.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/f2/98/26d3830875b53071f1f0ae6d547f1d98e964dd29ad35cbf94439120bb67a/regex-2024.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf"}}, - {name = "regex-2024.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/87/55/eb2a068334274db86208ab9d5599ffa63631b9f0f67ed70ea7c82a69bbc8/regex-2024.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c"}}, - {name = "regex-2024.11.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl",url = "https://files.pythonhosted.org/packages/74/c0/be707bcfe98254d8f9d2cff55d216e946f4ea48ad2fd8cf1428f8c5332ba/regex-2024.11.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl",hashes = {sha256 = "f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86"}}, - {name = "regex-2024.11.6-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/49/dc/bb45572ceb49e0f6509f7596e4ba7031f6819ecb26bc7610979af5a77f45/regex-2024.11.6-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67"}}, - {name = "regex-2024.11.6-cp310-cp310-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/5a/db/f43fd75dc4c0c2d96d0881967897926942e935d700863666f3c844a72ce6/regex-2024.11.6-cp310-cp310-musllinux_1_2_i686.whl",hashes = {sha256 = "040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d"}}, - {name = "regex-2024.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/99/d7/f94154db29ab5a89d69ff893159b19ada89e76b915c1293e98603d39838c/regex-2024.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2"}}, - {name = "regex-2024.11.6-cp310-cp310-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/f7/17/3cbfab1f23356fbbf07708220ab438a7efa1e0f34195bf857433f79f1788/regex-2024.11.6-cp310-cp310-musllinux_1_2_s390x.whl",hashes = {sha256 = "8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008"}}, - {name = "regex-2024.11.6-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/7e/f2/48b393b51900456155de3ad001900f94298965e1cad1c772b87f9cfea011/regex-2024.11.6-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62"}}, - {name = "regex-2024.11.6-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/45/3f/ef9589aba93e084cd3f8471fded352826dcae8489b650d0b9b27bc5bba8a/regex-2024.11.6-cp310-cp310-win32.whl",hashes = {sha256 = "b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e"}}, - {name = "regex-2024.11.6-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/42/7e/5f1b92c8468290c465fd50c5318da64319133231415a8aa6ea5ab995a815/regex-2024.11.6-cp310-cp310-win_amd64.whl",hashes = {sha256 = "5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519"}}, - {name = "regex-2024.11.6-cp39-cp39-macosx_10_9_universal2.whl",url = "https://files.pythonhosted.org/packages/89/23/c4a86df398e57e26f93b13ae63acce58771e04bdde86092502496fa57f9c/regex-2024.11.6-cp39-cp39-macosx_10_9_universal2.whl",hashes = {sha256 = "5704e174f8ccab2026bd2f1ab6c510345ae8eac818b613d7d73e785f1310f839"}}, - {name = "regex-2024.11.6-cp39-cp39-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/3c/8b/45c24ab7a51a1658441b961b86209c43e6bb9d39caf1e63f46ce6ea03bc7/regex-2024.11.6-cp39-cp39-macosx_10_9_x86_64.whl",hashes = {sha256 = "220902c3c5cc6af55d4fe19ead504de80eb91f786dc102fbd74894b1551f095e"}}, - {name = "regex-2024.11.6-cp39-cp39-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/7a/d1/598de10b17fdafc452d11f7dada11c3be4e379a8671393e4e3da3c4070df/regex-2024.11.6-cp39-cp39-macosx_11_0_arm64.whl",hashes = {sha256 = "5e7e351589da0850c125f1600a4c4ba3c722efefe16b297de54300f08d734fbf"}}, - {name = "regex-2024.11.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/49/70/c7eaa219efa67a215846766fde18d92d54cb590b6a04ffe43cef30057622/regex-2024.11.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "5056b185ca113c88e18223183aa1a50e66507769c9640a6ff75859619d73957b"}}, - {name = "regex-2024.11.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/89/e5/ef52c7eb117dd20ff1697968219971d052138965a4d3d9b95e92e549f505/regex-2024.11.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "2e34b51b650b23ed3354b5a07aab37034d9f923db2a40519139af34f485f77d0"}}, - {name = "regex-2024.11.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/5f/3f/9f5da81aff1d4167ac52711acf789df13e789fe6ac9545552e49138e3282/regex-2024.11.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "5670bce7b200273eee1840ef307bfa07cda90b38ae56e9a6ebcc9f50da9c469b"}}, - {name = "regex-2024.11.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/86/44/2101cc0890c3621b90365c9ee8d7291a597c0722ad66eccd6ffa7f1bcc09/regex-2024.11.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "08986dce1339bc932923e7d1232ce9881499a0e02925f7402fb7c982515419ef"}}, - {name = "regex-2024.11.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/ce/2e/3e0668d8d1c7c3c0d397bf54d92fc182575b3a26939aed5000d3cc78760f/regex-2024.11.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "93c0b12d3d3bc25af4ebbf38f9ee780a487e8bf6954c115b9f015822d3bb8e48"}}, - {name = "regex-2024.11.6-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl",url = "https://files.pythonhosted.org/packages/a6/49/1bc4584254355e3dba930a3a2fd7ad26ccba3ebbab7d9100db0aff2eedb0/regex-2024.11.6-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl",hashes = {sha256 = "764e71f22ab3b305e7f4c21f1a97e1526a25ebdd22513e251cf376760213da13"}}, - {name = "regex-2024.11.6-cp39-cp39-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/c8/dd/42879c1fc8a37a887cd08e358af3d3ba9e23038cd77c7fe044a86d9450ba/regex-2024.11.6-cp39-cp39-musllinux_1_2_aarch64.whl",hashes = {sha256 = "f056bf21105c2515c32372bbc057f43eb02aae2fda61052e2f7622c801f0b4e2"}}, - {name = "regex-2024.11.6-cp39-cp39-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/89/96/c05a0fe173cd2acd29d5e13c1adad8b706bcaa71b169e1ee57dcf2e74584/regex-2024.11.6-cp39-cp39-musllinux_1_2_i686.whl",hashes = {sha256 = "69ab78f848845569401469da20df3e081e6b5a11cb086de3eed1d48f5ed57c95"}}, - {name = "regex-2024.11.6-cp39-cp39-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/b5/f3/a757748066255f97f14506483436c5f6aded7af9e37bca04ec30c90ca683/regex-2024.11.6-cp39-cp39-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "86fddba590aad9208e2fa8b43b4c098bb0ec74f15718bb6a704e3c63e2cef3e9"}}, - {name = "regex-2024.11.6-cp39-cp39-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/5c/93/c6d2092fd479dcaeea40fc8fa673822829181ded77d294a7f950f1dda6e2/regex-2024.11.6-cp39-cp39-musllinux_1_2_s390x.whl",hashes = {sha256 = "684d7a212682996d21ca12ef3c17353c021fe9de6049e19ac8481ec35574a70f"}}, - {name = "regex-2024.11.6-cp39-cp39-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/ff/9c/daa99532c72f25051a90ef90e1413a8d54413a9e64614d9095b0c1c154d0/regex-2024.11.6-cp39-cp39-musllinux_1_2_x86_64.whl",hashes = {sha256 = "a03e02f48cd1abbd9f3b7e3586d97c8f7a9721c436f51a5245b3b9483044480b"}}, - {name = "regex-2024.11.6-cp39-cp39-win32.whl",url = "https://files.pythonhosted.org/packages/13/5d/61a533ccb8c231b474ac8e3a7d70155b00dfc61af6cafdccd1947df6d735/regex-2024.11.6-cp39-cp39-win32.whl",hashes = {sha256 = "41758407fc32d5c3c5de163888068cfee69cb4c2be844e7ac517a52770f9af57"}}, - {name = "regex-2024.11.6-cp39-cp39-win_amd64.whl",url = "https://files.pythonhosted.org/packages/dc/7b/e59b7f7c91ae110d154370c24133f947262525b5d6406df65f23422acc17/regex-2024.11.6-cp39-cp39-win_amd64.whl",hashes = {sha256 = "b2837718570f95dd41675328e111345f9b7095d821bac435aac173ac80b19983"}}, ] marker = "\"default\" in dependency_groups or \"recommended\" in extras" @@ -1945,7 +1667,7 @@ sdist = {name = "platformdirs-4.3.8.tar.gz", url = "https://files.pythonhosted.o wheels = [ {name = "platformdirs-4.3.8-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl",hashes = {sha256 = "ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}}, ] -marker = "\"dev\" in extras" +marker = "\"default\" in dependency_groups or \"dev\" in extras" [packages.tool.pdm] dependencies = [] @@ -2019,58 +1741,7 @@ wheels = [ {name = "yarl-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/af/85/9363f77bdfa1e4d690957cd39d192c4cacd1c58965df0470a4905253b54f/yarl-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "564ab3d517e3d01c408c67f2e5247aad4019dcf1969982aba3974b4093279004"}}, {name = "yarl-1.20.1-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/35/99/9918c8739ba271dcd935400cff8b32e3cd319eaf02fcd023d5dcd487a7c8/yarl-1.20.1-cp312-cp312-win32.whl",hashes = {sha256 = "daea0d313868da1cf2fac6b2d3a25c6e3a9e879483244be38c8e6a41f1d876a5"}}, {name = "yarl-1.20.1-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/eb/83/5d9092950565481b413b31a23e75dd3418ff0a277d6e0abf3729d4d1ce25/yarl-1.20.1-cp312-cp312-win_amd64.whl",hashes = {sha256 = "48ea7d7f9be0487339828a4de0360d7ce0efc06524a48e1810f945c45b813698"}}, - {name = "yarl-1.20.1-cp311-cp311-macosx_10_9_universal2.whl",url = "https://files.pythonhosted.org/packages/b1/18/893b50efc2350e47a874c5c2d67e55a0ea5df91186b2a6f5ac52eff887cd/yarl-1.20.1-cp311-cp311-macosx_10_9_universal2.whl",hashes = {sha256 = "47ee6188fea634bdfaeb2cc420f5b3b17332e6225ce88149a17c413c77ff269e"}}, - {name = "yarl-1.20.1-cp311-cp311-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/89/ed/b8773448030e6fc47fa797f099ab9eab151a43a25717f9ac043844ad5ea3/yarl-1.20.1-cp311-cp311-macosx_10_9_x86_64.whl",hashes = {sha256 = "d0f6500f69e8402d513e5eedb77a4e1818691e8f45e6b687147963514d84b44b"}}, - {name = "yarl-1.20.1-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/e3/e3/409bd17b1e42619bf69f60e4f031ce1ccb29bd7380117a55529e76933464/yarl-1.20.1-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "7a8900a42fcdaad568de58887c7b2f602962356908eedb7628eaf6021a6e435b"}}, - {name = "yarl-1.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/f8/77/64d8431a4d77c856eb2d82aa3de2ad6741365245a29b3a9543cd598ed8c5/yarl-1.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "bad6d131fda8ef508b36be3ece16d0902e80b88ea7200f030a0f6c11d9e508d4"}}, - {name = "yarl-1.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl",url = "https://files.pythonhosted.org/packages/8d/d2/0c7e4def093dcef0bd9fa22d4d24b023788b0a33b8d0088b51aa51e21e99/yarl-1.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl",hashes = {sha256 = "df018d92fe22aaebb679a7f89fe0c0f368ec497e3dda6cb81a567610f04501f1"}}, - {name = "yarl-1.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/f0/f3/fc514f4b2cf02cb59d10cbfe228691d25929ce8f72a38db07d3febc3f706/yarl-1.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "8f969afbb0a9b63c18d0feecf0db09d164b7a44a053e78a7d05f5df163e43833"}}, - {name = "yarl-1.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/ea/6d/a313ac8d8391381ff9006ac05f1d4331cee3b1efaa833a53d12253733255/yarl-1.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "812303eb4aa98e302886ccda58d6b099e3576b1b9276161469c25803a8db277d"}}, - {name = "yarl-1.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/00/70/8f78a95d6935a70263d46caa3dd18e1f223cf2f2ff2037baa01a22bc5b22/yarl-1.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "98c4a7d166635147924aa0bf9bfe8d8abad6fffa6102de9c99ea04a1376f91e8"}}, - {name = "yarl-1.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/cb/05/42773027968968f4f15143553970ee36ead27038d627f457cc44bbbeecf3/yarl-1.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "12e768f966538e81e6e7550f9086a6236b16e26cd964cf4df35349970f3551cf"}}, - {name = "yarl-1.20.1-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/05/be/665634aa196954156741ea591d2f946f1b78ceee8bb8f28488bf28c0dd62/yarl-1.20.1-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "fe41919b9d899661c5c28a8b4b0acf704510b88f27f0934ac7a7bebdd8938d5e"}}, - {name = "yarl-1.20.1-cp311-cp311-musllinux_1_2_armv7l.whl",url = "https://files.pythonhosted.org/packages/eb/90/73448401d36fa4e210ece5579895731f190d5119c4b66b43b52182e88cd5/yarl-1.20.1-cp311-cp311-musllinux_1_2_armv7l.whl",hashes = {sha256 = "8601bc010d1d7780592f3fc1bdc6c72e2b6466ea34569778422943e1a1f3c389"}}, - {name = "yarl-1.20.1-cp311-cp311-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/c3/b0/fce922d46dc1eb43c811f1889f7daa6001b27a4005587e94878570300881/yarl-1.20.1-cp311-cp311-musllinux_1_2_i686.whl",hashes = {sha256 = "daadbdc1f2a9033a2399c42646fbd46da7992e868a5fe9513860122d7fe7a73f"}}, - {name = "yarl-1.20.1-cp311-cp311-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/f1/0d/b172628fce039dae8977fd22caeff3eeebffd52e86060413f5673767c427/yarl-1.20.1-cp311-cp311-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "03aa1e041727cb438ca762628109ef1333498b122e4c76dd858d186a37cec845"}}, - {name = "yarl-1.20.1-cp311-cp311-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/6b/9b/5b886d7671f4580209e855974fe1cecec409aa4a89ea58b8f0560dc529b1/yarl-1.20.1-cp311-cp311-musllinux_1_2_s390x.whl",hashes = {sha256 = "642980ef5e0fa1de5fa96d905c7e00cb2c47cb468bfcac5a18c58e27dbf8d8d1"}}, - {name = "yarl-1.20.1-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/73/be/75ef5fd0fcd8f083a5d13f78fd3f009528132a1f2a1d7c925c39fa20aa79/yarl-1.20.1-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "86971e2795584fe8c002356d3b97ef6c61862720eeff03db2a7c86b678d85b3e"}}, - {name = "yarl-1.20.1-cp311-cp311-win32.whl",url = "https://files.pythonhosted.org/packages/50/4f/62faab3b479dfdcb741fe9e3f0323e2a7d5cd1ab2edc73221d57ad4834b2/yarl-1.20.1-cp311-cp311-win32.whl",hashes = {sha256 = "597f40615b8d25812f14562699e287f0dcc035d25eb74da72cae043bb884d773"}}, - {name = "yarl-1.20.1-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/f0/09/d9c7942f8f05c32ec72cd5c8e041c8b29b5807328b68b4801ff2511d4d5e/yarl-1.20.1-cp311-cp311-win_amd64.whl",hashes = {sha256 = "26ef53a9e726e61e9cd1cda6b478f17e350fb5800b4bd1cd9fe81c4d91cfeb2e"}}, - {name = "yarl-1.20.1-cp310-cp310-macosx_10_9_universal2.whl",url = "https://files.pythonhosted.org/packages/cb/65/7fed0d774abf47487c64be14e9223749468922817b5e8792b8a64792a1bb/yarl-1.20.1-cp310-cp310-macosx_10_9_universal2.whl",hashes = {sha256 = "6032e6da6abd41e4acda34d75a816012717000fa6839f37124a47fcefc49bec4"}}, - {name = "yarl-1.20.1-cp310-cp310-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/8a/7b/988f55a52da99df9e56dc733b8e4e5a6ae2090081dc2754fc8fd34e60aa0/yarl-1.20.1-cp310-cp310-macosx_10_9_x86_64.whl",hashes = {sha256 = "2c7b34d804b8cf9b214f05015c4fee2ebe7ed05cf581e7192c06555c71f4446a"}}, - {name = "yarl-1.20.1-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/f7/de/30d98f03e95d30c7e3cc093759982d038c8833ec2451001d45ef4854edc1/yarl-1.20.1-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "0c869f2651cc77465f6cd01d938d91a11d9ea5d798738c1dc077f3de0b5e5fed"}}, - {name = "yarl-1.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/e0/7a/f2f314f5ebfe9200724b0b748de2186b927acb334cf964fd312eb86fc286/yarl-1.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "62915e6688eb4d180d93840cda4110995ad50c459bf931b8b3775b37c264af1e"}}, - {name = "yarl-1.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl",url = "https://files.pythonhosted.org/packages/15/3f/718d26f189db96d993d14b984ce91de52e76309d0fd1d4296f34039856aa/yarl-1.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl",hashes = {sha256 = "41ebd28167bc6af8abb97fec1a399f412eec5fd61a3ccbe2305a18b84fb4ca73"}}, - {name = "yarl-1.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/a5/76/8fcfbf5fa2369157b9898962a4a7d96764b287b085b5b3d9ffae69cdefd1/yarl-1.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "21242b4288a6d56f04ea193adde174b7e347ac46ce6bc84989ff7c1b1ecea84e"}}, - {name = "yarl-1.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/3c/95/d7fc301cc4661785967acc04f54a4a42d5124905e27db27bb578aac49b5c/yarl-1.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "bea21cdae6c7eb02ba02a475f37463abfe0a01f5d7200121b03e605d6a0439f8"}}, - {name = "yarl-1.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/65/94/e21269718349582eee81efc5c1c08ee71c816bfc1585b77d0ec3f58089eb/yarl-1.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "1f8a891e4a22a89f5dde7862994485e19db246b70bb288d3ce73a34422e55b23"}}, - {name = "yarl-1.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/32/ae/8616d1f07853704523519f6131d21f092e567c5af93de7e3e94b38d7f065/yarl-1.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "dd803820d44c8853a109a34e3660e5a61beae12970da479cf44aa2954019bf70"}}, - {name = "yarl-1.20.1-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/48/aa/0ace06280861ef055855333707db5e49c6e3a08840a7ce62682259d0a6c0/yarl-1.20.1-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "b982fa7f74c80d5c0c7b5b38f908971e513380a10fecea528091405f519b9ebb"}}, - {name = "yarl-1.20.1-cp310-cp310-musllinux_1_2_armv7l.whl",url = "https://files.pythonhosted.org/packages/20/52/1e9d0e6916f45a8fb50e6844f01cb34692455f1acd548606cbda8134cd1e/yarl-1.20.1-cp310-cp310-musllinux_1_2_armv7l.whl",hashes = {sha256 = "33f29ecfe0330c570d997bcf1afd304377f2e48f61447f37e846a6058a4d33b2"}}, - {name = "yarl-1.20.1-cp310-cp310-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/f2/65/60452df742952c630e82f394cd409de10610481d9043aa14c61bf846b7b1/yarl-1.20.1-cp310-cp310-musllinux_1_2_i686.whl",hashes = {sha256 = "835ab2cfc74d5eb4a6a528c57f05688099da41cf4957cf08cad38647e4a83b30"}}, - {name = "yarl-1.20.1-cp310-cp310-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/7b/f5/6cd4ff38dcde57a70f23719a838665ee17079640c77087404c3d34da6727/yarl-1.20.1-cp310-cp310-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "46b5e0ccf1943a9a6e766b2c2b8c732c55b34e28be57d8daa2b3c1d1d4009309"}}, - {name = "yarl-1.20.1-cp310-cp310-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/d1/90/c42eefd79d0d8222cb3227bdd51b640c0c1d0aa33fe4cc86c36eccba77d3/yarl-1.20.1-cp310-cp310-musllinux_1_2_s390x.whl",hashes = {sha256 = "df47c55f7d74127d1b11251fe6397d84afdde0d53b90bedb46a23c0e534f9d24"}}, - {name = "yarl-1.20.1-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/03/c8/cea6b232cb4617514232e0f8a718153a95b5d82b5290711b201545825532/yarl-1.20.1-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "76d12524d05841276b0e22573f28d5fbcb67589836772ae9244d90dd7d66aa13"}}, - {name = "yarl-1.20.1-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/ce/a3/eaa0ab9712f1f3d01faf43cf6f1f7210ce4ea4a7e9b28b489a2261ca8db9/yarl-1.20.1-cp310-cp310-win32.whl",hashes = {sha256 = "6c4fbf6b02d70e512d7ade4b1f998f237137f1417ab07ec06358ea04f69134f8"}}, - {name = "yarl-1.20.1-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/8f/34/e4abde70a9256465fe31c88ed02c3f8502b7b5dead693a4f350a06413f28/yarl-1.20.1-cp310-cp310-win_amd64.whl",hashes = {sha256 = "aef6c4d69554d44b7f9d923245f8ad9a707d971e6209d51279196d8e8fe1ae16"}}, {name = "yarl-1.20.1-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/b4/2d/2345fce04cfd4bee161bf1e7d9cdc702e3e16109021035dbb24db654a622/yarl-1.20.1-py3-none-any.whl",hashes = {sha256 = "83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77"}}, - {name = "yarl-1.20.1-cp39-cp39-macosx_10_9_universal2.whl",url = "https://files.pythonhosted.org/packages/01/75/0d37402d208d025afa6b5b8eb80e466d267d3fd1927db8e317d29a94a4cb/yarl-1.20.1-cp39-cp39-macosx_10_9_universal2.whl",hashes = {sha256 = "e42ba79e2efb6845ebab49c7bf20306c4edf74a0b20fc6b2ccdd1a219d12fad3"}}, - {name = "yarl-1.20.1-cp39-cp39-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/73/84/1fb6c85ae0cf9901046f07d0ac9eb162f7ce6d95db541130aa542ed377e6/yarl-1.20.1-cp39-cp39-macosx_10_9_x86_64.whl",hashes = {sha256 = "41493b9b7c312ac448b7f0a42a089dffe1d6e6e981a2d76205801a023ed26a2b"}}, - {name = "yarl-1.20.1-cp39-cp39-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/f3/9c/eae746b24c4ea29a5accba9a06c197a70fa38a49c7df244e0d3951108861/yarl-1.20.1-cp39-cp39-macosx_11_0_arm64.whl",hashes = {sha256 = "f5a5928ff5eb13408c62a968ac90d43f8322fd56d87008b8f9dabf3c0f6ee983"}}, - {name = "yarl-1.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/fb/30/693e71003ec4bc1daf2e4cf7c478c417d0985e0a8e8f00b2230d517876fc/yarl-1.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "30c41ad5d717b3961b2dd785593b67d386b73feca30522048d37298fee981805"}}, - {name = "yarl-1.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl",url = "https://files.pythonhosted.org/packages/0f/a2/5264dbebf90763139aeb0b0b3154763239398400f754ae19a0518b654117/yarl-1.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl",hashes = {sha256 = "59febc3969b0781682b469d4aca1a5cab7505a4f7b85acf6db01fa500fa3f6ba"}}, - {name = "yarl-1.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/e7/17/77c7a89b3c05856489777e922f41db79ab4faf58621886df40d812c7facd/yarl-1.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "d2b6fb3622b7e5bf7a6e5b679a69326b4279e805ed1699d749739a61d242449e"}}, - {name = "yarl-1.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/6d/55/28409330b8ef5f2f681f5b478150496ec9cf3309b149dab7ec8ab5cfa3f0/yarl-1.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "749d73611db8d26a6281086f859ea7ec08f9c4c56cec864e52028c8b328db723"}}, - {name = "yarl-1.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/85/58/cb0257cbd4002828ff735f44d3c5b6966c4fd1fc8cc1cd3cd8a143fbc513/yarl-1.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "9427925776096e664c39e131447aa20ec738bdd77c049c48ea5200db2237e000"}}, - {name = "yarl-1.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/53/f6/c77960370cfa46f6fb3d6a5a79a49d3abfdb9ef92556badc2dcd2748bc2a/yarl-1.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "ff70f32aa316393eaf8222d518ce9118148eddb8a53073c2403863b41033eed5"}}, - {name = "yarl-1.20.1-cp39-cp39-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/64/ab/be0b10b8e029553c10905b6b00c64ecad3ebc8ace44b02293a62579343f6/yarl-1.20.1-cp39-cp39-musllinux_1_2_aarch64.whl",hashes = {sha256 = "c7ddf7a09f38667aea38801da8b8d6bfe81df767d9dfc8c88eb45827b195cd1c"}}, - {name = "yarl-1.20.1-cp39-cp39-musllinux_1_2_armv7l.whl",url = "https://files.pythonhosted.org/packages/c5/c3/3f327bd3905a4916029bf5feb7f86dcf864c7704f099715f62155fb386b2/yarl-1.20.1-cp39-cp39-musllinux_1_2_armv7l.whl",hashes = {sha256 = "57edc88517d7fc62b174fcfb2e939fbc486a68315d648d7e74d07fac42cec240"}}, - {name = "yarl-1.20.1-cp39-cp39-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/d1/42/040bdd5d3b3bb02b4a6ace4ed4075e02f85df964d6e6cb321795d2a6496a/yarl-1.20.1-cp39-cp39-musllinux_1_2_i686.whl",hashes = {sha256 = "dab096ce479d5894d62c26ff4f699ec9072269d514b4edd630a393223f45a0ee"}}, - {name = "yarl-1.20.1-cp39-cp39-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/0d/1c/911867b8e8c7463b84dfdc275e0d99b04b66ad5132b503f184fe76be8ea4/yarl-1.20.1-cp39-cp39-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "14a85f3bd2d7bb255be7183e5d7d6e70add151a98edf56a770d6140f5d5f4010"}}, - {name = "yarl-1.20.1-cp39-cp39-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/e2/31/8c389f6c6ca0379b57b2da87f1f126c834777b4931c5ee8427dd65d0ff6b/yarl-1.20.1-cp39-cp39-musllinux_1_2_s390x.whl",hashes = {sha256 = "2c89b5c792685dd9cd3fa9761c1b9f46fc240c2a3265483acc1565769996a3f8"}}, - {name = "yarl-1.20.1-cp39-cp39-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/7f/09/ae4a649fb3964324c70a3e2b61f45e566d9ffc0affd2b974cbf628957673/yarl-1.20.1-cp39-cp39-musllinux_1_2_x86_64.whl",hashes = {sha256 = "69e9b141de5511021942a6866990aea6d111c9042235de90e08f94cf972ca03d"}}, - {name = "yarl-1.20.1-cp39-cp39-win32.whl",url = "https://files.pythonhosted.org/packages/8d/43/bbb4ed4c34d5bb62b48bf957f68cd43f736f79059d4f85225ab1ef80f4b9/yarl-1.20.1-cp39-cp39-win32.whl",hashes = {sha256 = "b5f307337819cdfdbb40193cad84978a029f847b0a357fbe49f712063cfc4f06"}}, - {name = "yarl-1.20.1-cp39-cp39-win_amd64.whl",url = "https://files.pythonhosted.org/packages/d7/cd/ce185848a7dba68ea69e932674b5c1a42a1852123584bccc5443120f857c/yarl-1.20.1-cp39-cp39-win_amd64.whl",hashes = {sha256 = "eae7bfe2069f9c1c5b05fc7fe5d612e5bbc089a39309904ee8b829e322dcad00"}}, ] marker = "\"default\" in dependency_groups or \"dev\" in extras" @@ -2135,61 +1806,26 @@ wheels = [ {name = "propcache-0.3.2-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/a4/7c/3f539fcae630408d0bd8bf3208b9a647ccad10976eda62402a80adf8fc34/propcache-0.3.2-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "62b4239611205294cc433845b914131b2a1f03500ff3c1ed093ed216b82621e1"}}, {name = "propcache-0.3.2-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/7c/d2/34b9eac8c35f79f8a962546b3e97e9d4b990c420ee66ac8255d5d9611648/propcache-0.3.2-cp312-cp312-win32.whl",hashes = {sha256 = "df4a81b9b53449ebc90cc4deefb052c1dd934ba85012aa912c7ea7b7e38b60c1"}}, {name = "propcache-0.3.2-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/19/61/d582be5d226cf79071681d1b46b848d6cb03d7b70af7063e33a2787eaa03/propcache-0.3.2-cp312-cp312-win_amd64.whl",hashes = {sha256 = "7046e79b989d7fe457bb755844019e10f693752d169076138abf17f31380800c"}}, - {name = "propcache-0.3.2-cp311-cp311-macosx_10_9_universal2.whl",url = "https://files.pythonhosted.org/packages/80/8d/e8b436717ab9c2cfc23b116d2c297305aa4cd8339172a456d61ebf5669b8/propcache-0.3.2-cp311-cp311-macosx_10_9_universal2.whl",hashes = {sha256 = "0b8d2f607bd8f80ddc04088bc2a037fdd17884a6fcadc47a96e334d72f3717be"}}, - {name = "propcache-0.3.2-cp311-cp311-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/d6/29/1e34000e9766d112171764b9fa3226fa0153ab565d0c242c70e9945318a7/propcache-0.3.2-cp311-cp311-macosx_10_9_x86_64.whl",hashes = {sha256 = "06766d8f34733416e2e34f46fea488ad5d60726bb9481d3cddf89a6fa2d9603f"}}, - {name = "propcache-0.3.2-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/46/92/1ad5af0df781e76988897da39b5f086c2bf0f028b7f9bd1f409bb05b6874/propcache-0.3.2-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "a2dc1f4a1df4fecf4e6f68013575ff4af84ef6f478fe5344317a65d38a8e6dc9"}}, - {name = "propcache-0.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/b3/ce/e96392460f9fb68461fabab3e095cb00c8ddf901205be4eae5ce246e5b7e/propcache-0.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "be29c4f4810c5789cf10ddf6af80b041c724e629fa51e308a7a0fb19ed1ef7bf"}}, - {name = "propcache-0.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/c5/2a/866726ea345299f7ceefc861a5e782b045545ae6940851930a6adaf1fca6/propcache-0.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "59d61f6970ecbd8ff2e9360304d5c8876a6abd4530cb752c06586849ac8a9dc9"}}, - {name = "propcache-0.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/de/03/07d992ccb6d930398689187e1b3c718339a1c06b8b145a8d9650e4726166/propcache-0.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "62180e0b8dbb6b004baec00a7983e4cc52f5ada9cd11f48c3528d8cfa7b96a66"}}, - {name = "propcache-0.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/5d/e6/116ba39448753b1330f48ab8ba927dcd6cf0baea8a0ccbc512dfb49ba670/propcache-0.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "c144ca294a204c470f18cf4c9d78887810d04a3e2fbb30eea903575a779159df"}}, - {name = "propcache-0.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/a6/85/f01f5d97e54e428885a5497ccf7f54404cbb4f906688a1690cd51bf597dc/propcache-0.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "c5c2a784234c28854878d68978265617aa6dc0780e53d44b4d67f3651a17a9a2"}}, - {name = "propcache-0.3.2-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/e3/79/7bf5ab9033b8b8194cc3f7cf1aaa0e9c3256320726f64a3e1f113a812dce/propcache-0.3.2-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "5745bc7acdafa978ca1642891b82c19238eadc78ba2aaa293c6863b304e552d7"}}, - {name = "propcache-0.3.2-cp311-cp311-musllinux_1_2_armv7l.whl",url = "https://files.pythonhosted.org/packages/31/0b/bd3e0c00509b609317df4a18e6b05a450ef2d9a963e1d8bc9c9415d86f30/propcache-0.3.2-cp311-cp311-musllinux_1_2_armv7l.whl",hashes = {sha256 = "c0075bf773d66fa8c9d41f66cc132ecc75e5bb9dd7cce3cfd14adc5ca184cb95"}}, - {name = "propcache-0.3.2-cp311-cp311-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/7a/23/fae0ff9b54b0de4e819bbe559508da132d5683c32d84d0dc2ccce3563ed4/propcache-0.3.2-cp311-cp311-musllinux_1_2_i686.whl",hashes = {sha256 = "5f57aa0847730daceff0497f417c9de353c575d8da3579162cc74ac294c5369e"}}, - {name = "propcache-0.3.2-cp311-cp311-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/b7/7f/ad6a3c22630aaa5f618b4dc3c3598974a72abb4c18e45a50b3cdd091eb2f/propcache-0.3.2-cp311-cp311-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "eef914c014bf72d18efb55619447e0aecd5fb7c2e3fa7441e2e5d6099bddff7e"}}, - {name = "propcache-0.3.2-cp311-cp311-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/5b/2c/ba4f1c0e8a4b4c75910742f0d333759d441f65a1c7f34683b4a74c0ee015/propcache-0.3.2-cp311-cp311-musllinux_1_2_s390x.whl",hashes = {sha256 = "2a4092e8549031e82facf3decdbc0883755d5bbcc62d3aea9d9e185549936dcf"}}, - {name = "propcache-0.3.2-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/88/e4/ebe30fc399e98572019eee82ad0caf512401661985cbd3da5e3140ffa1b0/propcache-0.3.2-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "85871b050f174bc0bfb437efbdb68aaf860611953ed12418e4361bc9c392749e"}}, - {name = "propcache-0.3.2-cp311-cp311-win32.whl",url = "https://files.pythonhosted.org/packages/96/0a/7d5260b914e01d1d0906f7f38af101f8d8ed0dc47426219eeaf05e8ea7c2/propcache-0.3.2-cp311-cp311-win32.whl",hashes = {sha256 = "36c8d9b673ec57900c3554264e630d45980fd302458e4ac801802a7fd2ef7897"}}, - {name = "propcache-0.3.2-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/e1/2d/89fe4489a884bc0da0c3278c552bd4ffe06a1ace559db5ef02ef24ab446b/propcache-0.3.2-cp311-cp311-win_amd64.whl",hashes = {sha256 = "e53af8cb6a781b02d2ea079b5b853ba9430fcbe18a8e3ce647d5982a3ff69f39"}}, - {name = "propcache-0.3.2-cp310-cp310-macosx_10_9_universal2.whl",url = "https://files.pythonhosted.org/packages/ab/14/510deed325e262afeb8b360043c5d7c960da7d3ecd6d6f9496c9c56dc7f4/propcache-0.3.2-cp310-cp310-macosx_10_9_universal2.whl",hashes = {sha256 = "22d9962a358aedbb7a2e36187ff273adeaab9743373a272976d2e348d08c7770"}}, - {name = "propcache-0.3.2-cp310-cp310-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/cd/4e/ad52a7925ff01c1325653a730c7ec3175a23f948f08626a534133427dcff/propcache-0.3.2-cp310-cp310-macosx_10_9_x86_64.whl",hashes = {sha256 = "0d0fda578d1dc3f77b6b5a5dce3b9ad69a8250a891760a548df850a5e8da87f3"}}, - {name = "propcache-0.3.2-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/63/7c/e9399ba5da7780871db4eac178e9c2e204c23dd3e7d32df202092a1ed400/propcache-0.3.2-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "3def3da3ac3ce41562d85db655d18ebac740cb3fa4367f11a52b3da9d03a5cc3"}}, - {name = "propcache-0.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/22/e1/58da211eb8fdc6fc854002387d38f415a6ca5f5c67c1315b204a5d3e9d7a/propcache-0.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "9bec58347a5a6cebf239daba9bda37dffec5b8d2ce004d9fe4edef3d2815137e"}}, - {name = "propcache-0.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/c4/0a/550ea0f52aac455cb90111c8bab995208443e46d925e51e2f6ebdf869525/propcache-0.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "55ffda449a507e9fbd4aca1a7d9aa6753b07d6166140e5a18d2ac9bc49eac220"}}, - {name = "propcache-0.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/5a/af/9893b7d878deda9bb69fcf54600b247fba7317761b7db11fede6e0f28bd0/propcache-0.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "64a67fb39229a8a8491dd42f864e5e263155e729c2e7ff723d6e25f596b1e8cb"}}, - {name = "propcache-0.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/7c/bb/38fd08b278ca85cde36d848091ad2b45954bc5f15cce494bb300b9285831/propcache-0.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "9da1cf97b92b51253d5b68cf5a2b9e0dafca095e36b7f2da335e27dc6172a614"}}, - {name = "propcache-0.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/78/8c/9fe55bd01d362bafb413dfe508c48753111a1e269737fa143ba85693592c/propcache-0.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "5f559e127134b07425134b4065be45b166183fdcb433cb6c24c8e4149056ad50"}}, - {name = "propcache-0.3.2-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/54/14/4701c33852937a22584e08abb531d654c8bcf7948a8f87ad0a4822394147/propcache-0.3.2-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "aff2e4e06435d61f11a428360a932138d0ec288b0a31dd9bd78d200bd4a2b339"}}, - {name = "propcache-0.3.2-cp310-cp310-musllinux_1_2_armv7l.whl",url = "https://files.pythonhosted.org/packages/16/44/447f2253d859602095356007657ee535e0093215ea0b3d1d6a41d16e5201/propcache-0.3.2-cp310-cp310-musllinux_1_2_armv7l.whl",hashes = {sha256 = "4927842833830942a5d0a56e6f4839bc484785b8e1ce8d287359794818633ba0"}}, - {name = "propcache-0.3.2-cp310-cp310-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/f2/b3/e4756258749bb2d3b46defcff606a2f47410bab82be5824a67e84015b267/propcache-0.3.2-cp310-cp310-musllinux_1_2_i686.whl",hashes = {sha256 = "6107ddd08b02654a30fb8ad7a132021759d750a82578b94cd55ee2772b6ebea2"}}, - {name = "propcache-0.3.2-cp310-cp310-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/1e/df/e6d3c7574233164b6330b9fd697beeac402afd367280e6dc377bb99b43d9/propcache-0.3.2-cp310-cp310-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "70bd8b9cd6b519e12859c99f3fc9a93f375ebd22a50296c3a295028bea73b9e7"}}, - {name = "propcache-0.3.2-cp310-cp310-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/b2/53/e4d31dd5170b4a0e2e6b730f2385a96410633b4833dc25fe5dffd1f73294/propcache-0.3.2-cp310-cp310-musllinux_1_2_s390x.whl",hashes = {sha256 = "2183111651d710d3097338dd1893fcf09c9f54e27ff1a8795495a16a469cc90b"}}, - {name = "propcache-0.3.2-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/7f/fe/74d54cf9fbe2a20ff786e5f7afcfde446588f0cf15fb2daacfbc267b866c/propcache-0.3.2-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "fb075ad271405dcad8e2a7ffc9a750a3bf70e533bd86e89f0603e607b93aa64c"}}, - {name = "propcache-0.3.2-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/22/ec/c469c9d59dada8a7679625e0440b544fe72e99311a4679c279562051f6fc/propcache-0.3.2-cp310-cp310-win32.whl",hashes = {sha256 = "404d70768080d3d3bdb41d0771037da19d8340d50b08e104ca0e7f9ce55fce70"}}, - {name = "propcache-0.3.2-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/38/35/07a471371ac89d418f8d0b699c75ea6dca2041fbda360823de21f6a9ce0a/propcache-0.3.2-cp310-cp310-win_amd64.whl",hashes = {sha256 = "7435d766f978b4ede777002e6b3b6641dd229cd1da8d3d3106a45770365f9ad9"}}, {name = "propcache-0.3.2-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl",hashes = {sha256 = "98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f"}}, - {name = "propcache-0.3.2-cp39-cp39-macosx_10_9_universal2.whl",url = "https://files.pythonhosted.org/packages/6c/39/8ea9bcfaaff16fd0b0fc901ee522e24c9ec44b4ca0229cfffb8066a06959/propcache-0.3.2-cp39-cp39-macosx_10_9_universal2.whl",hashes = {sha256 = "a7fad897f14d92086d6b03fdd2eb844777b0c4d7ec5e3bac0fbae2ab0602bbe5"}}, - {name = "propcache-0.3.2-cp39-cp39-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/d3/85/cab84c86966e1d354cf90cdc4ba52f32f99a5bca92a1529d666d957d7686/propcache-0.3.2-cp39-cp39-macosx_10_9_x86_64.whl",hashes = {sha256 = "1f43837d4ca000243fd7fd6301947d7cb93360d03cd08369969450cc6b2ce3b4"}}, - {name = "propcache-0.3.2-cp39-cp39-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/23/f7/9cb719749152d8b26d63801b3220ce2d3931312b2744d2b3a088b0ee9947/propcache-0.3.2-cp39-cp39-macosx_11_0_arm64.whl",hashes = {sha256 = "261df2e9474a5949c46e962065d88eb9b96ce0f2bd30e9d3136bcde84befd8f2"}}, - {name = "propcache-0.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/a2/a2/0b2b5a210ff311260002a315f6f9531b65a36064dfb804655432b2f7d3e3/propcache-0.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "e514326b79e51f0a177daab1052bc164d9d9e54133797a3a58d24c9c87a3fe6d"}}, - {name = "propcache-0.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/3f/e0/7aff5de0c535f783b0c8be5bdb750c305c1961d69fbb136939926e155d98/propcache-0.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "d4a996adb6904f85894570301939afeee65f072b4fd265ed7e569e8d9058e4ec"}}, - {name = "propcache-0.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/92/1d/65fa889eb3b2a7d6e4ed3c2b568a9cb8817547a1450b572de7bf24872800/propcache-0.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "76cace5d6b2a54e55b137669b30f31aa15977eeed390c7cbfb1dafa8dfe9a701"}}, - {name = "propcache-0.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/9a/e2/eecf6989870988dfd731de408a6fa366e853d361a06c2133b5878ce821ad/propcache-0.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "31248e44b81d59d6addbb182c4720f90b44e1efdc19f58112a3c3a1615fb47ef"}}, - {name = "propcache-0.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/12/06/c32be4950967f18f77489268488c7cdc78cbfc65a8ba8101b15e526b83dc/propcache-0.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "abb7fa19dbf88d3857363e0493b999b8011eea856b846305d8c0512dfdf8fbb1"}}, - {name = "propcache-0.3.2-cp39-cp39-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/46/6c/17b521a6b3b7cbe277a4064ff0aa9129dd8c89f425a5a9b6b4dd51cc3ff4/propcache-0.3.2-cp39-cp39-musllinux_1_2_aarch64.whl",hashes = {sha256 = "d81ac3ae39d38588ad0549e321e6f773a4e7cc68e7751524a22885d5bbadf886"}}, - {name = "propcache-0.3.2-cp39-cp39-musllinux_1_2_armv7l.whl",url = "https://files.pythonhosted.org/packages/62/cb/3bdba2b736b3e45bc0e40f4370f745b3e711d439ffbffe3ae416393eece9/propcache-0.3.2-cp39-cp39-musllinux_1_2_armv7l.whl",hashes = {sha256 = "cc2782eb0f7a16462285b6f8394bbbd0e1ee5f928034e941ffc444012224171b"}}, - {name = "propcache-0.3.2-cp39-cp39-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/29/bd/760c5c6a60a4a2c55a421bc34a25ba3919d49dee411ddb9d1493bb51d46e/propcache-0.3.2-cp39-cp39-musllinux_1_2_i686.whl",hashes = {sha256 = "db429c19a6c7e8a1c320e6a13c99799450f411b02251fb1b75e6217cf4a14fcb"}}, - {name = "propcache-0.3.2-cp39-cp39-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/76/58/ced2757a46f55b8c84358d6ab8de4faf57cba831c51e823654da7144b13a/propcache-0.3.2-cp39-cp39-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "21d8759141a9e00a681d35a1f160892a36fb6caa715ba0b832f7747da48fb6ea"}}, - {name = "propcache-0.3.2-cp39-cp39-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/bb/ec/d98ea8d5a4d8fe0e372033f5254eddf3254344c0c5dc6c49ab84349e4733/propcache-0.3.2-cp39-cp39-musllinux_1_2_s390x.whl",hashes = {sha256 = "2ca6d378f09adb13837614ad2754fa8afaee330254f404299611bce41a8438cb"}}, - {name = "propcache-0.3.2-cp39-cp39-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/56/84/b6d8a7ecf3f62d7dd09d9d10bbf89fad6837970ef868b35b5ffa0d24d9de/propcache-0.3.2-cp39-cp39-musllinux_1_2_x86_64.whl",hashes = {sha256 = "34a624af06c048946709f4278b4176470073deda88d91342665d95f7c6270fbe"}}, - {name = "propcache-0.3.2-cp39-cp39-win32.whl",url = "https://files.pythonhosted.org/packages/bf/32/889f4903ddfe4a9dc61da71ee58b763758cf2d608fe1decede06e6467f8d/propcache-0.3.2-cp39-cp39-win32.whl",hashes = {sha256 = "4ba3fef1c30f306b1c274ce0b8baaa2c3cdd91f645c48f06394068f37d3837a1"}}, - {name = "propcache-0.3.2-cp39-cp39-win_amd64.whl",url = "https://files.pythonhosted.org/packages/67/74/d666795fb9ba1dc139d30de64f3b6fd1ff9c9d3d96ccfdb992cd715ce5d2/propcache-0.3.2-cp39-cp39-win_amd64.whl",hashes = {sha256 = "7a2368eed65fc69a7a7a40b27f22e85e7627b74216f0846b04ba5c116e191ec9"}}, ] marker = "\"default\" in dependency_groups or \"dev\" in extras" [packages.tool.pdm] dependencies = [] +[[packages]] +name = "aiofiles" +version = "24.1.0" +requires-python = ">=3.8" +sdist = {name = "aiofiles-24.1.0.tar.gz", url = "https://files.pythonhosted.org/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hashes = {sha256 = "22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"}} +wheels = [ + {name = "aiofiles-24.1.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl",hashes = {sha256 = "b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5"}}, +] +marker = "\"default\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [] + [[packages]] name = "aiohappyeyeballs" version = "2.6.1" @@ -2203,6 +1839,21 @@ marker = "\"default\" in dependency_groups or \"dev\" in extras" [packages.tool.pdm] dependencies = [] +[[packages]] +name = "aiologic" +version = "0.14.0" +requires-python = ">=3.8" +sdist = {name = "aiologic-0.14.0.tar.gz", url = "https://files.pythonhosted.org/packages/7e/2d/e893dcfa041dab1d045abfc8898239747cde19881796640861609138d360/aiologic-0.14.0.tar.gz", hashes = {sha256 = "c87925fa2bfe9ae292859e1094eb8fb6d456c8202a16405b0a44134803c8a791"}} +wheels = [ + {name = "aiologic-0.14.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/4d/1f/f797b684fb4e11a5066ab464b460b5cfdbaedea9c4a3d0f0afc8e894ada0/aiologic-0.14.0-py3-none-any.whl",hashes = {sha256 = "cc59d39dc1d5e2575b4a6b5faf678b551fb0f910c7cb42e4c5f5689ffedcce78"}}, +] +marker = "\"default\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [ + "wrapt>=1.16.0", +] + [[packages]] name = "aiosignal" version = "1.4.0" @@ -2276,58 +1927,7 @@ wheels = [ {name = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/1c/80/9a0eb48b944050f94cc51ee1c413eb14a39543cc4f760ed12657a5a3c45a/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "290a172aae5a4c278c6da8a96222e6337744cd9c77313efe33d5670b9f65fc43"}}, {name = "frozenlist-1.7.0-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/f3/74/87601e0fb0369b7a2baf404ea921769c53b7ae00dee7dcfe5162c8c6dbf0/frozenlist-1.7.0-cp312-cp312-win32.whl",hashes = {sha256 = "426c7bc70e07cfebc178bc4c2bf2d861d720c4fff172181eeb4a4c41d4ca2ad3"}}, {name = "frozenlist-1.7.0-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/0b/15/c026e9a9fc17585a9d461f65d8593d281fedf55fbf7eb53f16c6df2392f9/frozenlist-1.7.0-cp312-cp312-win_amd64.whl",hashes = {sha256 = "563b72efe5da92e02eb68c59cb37205457c977aa7a449ed1b37e6939e5c47c6a"}}, - {name = "frozenlist-1.7.0-cp311-cp311-macosx_10_9_universal2.whl",url = "https://files.pythonhosted.org/packages/34/7e/803dde33760128acd393a27eb002f2020ddb8d99d30a44bfbaab31c5f08a/frozenlist-1.7.0-cp311-cp311-macosx_10_9_universal2.whl",hashes = {sha256 = "aa51e147a66b2d74de1e6e2cf5921890de6b0f4820b257465101d7f37b49fb5a"}}, - {name = "frozenlist-1.7.0-cp311-cp311-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/75/a9/9c2c5760b6ba45eae11334db454c189d43d34a4c0b489feb2175e5e64277/frozenlist-1.7.0-cp311-cp311-macosx_10_9_x86_64.whl",hashes = {sha256 = "9b35db7ce1cd71d36ba24f80f0c9e7cff73a28d7a74e91fe83e23d27c7828750"}}, - {name = "frozenlist-1.7.0-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/47/be/4038e2d869f8a2da165f35a6befb9158c259819be22eeaf9c9a8f6a87771/frozenlist-1.7.0-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "34a69a85e34ff37791e94542065c8416c1afbf820b68f720452f636d5fb990cd"}}, - {name = "frozenlist-1.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/79/26/85314b8a83187c76a37183ceed886381a5f992975786f883472fcb6dc5f2/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "4a646531fa8d82c87fe4bb2e596f23173caec9185bfbca5d583b4ccfb95183e2"}}, - {name = "frozenlist-1.7.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl",url = "https://files.pythonhosted.org/packages/1f/fd/e5b64f7d2c92a41639ffb2ad44a6a82f347787abc0c7df5f49057cf11770/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl",hashes = {sha256 = "79b2ffbba483f4ed36a0f236ccb85fbb16e670c9238313709638167670ba235f"}}, - {name = "frozenlist-1.7.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/20/fb/03395c0a43a5976af4bf7534759d214405fbbb4c114683f434dfdd3128ef/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "a26f205c9ca5829cbf82bb2a84b5c36f7184c4316617d7ef1b271a56720d6b30"}}, - {name = "frozenlist-1.7.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/d0/15/c01c8e1dffdac5d9803507d824f27aed2ba76b6ed0026fab4d9866e82f1f/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "bcacfad3185a623fa11ea0e0634aac7b691aa925d50a440f39b458e41c561d98"}}, - {name = "frozenlist-1.7.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/14/99/3f4c6fe882c1f5514b6848aa0a69b20cb5e5d8e8f51a339d48c0e9305ed0/frozenlist-1.7.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "72c1b0fe8fe451b34f12dce46445ddf14bd2a5bcad7e324987194dc8e3a74c86"}}, - {name = "frozenlist-1.7.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/4d/83/220a374bd7b2aeba9d0725130665afe11de347d95c3620b9b82cc2fcab97/frozenlist-1.7.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "61d1a5baeaac6c0798ff6edfaeaa00e0e412d49946c53fae8d4b8e8b3566c4ae"}}, - {name = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/03/3c/3e3390d75334a063181625343e8daab61b77e1b8214802cc4e8a1bb678fc/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "7edf5c043c062462f09b6820de9854bf28cc6cc5b6714b383149745e287181a8"}}, - {name = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_armv7l.whl",url = "https://files.pythonhosted.org/packages/23/1e/58232c19608b7a549d72d9903005e2d82488f12554a32de2d5fb59b9b1ba/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_armv7l.whl",hashes = {sha256 = "d50ac7627b3a1bd2dcef6f9da89a772694ec04d9a61b66cf87f7d9446b4a0c31"}}, - {name = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/c0/a4/e4a567e01702a88a74ce8a324691e62a629bf47d4f8607f24bf1c7216e7f/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_i686.whl",hashes = {sha256 = "ce48b2fece5aeb45265bb7a58259f45027db0abff478e3077e12b05b17fb9da7"}}, - {name = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/73/a6/63b3374f7d22268b41a9db73d68a8233afa30ed164c46107b33c4d18ecdd/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "fe2365ae915a1fafd982c146754e1de6ab3478def8a59c86e1f7242d794f97d5"}}, - {name = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/6d/eb/d18b3f6e64799a79673c4ba0b45e4cfbe49c240edfd03a68be20002eaeaa/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_s390x.whl",hashes = {sha256 = "45a6f2fdbd10e074e8814eb98b05292f27bad7d1883afbe009d96abdcf3bc898"}}, - {name = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/5a/f5/720f3812e3d06cd89a1d5db9ff6450088b8f5c449dae8ffb2971a44da506/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "21884e23cffabb157a9dd7e353779077bf5b8f9a58e9b262c6caad2ef5f80a56"}}, - {name = "frozenlist-1.7.0-cp311-cp311-win32.whl",url = "https://files.pythonhosted.org/packages/69/68/03efbf545e217d5db8446acfd4c447c15b7c8cf4dbd4a58403111df9322d/frozenlist-1.7.0-cp311-cp311-win32.whl",hashes = {sha256 = "284d233a8953d7b24f9159b8a3496fc1ddc00f4db99c324bd5fb5f22d8698ea7"}}, - {name = "frozenlist-1.7.0-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/58/17/fe61124c5c333ae87f09bb67186d65038834a47d974fc10a5fadb4cc5ae1/frozenlist-1.7.0-cp311-cp311-win_amd64.whl",hashes = {sha256 = "387cbfdcde2f2353f19c2f66bbb52406d06ed77519ac7ee21be0232147c2592d"}}, - {name = "frozenlist-1.7.0-cp310-cp310-macosx_10_9_universal2.whl",url = "https://files.pythonhosted.org/packages/af/36/0da0a49409f6b47cc2d060dc8c9040b897b5902a8a4e37d9bc1deb11f680/frozenlist-1.7.0-cp310-cp310-macosx_10_9_universal2.whl",hashes = {sha256 = "cc4df77d638aa2ed703b878dd093725b72a824c3c546c076e8fdf276f78ee84a"}}, - {name = "frozenlist-1.7.0-cp310-cp310-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/77/f0/77c11d13d39513b298e267b22eb6cb559c103d56f155aa9a49097221f0b6/frozenlist-1.7.0-cp310-cp310-macosx_10_9_x86_64.whl",hashes = {sha256 = "716a9973a2cc963160394f701964fe25012600f3d311f60c790400b00e568b61"}}, - {name = "frozenlist-1.7.0-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/37/12/9d07fa18971a44150593de56b2f2947c46604819976784bcf6ea0d5db43b/frozenlist-1.7.0-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "a0fd1bad056a3600047fb9462cff4c5322cebc59ebf5d0a3725e0ee78955001d"}}, - {name = "frozenlist-1.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/70/34/f73539227e06288fcd1f8a76853e755b2b48bca6747e99e283111c18bcd4/frozenlist-1.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "3789ebc19cb811163e70fe2bd354cea097254ce6e707ae42e56f45e31e96cb8e"}}, - {name = "frozenlist-1.7.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl",url = "https://files.pythonhosted.org/packages/fb/68/c1d9c2f4a6e438e14613bad0f2973567586610cc22dcb1e1241da71de9d3/frozenlist-1.7.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl",hashes = {sha256 = "af369aa35ee34f132fcfad5be45fbfcde0e3a5f6a1ec0712857f286b7d20cca9"}}, - {name = "frozenlist-1.7.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/b9/d0/98e8f9a515228d708344d7c6986752be3e3192d1795f748c24bcf154ad99/frozenlist-1.7.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "ac64b6478722eeb7a3313d494f8342ef3478dff539d17002f849101b212ef97c"}}, - {name = "frozenlist-1.7.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/79/df/8a11bcec5600557f40338407d3e5bea80376ed1c01a6c0910fcfdc4b8993/frozenlist-1.7.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "f89f65d85774f1797239693cef07ad4c97fdd0639544bad9ac4b869782eb1981"}}, - {name = "frozenlist-1.7.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/50/82/41cb97d9c9a5ff94438c63cc343eb7980dac4187eb625a51bdfdb7707314/frozenlist-1.7.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "1073557c941395fdfcfac13eb2456cb8aad89f9de27bae29fabca8e563b12615"}}, - {name = "frozenlist-1.7.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/13/47/f9179ee5ee4f55629e4f28c660b3fdf2775c8bfde8f9c53f2de2d93f52a9/frozenlist-1.7.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "1ed8d2fa095aae4bdc7fdd80351009a48d286635edffee66bf865e37a9125c50"}}, - {name = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/1a/52/df81e41ec6b953902c8b7e3a83bee48b195cb0e5ec2eabae5d8330c78038/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "24c34bea555fe42d9f928ba0a740c553088500377448febecaa82cc3e88aa1fa"}}, - {name = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_armv7l.whl",url = "https://files.pythonhosted.org/packages/84/17/30d6ea87fa95a9408245a948604b82c1a4b8b3e153cea596421a2aef2754/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_armv7l.whl",hashes = {sha256 = "69cac419ac6a6baad202c85aaf467b65ac860ac2e7f2ac1686dc40dbb52f6577"}}, - {name = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/8f/00/ecbeb51669e3c3df76cf2ddd66ae3e48345ec213a55e3887d216eb4fbab3/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_i686.whl",hashes = {sha256 = "960d67d0611f4c87da7e2ae2eacf7ea81a5be967861e0c63cf205215afbfac59"}}, - {name = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/1a/c0/c224ce0e0eb31cc57f67742071bb470ba8246623c1823a7530be0e76164c/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "41be2964bd4b15bf575e5daee5a5ce7ed3115320fb3c2b71fca05582ffa4dc9e"}}, - {name = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/55/3c/34cb694abf532f31f365106deebdeac9e45c19304d83cf7d51ebbb4ca4d1/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_s390x.whl",hashes = {sha256 = "46d84d49e00c9429238a7ce02dc0be8f6d7cd0cd405abd1bebdc991bf27c15bd"}}, - {name = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/98/c0/2052d8b6cecda2e70bd81299e3512fa332abb6dcd2969b9c80dfcdddbf75/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "15900082e886edb37480335d9d518cec978afc69ccbc30bd18610b7c1b22a718"}}, - {name = "frozenlist-1.7.0-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/c5/bf/7dcebae315436903b1d98ffb791a09d674c88480c158aa171958a3ac07f0/frozenlist-1.7.0-cp310-cp310-win32.whl",hashes = {sha256 = "400ddd24ab4e55014bba442d917203c73b2846391dd42ca5e38ff52bb18c3c5e"}}, - {name = "frozenlist-1.7.0-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/8f/5f/f69818f017fa9a3d24d1ae39763e29b7f60a59e46d5f91b9c6b21622f4cd/frozenlist-1.7.0-cp310-cp310-win_amd64.whl",hashes = {sha256 = "6eb93efb8101ef39d32d50bce242c84bcbddb4f7e9febfa7b524532a239b4464"}}, {name = "frozenlist-1.7.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/ee/45/b82e3c16be2182bff01179db177fe144d58b5dc787a7d4492c6ed8b9317f/frozenlist-1.7.0-py3-none-any.whl",hashes = {sha256 = "9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e"}}, - {name = "frozenlist-1.7.0-cp39-cp39-macosx_10_9_universal2.whl",url = "https://files.pythonhosted.org/packages/dd/b1/ee59496f51cd244039330015d60f13ce5a54a0f2bd8d79e4a4a375ab7469/frozenlist-1.7.0-cp39-cp39-macosx_10_9_universal2.whl",hashes = {sha256 = "cea3dbd15aea1341ea2de490574a4a37ca080b2ae24e4b4f4b51b9057b4c3630"}}, - {name = "frozenlist-1.7.0-cp39-cp39-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/75/e1/d518391ce36a6279b3fa5bc14327dde80bcb646bb50d059c6ca0756b8d05/frozenlist-1.7.0-cp39-cp39-macosx_10_9_x86_64.whl",hashes = {sha256 = "7d536ee086b23fecc36c2073c371572374ff50ef4db515e4e503925361c24f71"}}, - {name = "frozenlist-1.7.0-cp39-cp39-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/b7/8d/a0d04f28b6e821a9685c22e67b5fb798a5a7b68752f104bfbc2dccf080c4/frozenlist-1.7.0-cp39-cp39-macosx_11_0_arm64.whl",hashes = {sha256 = "dfcebf56f703cb2e346315431699f00db126d158455e513bd14089d992101e44"}}, - {name = "frozenlist-1.7.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/93/3a/a5334c0535c8b7c78eeabda1579179e44fe3d644e07118e59a2276dedaf1/frozenlist-1.7.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "974c5336e61d6e7eb1ea5b929cb645e882aadab0095c5a6974a111e6479f8878"}}, - {name = "frozenlist-1.7.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl",url = "https://files.pythonhosted.org/packages/0a/67/8258d971f519dc3f278c55069a775096cda6610a267b53f6248152b72b2f/frozenlist-1.7.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl",hashes = {sha256 = "c70db4a0ab5ab20878432c40563573229a7ed9241506181bba12f6b7d0dc41cb"}}, - {name = "frozenlist-1.7.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/fc/89/8225905bf889b97c6d935dd3aeb45668461e59d415cb019619383a8a7c3b/frozenlist-1.7.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "1137b78384eebaf70560a36b7b229f752fb64d463d38d1304939984d5cb887b6"}}, - {name = "frozenlist-1.7.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/54/6e/ef52375aa93d4bc510d061df06205fa6dcfd94cd631dd22956b09128f0d4/frozenlist-1.7.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "e793a9f01b3e8b5c0bc646fb59140ce0efcc580d22a3468d70766091beb81b35"}}, - {name = "frozenlist-1.7.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/ee/55/62c87d1a6547bfbcd645df10432c129100c5bd0fd92a384de6e3378b07c1/frozenlist-1.7.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "74739ba8e4e38221d2c5c03d90a7e542cb8ad681915f4ca8f68d04f810ee0a87"}}, - {name = "frozenlist-1.7.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/45/d2/263fea1f658b8ad648c7d94d18a87bca7e8c67bd6a1bbf5445b1bd5b158c/frozenlist-1.7.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "1e63344c4e929b1a01e29bc184bbb5fd82954869033765bfe8d65d09e336a677"}}, - {name = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/7b/22/7145e35d12fb368d92124f679bea87309495e2e9ddf14c6533990cb69218/frozenlist-1.7.0-cp39-cp39-musllinux_1_2_aarch64.whl",hashes = {sha256 = "2ea2a7369eb76de2217a842f22087913cdf75f63cf1307b9024ab82dfb525938"}}, - {name = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_armv7l.whl",url = "https://files.pythonhosted.org/packages/44/1e/7dae8c54301beb87bcafc6144b9a103bfd2c8f38078c7902984c9a0c4e5b/frozenlist-1.7.0-cp39-cp39-musllinux_1_2_armv7l.whl",hashes = {sha256 = "836b42f472a0e006e02499cef9352ce8097f33df43baaba3e0a28a964c26c7d2"}}, - {name = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/4b/1e/99c93e54aa382e949a98976a73b9b20c3aae6d9d893f31bbe4991f64e3a8/frozenlist-1.7.0-cp39-cp39-musllinux_1_2_i686.whl",hashes = {sha256 = "e22b9a99741294b2571667c07d9f8cceec07cb92aae5ccda39ea1b6052ed4319"}}, - {name = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/5e/9c/ca5105fa7fb5abdfa8837581be790447ae051da75d32f25c8f81082ffc45/frozenlist-1.7.0-cp39-cp39-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "9a19e85cc503d958abe5218953df722748d87172f71b73cf3c9257a91b999890"}}, - {name = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/8d/4d/e99014756093b4ddbb67fb8f0df11fe7a415760d69ace98e2ac6d5d43402/frozenlist-1.7.0-cp39-cp39-musllinux_1_2_s390x.whl",hashes = {sha256 = "f22dac33bb3ee8fe3e013aa7b91dc12f60d61d05b7fe32191ffa84c3aafe77bd"}}, - {name = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/8b/72/a19a40bcdaa28a51add2aaa3a1a294ec357f36f27bd836a012e070c5e8a5/frozenlist-1.7.0-cp39-cp39-musllinux_1_2_x86_64.whl",hashes = {sha256 = "9ccec739a99e4ccf664ea0775149f2749b8a6418eb5b8384b4dc0a7d15d304cb"}}, - {name = "frozenlist-1.7.0-cp39-cp39-win32.whl",url = "https://files.pythonhosted.org/packages/08/49/0042469993e023a758af81db68c76907cd29e847d772334d4d201cbe9a42/frozenlist-1.7.0-cp39-cp39-win32.whl",hashes = {sha256 = "b3950f11058310008a87757f3eee16a8e1ca97979833239439586857bc25482e"}}, - {name = "frozenlist-1.7.0-cp39-cp39-win_amd64.whl",url = "https://files.pythonhosted.org/packages/5a/45/827d86ee475c877f5f766fbc23fb6acb6fada9e52f1c9720e2ba3eae32da/frozenlist-1.7.0-cp39-cp39-win_amd64.whl",hashes = {sha256 = "43a82fce6769c70f2f5a06248b614a7d268080a9d20f7457ef10ecee5af82b63"}}, ] marker = "\"default\" in dependency_groups or \"dev\" in extras" @@ -2362,6 +1962,19 @@ marker = "\"default\" in dependency_groups or \"dev\" in extras" [packages.tool.pdm] dependencies = [] +[[packages]] +name = "audioread" +version = "3.0.1" +requires-python = ">=3.6" +sdist = {name = "audioread-3.0.1.tar.gz", url = "https://files.pythonhosted.org/packages/db/d2/87016ca9f083acadffb2d8da59bfa3253e4da7eeb9f71fb8e7708dc97ecd/audioread-3.0.1.tar.gz", hashes = {sha256 = "ac5460a5498c48bdf2e8e767402583a4dcd13f4414d286f42ce4379e8b35066d"}} +wheels = [ + {name = "audioread-3.0.1-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/57/8d/30aa32745af16af0a9a650115fbe81bde7c610ed5c21b381fca0196f3a7f/audioread-3.0.1-py3-none-any.whl",hashes = {sha256 = "4cdce70b8adc0da0a3c9e0d85fb10b3ace30fbdf8d1670fd443929b61d117c33"}}, +] +marker = "\"default\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [] + [[packages]] name = "babel" version = "2.17.0" @@ -2431,87 +2044,76 @@ dependencies = [] [[packages]] name = "coverage" -version = "7.10.6" +version = "7.10.7" requires-python = ">=3.9" -sdist = {name = "coverage-7.10.6.tar.gz", url = "https://files.pythonhosted.org/packages/14/70/025b179c993f019105b79575ac6edb5e084fb0f0e63f15cdebef4e454fb5/coverage-7.10.6.tar.gz", hashes = {sha256 = "f644a3ae5933a552a29dbb9aa2f90c677a875f80ebea028e5a52a4f429044b90"}} -wheels = [ - {name = "coverage-7.10.6-cp314-cp314-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/d3/aa/76cf0b5ec00619ef208da4689281d48b57f2c7fde883d14bf9441b74d59f/coverage-7.10.6-cp314-cp314-macosx_10_13_x86_64.whl",hashes = {sha256 = "6008a021907be8c4c02f37cdc3ffb258493bdebfeaf9a839f9e71dfdc47b018e"}}, - {name = "coverage-7.10.6-cp314-cp314-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/65/91/8e41b8c7c505d398d7730206f3cbb4a875a35ca1041efc518051bfce0f6b/coverage-7.10.6-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "5e75e37f23eb144e78940b40395b42f2321951206a4f50e23cfd6e8a198d3ceb"}}, - {name = "coverage-7.10.6-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/87/7f/f718e732a423d442e6616580a951b8d1ec3575ea48bcd0e2228386805e79/coverage-7.10.6-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "0f7cb359a448e043c576f0da00aa8bfd796a01b06aa610ca453d4dde09cc1034"}}, - {name = "coverage-7.10.6-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/e6/52/c1106120e6d801ac03e12b5285e971e758e925b6f82ee9b86db3aa10045d/coverage-7.10.6-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "c68018e4fc4e14b5668f1353b41ccf4bc83ba355f0e1b3836861c6f042d89ac1"}}, - {name = "coverage-7.10.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/3d/ec/3a8645b1bb40e36acde9c0609f08942852a4af91a937fe2c129a38f2d3f5/coverage-7.10.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "cd4b2b0707fc55afa160cd5fc33b27ccbf75ca11d81f4ec9863d5793fc6df56a"}}, - {name = "coverage-7.10.6-cp314-cp314-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/a1/70/09ecb68eeb1155b28a1d16525fd3a9b65fbe75337311a99830df935d62b6/coverage-7.10.6-cp314-cp314-musllinux_1_2_aarch64.whl",hashes = {sha256 = "4cec13817a651f8804a86e4f79d815b3b28472c910e099e4d5a0e8a3b6a1d4cb"}}, - {name = "coverage-7.10.6-cp314-cp314-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/c6/80/47df374b893fa812e953b5bc93dcb1427a7b3d7a1a7d2db33043d17f74b9/coverage-7.10.6-cp314-cp314-musllinux_1_2_i686.whl",hashes = {sha256 = "f2a6a8e06bbda06f78739f40bfb56c45d14eb8249d0f0ea6d4b3d48e1f7c695d"}}, - {name = "coverage-7.10.6-cp314-cp314-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/4a/65/9f98640979ecee1b0d1a7164b589de720ddf8100d1747d9bbdb84be0c0fb/coverage-7.10.6-cp314-cp314-musllinux_1_2_x86_64.whl",hashes = {sha256 = "081b98395ced0d9bcf60ada7661a0b75f36b78b9d7e39ea0790bb4ed8da14747"}}, - {name = "coverage-7.10.6-cp314-cp314-win32.whl",url = "https://files.pythonhosted.org/packages/1f/55/eeb6603371e6629037f47bd25bef300387257ed53a3c5fdb159b7ac8c651/coverage-7.10.6-cp314-cp314-win32.whl",hashes = {sha256 = "6937347c5d7d069ee776b2bf4e1212f912a9f1f141a429c475e6089462fcecc5"}}, - {name = "coverage-7.10.6-cp314-cp314-win_amd64.whl",url = "https://files.pythonhosted.org/packages/15/d1/a0912b7611bc35412e919a2cd59ae98e7ea3b475e562668040a43fb27897/coverage-7.10.6-cp314-cp314-win_amd64.whl",hashes = {sha256 = "adec1d980fa07e60b6ef865f9e5410ba760e4e1d26f60f7e5772c73b9a5b0713"}}, - {name = "coverage-7.10.6-cp314-cp314-win_arm64.whl",url = "https://files.pythonhosted.org/packages/ef/2d/11880bb8ef80a45338e0b3e0725e4c2d73ffbb4822c29d987078224fd6a5/coverage-7.10.6-cp314-cp314-win_arm64.whl",hashes = {sha256 = "a80f7aef9535442bdcf562e5a0d5a5538ce8abe6bb209cfbf170c462ac2c2a32"}}, - {name = "coverage-7.10.6-cp314-cp314t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/83/c0/1f00caad775c03a700146f55536ecd097a881ff08d310a58b353a1421be0/coverage-7.10.6-cp314-cp314t-macosx_10_13_x86_64.whl",hashes = {sha256 = "0de434f4fbbe5af4fa7989521c655c8c779afb61c53ab561b64dcee6149e4c65"}}, - {name = "coverage-7.10.6-cp314-cp314t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/a9/c4/b1c5d2bd7cc412cbeb035e257fd06ed4e3e139ac871d16a07434e145d18d/coverage-7.10.6-cp314-cp314t-macosx_11_0_arm64.whl",hashes = {sha256 = "6e31b8155150c57e5ac43ccd289d079eb3f825187d7c66e755a055d2c85794c6"}}, - {name = "coverage-7.10.6-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/3f/07/4468d37c94724bf6ec354e4ec2f205fda194343e3e85fd2e59cec57e6a54/coverage-7.10.6-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "98cede73eb83c31e2118ae8d379c12e3e42736903a8afcca92a7218e1f2903b0"}}, - {name = "coverage-7.10.6-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/82/d8/f8fb351be5fee31690cd8da768fd62f1cfab33c31d9f7baba6cd8960f6b8/coverage-7.10.6-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "f863c08f4ff6b64fa8045b1e3da480f5374779ef187f07b82e0538c68cb4ff8e"}}, - {name = "coverage-7.10.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/e8/70/65d4d7cfc75c5c6eb2fed3ee5cdf420fd8ae09c4808723a89a81d5b1b9c3/coverage-7.10.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "2b38261034fda87be356f2c3f42221fdb4171c3ce7658066ae449241485390d5"}}, - {name = "coverage-7.10.6-cp314-cp314t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/98/3c/069df106d19024324cde10e4ec379fe2fb978017d25e97ebee23002fbadf/coverage-7.10.6-cp314-cp314t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "0e93b1476b79eae849dc3872faeb0bf7948fd9ea34869590bc16a2a00b9c82a7"}}, - {name = "coverage-7.10.6-cp314-cp314t-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/fc/8a/2974d53904080c5dc91af798b3a54a4ccb99a45595cc0dcec6eb9616a57d/coverage-7.10.6-cp314-cp314t-musllinux_1_2_i686.whl",hashes = {sha256 = "ff8a991f70f4c0cf53088abf1e3886edcc87d53004c7bb94e78650b4d3dac3b5"}}, - {name = "coverage-7.10.6-cp314-cp314t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/30/38/9616a6b49c686394b318974d7f6e08f38b8af2270ce7488e879888d1e5db/coverage-7.10.6-cp314-cp314t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "ac765b026c9f33044419cbba1da913cfb82cca1b60598ac1c7a5ed6aac4621a0"}}, - {name = "coverage-7.10.6-cp314-cp314t-win32.whl",url = "https://files.pythonhosted.org/packages/76/16/3ed2d6312b371a8cf804abf4e14895b70e4c3491c6e53536d63fd0958a8d/coverage-7.10.6-cp314-cp314t-win32.whl",hashes = {sha256 = "441c357d55f4936875636ef2cfb3bee36e466dcf50df9afbd398ce79dba1ebb7"}}, - {name = "coverage-7.10.6-cp314-cp314t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/d5/e5/d38d0cb830abede2adb8b147770d2a3d0e7fecc7228245b9b1ae6c24930a/coverage-7.10.6-cp314-cp314t-win_amd64.whl",hashes = {sha256 = "073711de3181b2e204e4870ac83a7c4853115b42e9cd4d145f2231e12d670930"}}, - {name = "coverage-7.10.6-cp314-cp314t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/f4/51/e48e550f6279349895b0ffcd6d2a690e3131ba3a7f4eafccc141966d4dea/coverage-7.10.6-cp314-cp314t-win_arm64.whl",hashes = {sha256 = "137921f2bac5559334ba66122b753db6dc5d1cf01eb7b64eb412bb0d064ef35b"}}, - {name = "coverage-7.10.6-cp313-cp313-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/bd/e7/917e5953ea29a28c1057729c1d5af9084ab6d9c66217523fd0e10f14d8f6/coverage-7.10.6-cp313-cp313-macosx_10_13_x86_64.whl",hashes = {sha256 = "ffea0575345e9ee0144dfe5701aa17f3ba546f8c3bb48db62ae101afb740e7d6"}}, - {name = "coverage-7.10.6-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/eb/86/2e161b93a4f11d0ea93f9bebb6a53f113d5d6e416d7561ca41bb0a29996b/coverage-7.10.6-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "95d91d7317cde40a1c249d6b7382750b7e6d86fad9d8eaf4fa3f8f44cf171e80"}}, - {name = "coverage-7.10.6-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/0e/66/d03348fdd8df262b3a7fb4ee5727e6e4936e39e2f3a842e803196946f200/coverage-7.10.6-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "3e23dd5408fe71a356b41baa82892772a4cefcf758f2ca3383d2aa39e1b7a003"}}, - {name = "coverage-7.10.6-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/73/dd/508420fb47d09d904d962f123221bc249f64b5e56aa93d5f5f7603be475f/coverage-7.10.6-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "0f3f56e4cb573755e96a16501a98bf211f100463d70275759e73f3cbc00d4f27"}}, - {name = "coverage-7.10.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/e9/1f/9020135734184f439da85c70ea78194c2730e56c2d18aee6e8ff1719d50d/coverage-7.10.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "db4a1d897bbbe7339946ffa2fe60c10cc81c43fab8b062d3fcb84188688174a4"}}, - {name = "coverage-7.10.6-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/a4/a4/3d228f3942bb5a2051fde28c136eea23a761177dc4ff4ef54533164ce255/coverage-7.10.6-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "d8fd7879082953c156d5b13c74aa6cca37f6a6f4747b39538504c3f9c63d043d"}}, - {name = "coverage-7.10.6-cp313-cp313-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/36/e3/293dce8cdb9a83de971637afc59b7190faad60603b40e32635cbd15fbf61/coverage-7.10.6-cp313-cp313-musllinux_1_2_i686.whl",hashes = {sha256 = "28395ca3f71cd103b8c116333fa9db867f3a3e1ad6a084aa3725ae002b6583bc"}}, - {name = "coverage-7.10.6-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/90/26/64eecfa214e80dd1d101e420cab2901827de0e49631d666543d0e53cf597/coverage-7.10.6-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "61c950fc33d29c91b9e18540e1aed7d9f6787cc870a3e4032493bbbe641d12fc"}}, - {name = "coverage-7.10.6-cp313-cp313-win32.whl",url = "https://files.pythonhosted.org/packages/3e/70/bd80588338f65ea5b0d97e424b820fb4068b9cfb9597fbd91963086e004b/coverage-7.10.6-cp313-cp313-win32.whl",hashes = {sha256 = "160c00a5e6b6bdf4e5984b0ef21fc860bc94416c41b7df4d63f536d17c38902e"}}, - {name = "coverage-7.10.6-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/a7/14/0b831122305abcc1060c008f6c97bbdc0a913ab47d65070a01dc50293c2b/coverage-7.10.6-cp313-cp313-win_amd64.whl",hashes = {sha256 = "628055297f3e2aa181464c3808402887643405573eb3d9de060d81531fa79d32"}}, - {name = "coverage-7.10.6-cp313-cp313-win_arm64.whl",url = "https://files.pythonhosted.org/packages/83/c6/81a83778c1f83f1a4a168ed6673eeedc205afb562d8500175292ca64b94e/coverage-7.10.6-cp313-cp313-win_arm64.whl",hashes = {sha256 = "df4ec1f8540b0bcbe26ca7dd0f541847cc8a108b35596f9f91f59f0c060bfdd2"}}, - {name = "coverage-7.10.6-cp313-cp313t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/d7/1c/ccccf4bf116f9517275fa85047495515add43e41dfe8e0bef6e333c6b344/coverage-7.10.6-cp313-cp313t-macosx_10_13_x86_64.whl",hashes = {sha256 = "c9a8b7a34a4de3ed987f636f71881cd3b8339f61118b1aa311fbda12741bff0b"}}, - {name = "coverage-7.10.6-cp313-cp313t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/92/97/8a3ceff833d27c7492af4f39d5da6761e9ff624831db9e9f25b3886ddbca/coverage-7.10.6-cp313-cp313t-macosx_11_0_arm64.whl",hashes = {sha256 = "8dd5af36092430c2b075cee966719898f2ae87b636cefb85a653f1d0ba5d5393"}}, - {name = "coverage-7.10.6-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/92/d8/50b4a32580cf41ff0423777a2791aaf3269ab60c840b62009aec12d3970d/coverage-7.10.6-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "b0353b0f0850d49ada66fdd7d0c7cdb0f86b900bb9e367024fd14a60cecc1e27"}}, - {name = "coverage-7.10.6-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/7e/7e/6a7df5a6fb440a0179d94a348eb6616ed4745e7df26bf2a02bc4db72c421/coverage-7.10.6-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "d6b9ae13d5d3e8aeca9ca94198aa7b3ebbc5acfada557d724f2a1f03d2c0b0df"}}, - {name = "coverage-7.10.6-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/3a/4c/a270a414f4ed5d196b9d3d67922968e768cd971d1b251e1b4f75e9362f75/coverage-7.10.6-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "675824a363cc05781b1527b39dc2587b8984965834a748177ee3c37b64ffeafb"}}, - {name = "coverage-7.10.6-cp313-cp313t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/9c/8b/3210d663d594926c12f373c5370bf1e7c5c3a427519a8afa65b561b9a55c/coverage-7.10.6-cp313-cp313t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "692d70ea725f471a547c305f0d0fc6a73480c62fb0da726370c088ab21aed282"}}, - {name = "coverage-7.10.6-cp313-cp313t-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/72/d0/e1961eff67e9e1dba3fc5eb7a4caf726b35a5b03776892da8d79ec895775/coverage-7.10.6-cp313-cp313t-musllinux_1_2_i686.whl",hashes = {sha256 = "851430a9a361c7a8484a36126d1d0ff8d529d97385eacc8dfdc9bfc8c2d2cbe4"}}, - {name = "coverage-7.10.6-cp313-cp313t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/3a/06/d6478d152cd189b33eac691cba27a40704990ba95de49771285f34a5861e/coverage-7.10.6-cp313-cp313t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "d9369a23186d189b2fc95cc08b8160ba242057e887d766864f7adf3c46b2df21"}}, - {name = "coverage-7.10.6-cp313-cp313t-win32.whl",url = "https://files.pythonhosted.org/packages/ed/73/737440247c914a332f0b47f7598535b29965bf305e19bbc22d4c39615d2b/coverage-7.10.6-cp313-cp313t-win32.whl",hashes = {sha256 = "92be86fcb125e9bda0da7806afd29a3fd33fdf58fba5d60318399adf40bf37d0"}}, - {name = "coverage-7.10.6-cp313-cp313t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/bd/76/b92d3214740f2357ef4a27c75a526eb6c28f79c402e9f20a922c295c05e2/coverage-7.10.6-cp313-cp313t-win_amd64.whl",hashes = {sha256 = "6b3039e2ca459a70c79523d39347d83b73f2f06af5624905eba7ec34d64d80b5"}}, - {name = "coverage-7.10.6-cp313-cp313t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/fc/8e/6dcb29c599c8a1f654ec6cb68d76644fe635513af16e932d2d4ad1e5ac6e/coverage-7.10.6-cp313-cp313t-win_arm64.whl",hashes = {sha256 = "3fb99d0786fe17b228eab663d16bee2288e8724d26a199c29325aac4b0319b9b"}}, - {name = "coverage-7.10.6-cp312-cp312-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/26/06/263f3305c97ad78aab066d116b52250dd316e74fcc20c197b61e07eb391a/coverage-7.10.6-cp312-cp312-macosx_10_13_x86_64.whl",hashes = {sha256 = "5b2dd6059938063a2c9fee1af729d4f2af28fd1a545e9b7652861f0d752ebcea"}}, - {name = "coverage-7.10.6-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/e9/60/1e1ded9a4fe80d843d7d53b3e395c1db3ff32d6c301e501f393b2e6c1c1f/coverage-7.10.6-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "388d80e56191bf846c485c14ae2bc8898aa3124d9d35903fef7d907780477634"}}, - {name = "coverage-7.10.6-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/b8/25/52136173c14e26dfed8b106ed725811bb53c30b896d04d28d74cb64318b3/coverage-7.10.6-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "90cb5b1a4670662719591aa92d0095bb41714970c0b065b02a2610172dbf0af6"}}, - {name = "coverage-7.10.6-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/cb/1d/ae25a7dc58fcce8b172d42ffe5313fc267afe61c97fa872b80ee72d9515a/coverage-7.10.6-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "961834e2f2b863a0e14260a9a273aff07ff7818ab6e66d2addf5628590c628f9"}}, - {name = "coverage-7.10.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/f5/7a/1f561d47743710fe996957ed7c124b421320f150f1d38523d8d9102d3e2a/coverage-7.10.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "bf9a19f5012dab774628491659646335b1928cfc931bf8d97b0d5918dd58033c"}}, - {name = "coverage-7.10.6-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/6c/ad/8b97cd5d28aecdfde792dcbf646bac141167a5cacae2cd775998b45fabb5/coverage-7.10.6-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "99c4283e2a0e147b9c9cc6bc9c96124de9419d6044837e9799763a0e29a7321a"}}, - {name = "coverage-7.10.6-cp312-cp312-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/33/6a/95c32b558d9a61858ff9d79580d3877df3eb5bc9eed0941b1f187c89e143/coverage-7.10.6-cp312-cp312-musllinux_1_2_i686.whl",hashes = {sha256 = "282b1b20f45df57cc508c1e033403f02283adfb67d4c9c35a90281d81e5c52c5"}}, - {name = "coverage-7.10.6-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/0d/9c/8ce95dee640a38e760d5b747c10913e7a06554704d60b41e73fdea6a1ffd/coverage-7.10.6-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "8cdbe264f11afd69841bd8c0d83ca10b5b32853263ee62e6ac6a0ab63895f972"}}, - {name = "coverage-7.10.6-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/04/12/7a55b0bdde78a98e2eb2356771fd2dcddb96579e8342bb52aa5bc52e96f0/coverage-7.10.6-cp312-cp312-win32.whl",hashes = {sha256 = "a517feaf3a0a3eca1ee985d8373135cfdedfbba3882a5eab4362bda7c7cf518d"}}, - {name = "coverage-7.10.6-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/36/4a/32b185b8b8e327802c9efce3d3108d2fe2d9d31f153a0f7ecfd59c773705/coverage-7.10.6-cp312-cp312-win_amd64.whl",hashes = {sha256 = "856986eadf41f52b214176d894a7de05331117f6035a28ac0016c0f63d887629"}}, - {name = "coverage-7.10.6-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/08/3a/d5d8dc703e4998038c3099eaf77adddb00536a3cec08c8dcd556a36a3eb4/coverage-7.10.6-cp312-cp312-win_arm64.whl",hashes = {sha256 = "acf36b8268785aad739443fa2780c16260ee3fa09d12b3a70f772ef100939d80"}}, - {name = "coverage-7.10.6-cp311-cp311-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/d4/16/2bea27e212c4980753d6d563a0803c150edeaaddb0771a50d2afc410a261/coverage-7.10.6-cp311-cp311-macosx_10_9_x86_64.whl",hashes = {sha256 = "c706db3cabb7ceef779de68270150665e710b46d56372455cd741184f3868d8f"}}, - {name = "coverage-7.10.6-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/2a/51/e7159e068831ab37e31aac0969d47b8c5ee25b7d307b51e310ec34869315/coverage-7.10.6-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "8e0c38dc289e0508ef68ec95834cb5d2e96fdbe792eaccaa1bccac3966bbadcc"}}, - {name = "coverage-7.10.6-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/e7/c0/246ccbea53d6099325d25cd208df94ea435cd55f0db38099dd721efc7a1f/coverage-7.10.6-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "752a3005a1ded28f2f3a6e8787e24f28d6abe176ca64677bcd8d53d6fe2ec08a"}}, - {name = "coverage-7.10.6-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/7d/fb/7435ef8ab9b2594a6e3f58505cc30e98ae8b33265d844007737946c59389/coverage-7.10.6-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "689920ecfd60f992cafca4f5477d55720466ad2c7fa29bb56ac8d44a1ac2b47a"}}, - {name = "coverage-7.10.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/51/f8/d9d64e8da7bcddb094d511154824038833c81e3a039020a9d6539bf303e9/coverage-7.10.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "ec98435796d2624d6905820a42f82149ee9fc4f2d45c2c5bc5a44481cc50db62"}}, - {name = "coverage-7.10.6-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/43/28/c43ba0ef19f446d6463c751315140d8f2a521e04c3e79e5c5fe211bfa430/coverage-7.10.6-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "b37201ce4a458c7a758ecc4efa92fa8ed783c66e0fa3c42ae19fc454a0792153"}}, - {name = "coverage-7.10.6-cp311-cp311-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/79/3e/53635bd0b72beaacf265784508a0b386defc9ab7fad99ff95f79ce9db555/coverage-7.10.6-cp311-cp311-musllinux_1_2_i686.whl",hashes = {sha256 = "2904271c80898663c810a6b067920a61dd8d38341244a3605bd31ab55250dad5"}}, - {name = "coverage-7.10.6-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/4c/55/0964aa87126624e8c159e32b0bc4e84edef78c89a1a4b924d28dd8265625/coverage-7.10.6-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "5aea98383463d6e1fa4e95416d8de66f2d0cb588774ee20ae1b28df826bcb619"}}, - {name = "coverage-7.10.6-cp311-cp311-win32.whl",url = "https://files.pythonhosted.org/packages/eb/ab/6cfa9dc518c6c8e14a691c54e53a9433ba67336c760607e299bfcf520cb1/coverage-7.10.6-cp311-cp311-win32.whl",hashes = {sha256 = "e3fb1fa01d3598002777dd259c0c2e6d9d5e10e7222976fc8e03992f972a2cba"}}, - {name = "coverage-7.10.6-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/5b/18/99b25346690cbc55922e7cfef06d755d4abee803ef335baff0014268eff4/coverage-7.10.6-cp311-cp311-win_amd64.whl",hashes = {sha256 = "f35ed9d945bece26553d5b4c8630453169672bea0050a564456eb88bdffd927e"}}, - {name = "coverage-7.10.6-cp311-cp311-win_arm64.whl",url = "https://files.pythonhosted.org/packages/d8/ed/81d86648a07ccb124a5cf1f1a7788712b8d7216b593562683cd5c9b0d2c1/coverage-7.10.6-cp311-cp311-win_arm64.whl",hashes = {sha256 = "99e1a305c7765631d74b98bf7dbf54eeea931f975e80f115437d23848ee8c27c"}}, - {name = "coverage-7.10.6-cp310-cp310-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/a8/1d/2e64b43d978b5bd184e0756a41415597dfef30fcbd90b747474bd749d45f/coverage-7.10.6-cp310-cp310-macosx_10_9_x86_64.whl",hashes = {sha256 = "70e7bfbd57126b5554aa482691145f798d7df77489a177a6bef80de78860a356"}}, - {name = "coverage-7.10.6-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/23/62/b1e0f513417c02cc10ef735c3ee5186df55f190f70498b3702d516aad06f/coverage-7.10.6-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "e41be6f0f19da64af13403e52f2dec38bbc2937af54df8ecef10850ff8d35301"}}, - {name = "coverage-7.10.6-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/e7/16/b800640b7a43e7c538429e4d7223e0a94fd72453a1a048f70bf766f12e96/coverage-7.10.6-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "c61fc91ab80b23f5fddbee342d19662f3d3328173229caded831aa0bd7595460"}}, - {name = "coverage-7.10.6-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/fb/6f/5e03631c3305cad187eaf76af0b559fff88af9a0b0c180d006fb02413d7a/coverage-7.10.6-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "10356fdd33a7cc06e8051413140bbdc6f972137508a3572e3f59f805cd2832fd"}}, - {name = "coverage-7.10.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/eb/a1/f30ea0fb400b080730125b490771ec62b3375789f90af0bb68bfb8a921d7/coverage-7.10.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "80b1695cf7c5ebe7b44bf2521221b9bb8cdf69b1f24231149a7e3eb1ae5fa2fb"}}, - {name = "coverage-7.10.6-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/02/8e/cfa8fee8e8ef9a6bb76c7bef039f3302f44e615d2194161a21d3d83ac2e9/coverage-7.10.6-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "2e4c33e6378b9d52d3454bd08847a8651f4ed23ddbb4a0520227bd346382bbc6"}}, - {name = "coverage-7.10.6-cp310-cp310-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/93/a9/51be09b75c55c4f6c16d8d73a6a1d46ad764acca0eab48fa2ffaef5958fe/coverage-7.10.6-cp310-cp310-musllinux_1_2_i686.whl",hashes = {sha256 = "c8a3ec16e34ef980a46f60dc6ad86ec60f763c3f2fa0db6d261e6e754f72e945"}}, - {name = "coverage-7.10.6-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/e9/a6/ba188b376529ce36483b2d585ca7bdac64aacbe5aa10da5978029a9c94db/coverage-7.10.6-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "7d79dabc0a56f5af990cc6da9ad1e40766e82773c075f09cc571e2076fef882e"}}, - {name = "coverage-7.10.6-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/d0/4c/37ed872374a21813e0d3215256180c9a382c3f5ced6f2e5da0102fc2fd3e/coverage-7.10.6-cp310-cp310-win32.whl",hashes = {sha256 = "86b9b59f2b16e981906e9d6383eb6446d5b46c278460ae2c36487667717eccf1"}}, - {name = "coverage-7.10.6-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/8e/36/9311352fdc551dec5b973b61f4e453227ce482985a9368305880af4f85dd/coverage-7.10.6-cp310-cp310-win_amd64.whl",hashes = {sha256 = "e132b9152749bd33534e5bd8565c7576f135f157b4029b975e15ee184325f528"}}, - {name = "coverage-7.10.6-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/44/0c/50db5379b615854b5cf89146f8f5bd1d5a9693d7f3a987e269693521c404/coverage-7.10.6-py3-none-any.whl",hashes = {sha256 = "92c4ecf6bf11b2e85fd4d8204814dc26e6a19f0c9d938c207c5cb0eadfcabbe3"}}, +sdist = {name = "coverage-7.10.7.tar.gz", url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hashes = {sha256 = "f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239"}} +wheels = [ + {name = "coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/23/9c/5844ab4ca6a4dd97a1850e030a15ec7d292b5c5cb93082979225126e35dd/coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl",hashes = {sha256 = "b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520"}}, + {name = "coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/f0/89/673f6514b0961d1f0e20ddc242e9342f6da21eaba3489901b565c0689f34/coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32"}}, + {name = "coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/05/e8/261cae479e85232828fb17ad536765c88dd818c8470aca690b0ac6feeaa3/coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f"}}, + {name = "coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/82/62/14ed6546d0207e6eda876434e3e8475a3e9adbe32110ce896c9e0c06bb9a/coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a"}}, + {name = "coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/ff/49/07f00db9ac6478e4358165a08fb41b469a1b053212e8a00cb02f0d27a05f/coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360"}}, + {name = "coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/a2/59/c5201c62dbf165dfbc91460f6dbbaa85a8b82cfa6131ac45d6c1bfb52deb/coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69"}}, + {name = "coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/07/ae/5920097195291a51fb00b3a70b9bbd2edbfe3c84876a1762bd1ef1565ebc/coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl",hashes = {sha256 = "cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14"}}, + {name = "coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/b9/3c/a815dde77a2981f5743a60b63df31cb322c944843e57dbd579326625a413/coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl",hashes = {sha256 = "39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe"}}, + {name = "coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/aa/99/f5cdd8421ea656abefb6c0ce92556709db2265c41e8f9fc6c8ae0f7824c9/coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl",hashes = {sha256 = "925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e"}}, + {name = "coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/c3/7a/e9a2da6a1fc5d007dd51fca083a663ab930a8c4d149c087732a5dbaa0029/coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl",hashes = {sha256 = "2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd"}}, + {name = "coverage-7.10.7-cp314-cp314-win32.whl",url = "https://files.pythonhosted.org/packages/ef/5b/0b5799aa30380a949005a353715095d6d1da81927d6dbed5def2200a4e25/coverage-7.10.7-cp314-cp314-win32.whl",hashes = {sha256 = "b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2"}}, + {name = "coverage-7.10.7-cp314-cp314-win_amd64.whl",url = "https://files.pythonhosted.org/packages/da/b0/e802fbb6eb746de006490abc9bb554b708918b6774b722bb3a0e6aa1b7de/coverage-7.10.7-cp314-cp314-win_amd64.whl",hashes = {sha256 = "1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681"}}, + {name = "coverage-7.10.7-cp314-cp314-win_arm64.whl",url = "https://files.pythonhosted.org/packages/9e/e8/71d0c8e374e31f39e3389bb0bd19e527d46f00ea8571ec7ec8fd261d8b44/coverage-7.10.7-cp314-cp314-win_arm64.whl",hashes = {sha256 = "097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880"}}, + {name = "coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/62/09/9a5608d319fa3eba7a2019addeacb8c746fb50872b57a724c9f79f146969/coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl",hashes = {sha256 = "a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63"}}, + {name = "coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/f5/6f/f58d46f33db9f2e3647b2d0764704548c184e6f5e014bef528b7f979ef84/coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl",hashes = {sha256 = "9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2"}}, + {name = "coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/74/5c/183ffc817ba68e0b443b8c934c8795553eb0c14573813415bd59941ee165/coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d"}}, + {name = "coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/0f/48/71a8abe9c1ad7e97548835e3cc1adbf361e743e9d60310c5f75c9e7bf847/coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0"}}, + {name = "coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/84/fd/193a8fb132acfc0a901f72020e54be5e48021e1575bb327d8ee1097a28fd/coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699"}}, + {name = "coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/b1/8f/74ecc30607dd95ad50e3034221113ccb1c6d4e8085cc761134782995daae/coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9"}}, + {name = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/0f/55/79ff53a769f20d71b07023ea115c9167c0bb56f281320520cf64c5298a96/coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f"}}, + {name = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/88/e2/dac66c140009b61ac3fc13af673a574b00c16efdf04f9b5c740703e953c0/coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl",hashes = {sha256 = "0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1"}}, + {name = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/a2/f1/f48f645e3f33bb9ca8a496bc4a9671b52f2f353146233ebd7c1df6160440/coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl",hashes = {sha256 = "a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0"}}, + {name = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/bb/3b/8442618972c51a7affeead957995cfa8323c0c9bcf8fa5a027421f720ff4/coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399"}}, + {name = "coverage-7.10.7-cp314-cp314t-win32.whl",url = "https://files.pythonhosted.org/packages/b2/dc/101f3fa3a45146db0cb03f5b4376e24c0aac818309da23e2de0c75295a91/coverage-7.10.7-cp314-cp314t-win32.whl",hashes = {sha256 = "67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235"}}, + {name = "coverage-7.10.7-cp314-cp314t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/4c/a1/74c51803fc70a8a40d7346660379e144be772bab4ac7bb6e6b905152345c/coverage-7.10.7-cp314-cp314t-win_amd64.whl",hashes = {sha256 = "e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d"}}, + {name = "coverage-7.10.7-cp314-cp314t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/12/65/f116a6d2127df30bcafbceef0302d8a64ba87488bf6f73a6d8eebf060873/coverage-7.10.7-cp314-cp314t-win_arm64.whl",hashes = {sha256 = "7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a"}}, + {name = "coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/9a/94/b765c1abcb613d103b64fcf10395f54d69b0ef8be6a0dd9c524384892cc7/coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl",hashes = {sha256 = "981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d"}}, + {name = "coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/72/4f/732fff31c119bb73b35236dd333030f32c4bfe909f445b423e6c7594f9a2/coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b"}}, + {name = "coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/87/02/ae7e0af4b674be47566707777db1aa375474f02a1d64b9323e5813a6cdd5/coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e"}}, + {name = "coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/a2/77/8c6d22bf61921a59bce5471c2f1f7ac30cd4ac50aadde72b8c48d5727902/coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b"}}, + {name = "coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/b1/20/b6ea4f69bbb52dac0aebd62157ba6a9dddbfe664f5af8122dac296c3ee15/coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49"}}, + {name = "coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/f9/28/4831523ba483a7f90f7b259d2018fef02cb4d5b90bc7c1505d6e5a84883c/coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911"}}, + {name = "coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/a7/9f/4331142bc98c10ca6436d2d620c3e165f31e6c58d43479985afce6f3191c/coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0"}}, + {name = "coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/ce/60/bda83b96602036b77ecf34e6393a3836365481b69f7ed7079ab85048202b/coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl",hashes = {sha256 = "b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f"}}, + {name = "coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/5f/af/152633ff35b2af63977edd835d8e6430f0caef27d171edf2fc76c270ef31/coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl",hashes = {sha256 = "b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c"}}, + {name = "coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/9d/71/d92105d122bd21cebba877228990e1646d862e34a98bb3374d3fece5a794/coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f"}}, + {name = "coverage-7.10.7-cp313-cp313-win32.whl",url = "https://files.pythonhosted.org/packages/a2/9e/9fdb08f4bf476c912f0c3ca292e019aab6712c93c9344a1653986c3fd305/coverage-7.10.7-cp313-cp313-win32.whl",hashes = {sha256 = "dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698"}}, + {name = "coverage-7.10.7-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/b1/b1/a75fd25df44eab52d1931e89980d1ada46824c7a3210be0d3c88a44aaa99/coverage-7.10.7-cp313-cp313-win_amd64.whl",hashes = {sha256 = "cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843"}}, + {name = "coverage-7.10.7-cp313-cp313-win_arm64.whl",url = "https://files.pythonhosted.org/packages/14/3a/d720d7c989562a6e9a14b2c9f5f2876bdb38e9367126d118495b89c99c37/coverage-7.10.7-cp313-cp313-win_arm64.whl",hashes = {sha256 = "4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546"}}, + {name = "coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/bb/22/e04514bf2a735d8b0add31d2b4ab636fc02370730787c576bb995390d2d5/coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl",hashes = {sha256 = "a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c"}}, + {name = "coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/11/0b/91128e099035ece15da3445d9015e4b4153a6059403452d324cbb0a575fa/coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl",hashes = {sha256 = "dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15"}}, + {name = "coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/8b/51/66420081e72801536a091a0c8f8c1f88a5c4bf7b9b1bdc6222c7afe6dc9b/coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4"}}, + {name = "coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/5d/22/9b8d458c2881b22df3db5bb3e7369e63d527d986decb6c11a591ba2364f7/coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0"}}, + {name = "coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/f7/08/16bee2c433e60913c610ea200b276e8eeef084b0d200bdcff69920bd5828/coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0"}}, + {name = "coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/20/9d/e53eb9771d154859b084b90201e5221bca7674ba449a17c101a5031d4054/coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65"}}, + {name = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/ad/b0/69bc7050f8d4e56a89fb550a1577d5d0d1db2278106f6f626464067b3817/coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541"}}, + {name = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/ef/4b/2514b060dbd1bc0aaf23b852c14bb5818f244c664cb16517feff6bb3a5ab/coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl",hashes = {sha256 = "2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6"}}, + {name = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/54/78/7ba2175007c246d75e496f64c06e94122bdb914790a1285d627a918bd271/coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl",hashes = {sha256 = "0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999"}}, + {name = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/c0/b3/fac9f7abbc841409b9a410309d73bfa6cfb2e51c3fada738cb607ce174f8/coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2"}}, + {name = "coverage-7.10.7-cp313-cp313t-win32.whl",url = "https://files.pythonhosted.org/packages/ee/51/a03bec00d37faaa891b3ff7387192cef20f01604e5283a5fabc95346befa/coverage-7.10.7-cp313-cp313t-win32.whl",hashes = {sha256 = "2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a"}}, + {name = "coverage-7.10.7-cp313-cp313t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/53/22/3cf25d614e64bf6d8e59c7c669b20d6d940bb337bdee5900b9ca41c820bb/coverage-7.10.7-cp313-cp313t-win_amd64.whl",hashes = {sha256 = "33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb"}}, + {name = "coverage-7.10.7-cp313-cp313t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/49/a1/00164f6d30d8a01c3c9c48418a7a5be394de5349b421b9ee019f380df2a0/coverage-7.10.7-cp313-cp313t-win_arm64.whl",hashes = {sha256 = "86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb"}}, + {name = "coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/13/e4/eb12450f71b542a53972d19117ea5a5cea1cab3ac9e31b0b5d498df1bd5a/coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl",hashes = {sha256 = "7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417"}}, + {name = "coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/37/66/593f9be12fc19fb36711f19a5371af79a718537204d16ea1d36f16bd78d2/coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973"}}, + {name = "coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/66/80/4c49f7ae09cafdacc73fbc30949ffe77359635c168f4e9ff33c9ebb07838/coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c"}}, + {name = "coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/a6/90/a64aaacab3b37a17aaedd83e8000142561a29eb262cede42d94a67f7556b/coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7"}}, + {name = "coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/98/2e/2dda59afd6103b342e096f246ebc5f87a3363b5412609946c120f4e7750d/coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6"}}, + {name = "coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/53/dc/8d8119c9051d50f3119bb4a75f29f1e4a6ab9415cd1fa8bf22fcc3fb3b5f/coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59"}}, + {name = "coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/98/b3/edaff9c5d79ee4d4b6d3fe046f2b1d799850425695b789d491a64225d493/coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b"}}, + {name = "coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/11/25/9a0728564bb05863f7e513e5a594fe5ffef091b325437f5430e8cfb0d530/coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl",hashes = {sha256 = "78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a"}}, + {name = "coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/e0/fd/ca2650443bfbef5b0e74373aac4df67b08180d2f184b482c41499668e258/coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl",hashes = {sha256 = "5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb"}}, + {name = "coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/24/79/f692f125fb4299b6f963b0745124998ebb8e73ecdfce4ceceb06a8c6bec5/coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1"}}, + {name = "coverage-7.10.7-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/5e/75/61b9bbd6c7d24d896bfeec57acba78e0f8deac68e6baf2d4804f7aae1f88/coverage-7.10.7-cp312-cp312-win32.whl",hashes = {sha256 = "77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256"}}, + {name = "coverage-7.10.7-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/ca/f3/3bf7905288b45b075918d372498f1cf845b5b579b723c8fd17168018d5f5/coverage-7.10.7-cp312-cp312-win_amd64.whl",hashes = {sha256 = "f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba"}}, + {name = "coverage-7.10.7-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/5c/44/3e32dbe933979d05cf2dac5e697c8599cfe038aaf51223ab901e208d5a62/coverage-7.10.7-cp312-cp312-win_arm64.whl",hashes = {sha256 = "bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf"}}, + {name = "coverage-7.10.7-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl",hashes = {sha256 = "f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260"}}, ] marker = "\"dev\" in extras" @@ -2519,19 +2121,17 @@ marker = "\"dev\" in extras" dependencies = [] [[packages]] -name = "exceptiongroup" -version = "1.3.0" -requires-python = ">=3.7" -sdist = {name = "exceptiongroup-1.3.0.tar.gz", url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hashes = {sha256 = "b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}} +name = "decorator" +version = "5.2.1" +requires-python = ">=3.8" +sdist = {name = "decorator-5.2.1.tar.gz", url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hashes = {sha256 = "65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360"}} wheels = [ - {name = "exceptiongroup-1.3.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl",hashes = {sha256 = "4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}}, + {name = "decorator-5.2.1-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl",hashes = {sha256 = "d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a"}}, ] -marker = "python_version < \"3.11\" and python_version >= \"3.9\" and \"default\" in dependency_groups or python_version < \"3.11\" and python_version >= \"3.9\" and \"dev\" in extras" +marker = "\"default\" in dependency_groups" [packages.tool.pdm] -dependencies = [ - "typing-extensions>=4.6.0; python_version < \"3.13\"", -] +dependencies = [] [[packages]] name = "h11" @@ -2547,12 +2147,51 @@ marker = "\"default\" in dependency_groups or \"dev\" in extras" dependencies = [] [[packages]] -name = "identify" -version = "2.6.12" -requires-python = ">=3.9" -sdist = {name = "identify-2.6.12.tar.gz", url = "https://files.pythonhosted.org/packages/a2/88/d193a27416618628a5eea64e3223acd800b40749a96ffb322a9b55a49ed1/identify-2.6.12.tar.gz", hashes = {sha256 = "d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6"}} +name = "html5tagger" +version = "1.3.0" +requires-python = ">=3.7" +sdist = {name = "html5tagger-1.3.0.tar.gz", url = "https://files.pythonhosted.org/packages/9e/02/2ae5f46d517a2c1d4a17f2b1e4834c2c7cc0fb3a69c92389172fa16ab389/html5tagger-1.3.0.tar.gz", hashes = {sha256 = "84fa3dfb49e5c83b79bbd856ab7b1de8e2311c3bb46a8be925f119e3880a8da9"}} wheels = [ - {name = "identify-2.6.12-py2.py3-none-any.whl",url = "https://files.pythonhosted.org/packages/7a/cd/18f8da995b658420625f7ef13f037be53ae04ec5ad33f9b718240dcfd48c/identify-2.6.12-py2.py3-none-any.whl",hashes = {sha256 = "ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2"}}, + {name = "html5tagger-1.3.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/9b/12/2f5d43ee912ea14a6baba4b3db6d309b02d932e3b7074c3339b4aded98ff/html5tagger-1.3.0-py3-none-any.whl",hashes = {sha256 = "ce14313515edffec8ed8a36c5890d023922641171b4e6e5774ad1a74998f5351"}}, +] +marker = "\"default\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [] + +[[packages]] +name = "httptools" +version = "0.6.4" +requires-python = ">=3.8.0" +sdist = {name = "httptools-0.6.4.tar.gz", url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hashes = {sha256 = "4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c"}} +wheels = [ + {name = "httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl",url = "https://files.pythonhosted.org/packages/94/a3/9fe9ad23fd35f7de6b91eeb60848986058bd8b5a5c1e256f5860a160cc3e/httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl",hashes = {sha256 = "ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660"}}, + {name = "httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/ea/d9/82d5e68bab783b632023f2fa31db20bebb4e89dfc4d2293945fd68484ee4/httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083"}}, + {name = "httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/96/c1/cb499655cbdbfb57b577734fde02f6fa0bbc3fe9fb4d87b742b512908dff/httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3"}}, + {name = "httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/af/71/ee32fd358f8a3bb199b03261f10921716990808a675d8160b5383487a317/httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071"}}, + {name = "httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/8a/0a/0d4df132bfca1507114198b766f1737d57580c9ad1cf93c1ff673e3387be/httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5"}}, + {name = "httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/1e/6a/787004fdef2cabea27bad1073bf6a33f2437b4dbd3b6fb4a9d71172b1c7c/httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0"}}, + {name = "httptools-0.6.4-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/4d/dc/7decab5c404d1d2cdc1bb330b1bf70e83d6af0396fd4fc76fc60c0d522bf/httptools-0.6.4-cp313-cp313-win_amd64.whl",hashes = {sha256 = "28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8"}}, + {name = "httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl",url = "https://files.pythonhosted.org/packages/bb/0e/d0b71465c66b9185f90a091ab36389a7352985fe857e352801c39d6127c8/httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl",hashes = {sha256 = "df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2"}}, + {name = "httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/e2/b8/412a9bb28d0a8988de3296e01efa0bd62068b33856cdda47fe1b5e890954/httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44"}}, + {name = "httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/9b/01/6fb20be3196ffdc8eeec4e653bc2a275eca7f36634c86302242c4fbb2760/httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1"}}, + {name = "httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/f7/d8/b644c44acc1368938317d76ac991c9bba1166311880bcc0ac297cb9d6bd7/httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2"}}, + {name = "httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/52/d8/254d16a31d543073a0e57f1c329ca7378d8924e7e292eda72d0064987486/httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81"}}, + {name = "httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/5f/3c/4aee161b4b7a971660b8be71a92c24d6c64372c1ab3ae7f366b3680df20f/httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f"}}, + {name = "httptools-0.6.4-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/12/b7/5cae71a8868e555f3f67a50ee7f673ce36eac970f029c0c5e9d584352961/httptools-0.6.4-cp312-cp312-win_amd64.whl",hashes = {sha256 = "db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970"}}, +] +marker = "\"default\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [] + +[[packages]] +name = "identify" +version = "2.6.12" +requires-python = ">=3.9" +sdist = {name = "identify-2.6.12.tar.gz", url = "https://files.pythonhosted.org/packages/a2/88/d193a27416618628a5eea64e3223acd800b40749a96ffb322a9b55a49ed1/identify-2.6.12.tar.gz", hashes = {sha256 = "d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6"}} +wheels = [ + {name = "identify-2.6.12-py2.py3-none-any.whl",url = "https://files.pythonhosted.org/packages/7a/cd/18f8da995b658420625f7ef13f037be53ae04ec5ad33f9b718240dcfd48c/identify-2.6.12-py2.py3-none-any.whl",hashes = {sha256 = "ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2"}}, ] marker = "\"dev\" in extras" @@ -2573,34 +2212,47 @@ marker = "\"dev\" in extras" dependencies = [] [[packages]] -name = "importlib-metadata" -version = "8.7.0" -requires-python = ">=3.9" -sdist = {name = "importlib_metadata-8.7.0.tar.gz", url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hashes = {sha256 = "d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000"}} +name = "jinja2" +version = "3.1.6" +requires-python = ">=3.7" +sdist = {name = "jinja2-3.1.6.tar.gz", url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hashes = {sha256 = "0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}} wheels = [ - {name = "importlib_metadata-8.7.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl",hashes = {sha256 = "e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd"}}, + {name = "jinja2-3.1.6-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl",hashes = {sha256 = "85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}}, ] -marker = "python_full_version < \"3.10.2\" and python_version >= \"3.9\" and \"dev\" in extras" +marker = "\"default\" in dependency_groups or \"dev\" in extras" [packages.tool.pdm] dependencies = [ - "zipp>=3.20", - "typing-extensions>=3.6.4; python_version < \"3.8\"", + "MarkupSafe>=2.0", ] [[packages]] -name = "jinja2" -version = "3.1.6" +name = "joblib" +version = "1.5.2" +requires-python = ">=3.9" +sdist = {name = "joblib-1.5.2.tar.gz", url = "https://files.pythonhosted.org/packages/e8/5d/447af5ea094b9e4c4054f82e223ada074c552335b9b4b2d14bd9b35a67c4/joblib-1.5.2.tar.gz", hashes = {sha256 = "3faa5c39054b2f03ca547da9b2f52fde67c06240c31853f306aea97f13647b55"}} +wheels = [ + {name = "joblib-1.5.2-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/1e/e8/685f47e0d754320684db4425a0967f7d3fa70126bffd76110b7009a0090f/joblib-1.5.2-py3-none-any.whl",hashes = {sha256 = "4e1f0bdbb987e6d843c70cf43714cb276623def372df3c22fe5266b2670bc241"}}, +] +marker = "\"default\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [] + +[[packages]] +name = "lazy-loader" +version = "0.4" requires-python = ">=3.7" -sdist = {name = "jinja2-3.1.6.tar.gz", url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hashes = {sha256 = "0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}} +sdist = {name = "lazy_loader-0.4.tar.gz", url = "https://files.pythonhosted.org/packages/6f/6b/c875b30a1ba490860c93da4cabf479e03f584eba06fe5963f6f6644653d8/lazy_loader-0.4.tar.gz", hashes = {sha256 = "47c75182589b91a4e1a85a136c074285a5ad4d9f39c63e0d7fb76391c4574cd1"}} wheels = [ - {name = "jinja2-3.1.6-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl",hashes = {sha256 = "85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}}, + {name = "lazy_loader-0.4-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/83/60/d497a310bde3f01cb805196ac61b7ad6dc5dcf8dce66634dc34364b20b4f/lazy_loader-0.4-py3-none-any.whl",hashes = {sha256 = "342aa8e14d543a154047afb4ba8ef17f5563baad3fc610d7b15b213b0f119efc"}}, ] -marker = "\"dev\" in extras" +marker = "\"default\" in dependency_groups" [packages.tool.pdm] dependencies = [ - "MarkupSafe>=2.0", + "packaging", + "importlib-metadata; python_version < \"3.8\"", ] [[packages]] @@ -2663,46 +2315,6 @@ wheels = [ {name = "lxml-6.0.1-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/bc/1f/962ea2696759abe331c3b0e838bb17e92224f39c638c2068bf0d8345e913/lxml-6.0.1-cp312-cp312-win32.whl",hashes = {sha256 = "987ad5c3941c64031f59c226167f55a04d1272e76b241bfafc968bdb778e07fb"}}, {name = "lxml-6.0.1-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/41/e2/22c86a990b51b44442b75c43ecb2f77b8daba8c4ba63696921966eac7022/lxml-6.0.1-cp312-cp312-win_amd64.whl",hashes = {sha256 = "abb05a45394fd76bf4a60c1b7bec0e6d4e8dfc569fc0e0b1f634cd983a006ddc"}}, {name = "lxml-6.0.1-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/b2/21/dc0c73325e5eb94ef9c9d60dbb5dcdcb2e7114901ea9509735614a74e75a/lxml-6.0.1-cp312-cp312-win_arm64.whl",hashes = {sha256 = "c4be29bce35020d8579d60aa0a4e95effd66fcfce31c46ffddf7e5422f73a299"}}, - {name = "lxml-6.0.1-cp311-cp311-macosx_10_9_universal2.whl",url = "https://files.pythonhosted.org/packages/29/c8/262c1d19339ef644cdc9eb5aad2e85bd2d1fa2d7c71cdef3ede1a3eed84d/lxml-6.0.1-cp311-cp311-macosx_10_9_universal2.whl",hashes = {sha256 = "c6acde83f7a3d6399e6d83c1892a06ac9b14ea48332a5fbd55d60b9897b9570a"}}, - {name = "lxml-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/e5/d4/1b0afbeb801468a310642c3a6f6704e53c38a4a6eb1ca6faea013333e02f/lxml-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl",hashes = {sha256 = "0d21c9cacb6a889cbb8eeb46c77ef2c1dd529cde10443fdeb1de847b3193c541"}}, - {name = "lxml-6.0.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/5b/c1/8db9b5402bf52ceb758618313f7423cd54aea85679fcf607013707d854a8/lxml-6.0.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "847458b7cd0d04004895f1fb2cca8e7c0f8ec923c49c06b7a72ec2d48ea6aca2"}}, - {name = "lxml-6.0.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/e7/78/838e115358dd2369c1c5186080dd874a50a691fb5cd80db6afe5e816e2c6/lxml-6.0.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "1dc13405bf315d008fe02b1472d2a9d65ee1c73c0a06de5f5a45e6e404d9a1c0"}}, - {name = "lxml-6.0.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/c7/b6/bdcb3a3ddd2438c5b1a1915161f34e8c85c96dc574b0ef3be3924f36315c/lxml-6.0.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "70f540c229a8c0a770dcaf6d5af56a5295e0fc314fc7ef4399d543328054bcea"}}, - {name = "lxml-6.0.1-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl",url = "https://files.pythonhosted.org/packages/73/e5/1bfb96185dc1a64c7c6fbb7369192bda4461952daa2025207715f9968205/lxml-6.0.1-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl",hashes = {sha256 = "d2f73aef768c70e8deb8c4742fca4fd729b132fda68458518851c7735b55297e"}}, - {name = "lxml-6.0.1-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/a2/ae/df3ea9ebc3c493b9c6bdc6bd8c554ac4e147f8d7839993388aab57ec606d/lxml-6.0.1-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "e7f4066b85a4fa25ad31b75444bd578c3ebe6b8ed47237896341308e2ce923c3"}}, - {name = "lxml-6.0.1-cp311-cp311-manylinux_2_31_armv7l.whl",url = "https://files.pythonhosted.org/packages/37/b3/65e1e33600542c08bc03a4c5c9c306c34696b0966a424a3be6ffec8038ed/lxml-6.0.1-cp311-cp311-manylinux_2_31_armv7l.whl",hashes = {sha256 = "0cce65db0cd8c750a378639900d56f89f7d6af11cd5eda72fde054d27c54b8ce"}}, - {name = "lxml-6.0.1-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/7a/46/ee3ed8f3a60e9457d7aea46542d419917d81dbfd5700fe64b2a36fb5ef61/lxml-6.0.1-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "c372d42f3eee5844b69dcab7b8d18b2f449efd54b46ac76970d6e06b8e8d9a66"}}, - {name = "lxml-6.0.1-cp311-cp311-musllinux_1_2_armv7l.whl",url = "https://files.pythonhosted.org/packages/9c/b9/8394538e7cdbeb3bfa36bc74924be1a4383e0bb5af75f32713c2c4aa0479/lxml-6.0.1-cp311-cp311-musllinux_1_2_armv7l.whl",hashes = {sha256 = "2e2b0e042e1408bbb1c5f3cfcb0f571ff4ac98d8e73f4bf37c5dd179276beedd"}}, - {name = "lxml-6.0.1-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/b3/21/3ef7da1ea2a73976c1a5a311d7cde5d379234eec0968ee609517714940b4/lxml-6.0.1-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "cc73bb8640eadd66d25c5a03175de6801f63c535f0f3cf50cac2f06a8211f420"}}, - {name = "lxml-6.0.1-cp311-cp311-win32.whl",url = "https://files.pythonhosted.org/packages/26/7d/0980016f124f00c572cba6f4243e13a8e80650843c66271ee692cddf25f3/lxml-6.0.1-cp311-cp311-win32.whl",hashes = {sha256 = "7c23fd8c839708d368e406282d7953cee5134f4592ef4900026d84566d2b4c88"}}, - {name = "lxml-6.0.1-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/b1/08/28440437521f265eff4413eb2a65efac269c4c7db5fd8449b586e75d8de2/lxml-6.0.1-cp311-cp311-win_amd64.whl",hashes = {sha256 = "2516acc6947ecd3c41a4a4564242a87c6786376989307284ddb115f6a99d927f"}}, - {name = "lxml-6.0.1-cp311-cp311-win_arm64.whl",url = "https://files.pythonhosted.org/packages/7b/dc/617e67296d98099213a505d781f04804e7b12923ecd15a781a4ab9181992/lxml-6.0.1-cp311-cp311-win_arm64.whl",hashes = {sha256 = "cb46f8cfa1b0334b074f40c0ff94ce4d9a6755d492e6c116adb5f4a57fb6ad96"}}, - {name = "lxml-6.0.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl",url = "https://files.pythonhosted.org/packages/41/37/41961f53f83ded57b37e65e4f47d1c6c6ef5fd02cb1d6ffe028ba0efa7d4/lxml-6.0.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl",hashes = {sha256 = "b556aaa6ef393e989dac694b9c95761e32e058d5c4c11ddeef33f790518f7a5e"}}, - {name = "lxml-6.0.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/3d/47/8631ea73f3dc776fb6517ccde4d5bd5072f35f9eacbba8c657caa4037a69/lxml-6.0.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "64fac7a05ebb3737b79fd89fe5a5b6c5546aac35cfcfd9208eb6e5d13215771c"}}, - {name = "lxml-6.0.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/3d/b8/39ae30ca3b1516729faeef941ed84bf8f12321625f2644492ed8320cb254/lxml-6.0.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "038d3c08babcfce9dc89aaf498e6da205efad5b7106c3b11830a488d4eadf56b"}}, - {name = "lxml-6.0.1-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/9c/ea/048dea6cdfc7a72d40ae8ed7e7d23cf4a6b6a6547b51b492a3be50af0e80/lxml-6.0.1-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "445f2cee71c404ab4259bc21e20339a859f75383ba2d7fb97dfe7c163994287b"}}, - {name = "lxml-6.0.1-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/6b/d4/c2b46e432377c45d611ae2f669aa47971df1586c1a5240675801d0f02bac/lxml-6.0.1-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "e352d8578e83822d70bea88f3d08b9912528e4c338f04ab707207ab12f4b7aac"}}, - {name = "lxml-6.0.1-pp311-pypy311_pp73-win_amd64.whl",url = "https://files.pythonhosted.org/packages/b6/db/8f620f1ac62cf32554821b00b768dd5957ac8e3fd051593532be5b40b438/lxml-6.0.1-pp311-pypy311_pp73-win_amd64.whl",hashes = {sha256 = "51bd5d1a9796ca253db6045ab45ca882c09c071deafffc22e06975b7ace36300"}}, - {name = "lxml-6.0.1-cp310-cp310-macosx_10_9_universal2.whl",url = "https://files.pythonhosted.org/packages/b2/06/29693634ad5fc8ae0bab6723ba913c821c780614eea9ab9ebb5b2105d0e4/lxml-6.0.1-cp310-cp310-macosx_10_9_universal2.whl",hashes = {sha256 = "3b38e20c578149fdbba1fd3f36cb1928a3aaca4b011dfd41ba09d11fb396e1b9"}}, - {name = "lxml-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/97/e0/69d4113afbda9441f0e4d5574d9336535ead6a0608ee6751b3db0832ade0/lxml-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl",hashes = {sha256 = "11a052cbd013b7140bbbb38a14e2329b6192478344c99097e378c691b7119551"}}, - {name = "lxml-6.0.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/eb/3d/8fa1dbf48a3ea0d6c646f0129bef89a5ecf9a1cfe935e26e07554261d728/lxml-6.0.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "21344d29c82ca8547ea23023bb8e7538fa5d4615a1773b991edf8176a870c1ea"}}, - {name = "lxml-6.0.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/2c/52/a48331a269900488b886d527611ab66238cddc6373054a60b3c15d4cefb2/lxml-6.0.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "aa8f130f4b2dc94baa909c17bb7994f0268a2a72b9941c872e8e558fd6709050"}}, - {name = "lxml-6.0.1-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/33/3b/8f6778a6fb9d30a692db2b1f5a9547dfcb674b27b397e1d864ca797486b1/lxml-6.0.1-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "4588806a721552692310ebe9f90c17ac6c7c5dac438cd93e3d74dd60531c3211"}}, - {name = "lxml-6.0.1-cp310-cp310-manylinux_2_26_i686.manylinux_2_28_i686.whl",url = "https://files.pythonhosted.org/packages/42/15/c9364f23fa89ef2d3dbb896912aa313108820286223cfa833a0a9e183c9e/lxml-6.0.1-cp310-cp310-manylinux_2_26_i686.manylinux_2_28_i686.whl",hashes = {sha256 = "8466faa66b0353802fb7c054a400ac17ce2cf416e3ad8516eadeff9cba85b741"}}, - {name = "lxml-6.0.1-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/04/af/11985b0d47786161ddcdc53dc06142dc863b81a38da7f221c7b997dd5d4b/lxml-6.0.1-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "50b5e54f6a9461b1e9c08b4a3420415b538d4773bd9df996b9abcbfe95f4f1fd"}}, - {name = "lxml-6.0.1-cp310-cp310-manylinux_2_31_armv7l.whl",url = "https://files.pythonhosted.org/packages/6a/42/74b35ccc9ef1bb53f0487a4dace5ff612f1652d27faafe91ada7f7b9ee60/lxml-6.0.1-cp310-cp310-manylinux_2_31_armv7l.whl",hashes = {sha256 = "6f393e10685b37f15b1daef8aa0d734ec61860bb679ec447afa0001a31e7253f"}}, - {name = "lxml-6.0.1-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/b0/5a/b934534f83561ad71fb64ba1753992e836ea73776cfb56fc0758dbb46bdf/lxml-6.0.1-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "07038c62fd0fe2743e2f5326f54d464715373c791035d7dda377b3c9a5d0ad77"}}, - {name = "lxml-6.0.1-cp310-cp310-musllinux_1_2_armv7l.whl",url = "https://files.pythonhosted.org/packages/6c/26/d833a56ec8ca943b696f3a7a1e54f97cfb63754c951037de5e222c011f3b/lxml-6.0.1-cp310-cp310-musllinux_1_2_armv7l.whl",hashes = {sha256 = "7a44a5fb1edd11b3a65c12c23e1049c8ae49d90a24253ff18efbcb6aa042d012"}}, - {name = "lxml-6.0.1-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/3f/cb/601aa274c7cda51d0cc84a13d9639096c1191de9d9adf58f6c195d4822a2/lxml-6.0.1-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "a57d9eb9aadf311c9e8785230eec83c6abb9aef2adac4c0587912caf8f3010b8"}}, - {name = "lxml-6.0.1-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/76/4e/e079f7b324e6d5f83007f30855448646e1cba74b5c30da1a081df75eba89/lxml-6.0.1-cp310-cp310-win32.whl",hashes = {sha256 = "d877874a31590b72d1fa40054b50dc33084021bfc15d01b3a661d85a302af821"}}, - {name = "lxml-6.0.1-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/65/0a/da298d7a96316c75ae096686de8d036d814ec3b72c7d643a2c226c364168/lxml-6.0.1-cp310-cp310-win_amd64.whl",hashes = {sha256 = "c43460f4aac016ee0e156bfa14a9de9b3e06249b12c228e27654ac3996a46d5b"}}, - {name = "lxml-6.0.1-cp310-cp310-win_arm64.whl",url = "https://files.pythonhosted.org/packages/0f/65/d7f61082fecf4543ab084e8bd3d4b9be0c1a0c83979f1fa2258e2a7987fb/lxml-6.0.1-cp310-cp310-win_arm64.whl",hashes = {sha256 = "615bb6c73fed7929e3a477a3297a797892846b253d59c84a62c98bdce3849a0a"}}, - {name = "lxml-6.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl",url = "https://files.pythonhosted.org/packages/ae/61/ad51fbecaf741f825d496947b19d8aea0dcd323fdc2be304e93ce59f66f0/lxml-6.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl",hashes = {sha256 = "0abfbaf4ebbd7fd33356217d317b6e4e2ef1648be6a9476a52b57ffc6d8d1780"}}, - {name = "lxml-6.0.1-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/1b/7f/310bef082cc69d0db46a8b9d8ca5f4a8fb41e1c5d299ef4ca5f391c4f12d/lxml-6.0.1-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "1ebbf2d9775be149235abebdecae88fe3b3dd06b1797cd0f6dffe6948e85309d"}}, - {name = "lxml-6.0.1-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/86/cc/dc5833def5998c783500666468df127d6d919e8b9678866904e5680b0b13/lxml-6.0.1-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "a389e9f11c010bd30531325805bbe97bdf7f728a73d0ec475adef57ffec60547"}}, - {name = "lxml-6.0.1-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/1b/dc/bdd4d413844b5348134444d64911f6f34b211f8b778361946d07623fc904/lxml-6.0.1-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "8f5cf2addfbbe745251132c955ad62d8519bb4b2c28b0aa060eca4541798d86e"}}, - {name = "lxml-6.0.1-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/d9/14/e60e9d46972603753824eb7bea06fbe4153c627cc0f7110111253b7c9fc5/lxml-6.0.1-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "f1b60a3287bf33a2a54805d76b82055bcc076e445fd539ee9ae1fe85ed373691"}}, - {name = "lxml-6.0.1-pp310-pypy310_pp73-win_amd64.whl",url = "https://files.pythonhosted.org/packages/42/fa/268c9be8c69a418b8106e096687aba2b1a781fb6fc1b3f04955fac2be2b9/lxml-6.0.1-pp310-pypy310_pp73-win_amd64.whl",hashes = {sha256 = "f7bbfb0751551a8786915fc6b615ee56344dacc1b1033697625b553aefdd9837"}}, ] marker = "\"recommended\" in extras" @@ -2745,38 +2357,8 @@ wheels = [ {name = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}}, {name = "MarkupSafe-3.0.2-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl",hashes = {sha256 = "0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}}, {name = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl",hashes = {sha256 = "8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}}, - {name = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl",url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl",hashes = {sha256 = "9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}}, - {name = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}}, - {name = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}}, - {name = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}}, - {name = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}}, - {name = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}}, - {name = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl",hashes = {sha256 = "5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}}, - {name = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}}, - {name = "MarkupSafe-3.0.2-cp311-cp311-win32.whl",url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl",hashes = {sha256 = "6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}}, - {name = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl",hashes = {sha256 = "70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}}, - {name = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl",url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl",hashes = {sha256 = "7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}}, - {name = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}}, - {name = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}}, - {name = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}}, - {name = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}}, - {name = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}}, - {name = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl",hashes = {sha256 = "e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}}, - {name = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}}, - {name = "MarkupSafe-3.0.2-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl",hashes = {sha256 = "fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}}, - {name = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl",hashes = {sha256 = "6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}}, - {name = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl",url = "https://files.pythonhosted.org/packages/a7/ea/9b1530c3fdeeca613faeb0fb5cbcf2389d816072fab72a71b45749ef6062/MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl",hashes = {sha256 = "eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}}, - {name = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/4b/c2/fbdbfe48848e7112ab05e627e718e854d20192b674952d9042ebd8c9e5de/MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl",hashes = {sha256 = "48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}}, - {name = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/f0/25/7a7c6e4dbd4f867d95d94ca15449e91e52856f6ed1905d58ef1de5e211d0/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}}, - {name = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/53/8f/f339c98a178f3c1e545622206b40986a4c3307fe39f70ccd3d9df9a9e425/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}}, - {name = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/1a/03/8496a1a78308456dbd50b23a385c69b41f2e9661c67ea1329849a598a8f9/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}}, - {name = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/e6/cf/0a490a4bd363048c3022f2f475c8c05582179bb179defcee4766fb3dcc18/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl",hashes = {sha256 = "1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}}, - {name = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/19/a3/34187a78613920dfd3cdf68ef6ce5e99c4f3417f035694074beb8848cd77/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl",hashes = {sha256 = "3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}}, - {name = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/17/d8/5811082f85bb88410ad7e452263af048d685669bbbfb7b595e8689152498/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl",hashes = {sha256 = "eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}}, - {name = "MarkupSafe-3.0.2-cp39-cp39-win32.whl",url = "https://files.pythonhosted.org/packages/7c/31/bd635fb5989440d9365c5e3c47556cfea121c7803f5034ac843e8f37c2f2/MarkupSafe-3.0.2-cp39-cp39-win32.whl",hashes = {sha256 = "8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}}, - {name = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl",url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl",hashes = {sha256 = "6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}}, ] -marker = "\"dev\" in extras" +marker = "\"default\" in dependency_groups or \"dev\" in extras" [packages.tool.pdm] dependencies = [] @@ -2817,13 +2399,6 @@ requires-python = ">=3.8" sdist = {name = "multiprocess-0.70.16.tar.gz", url = "https://files.pythonhosted.org/packages/b5/ae/04f39c5d0d0def03247c2893d6f2b83c136bf3320a2154d7b8858f2ba72d/multiprocess-0.70.16.tar.gz", hashes = {sha256 = "161af703d4652a0e1410be6abccecde4a7ddffd19341be0a7011b94aeb171ac1"}} wheels = [ {name = "multiprocess-0.70.16-py312-none-any.whl",url = "https://files.pythonhosted.org/packages/0a/7d/a988f258104dcd2ccf1ed40fdc97e26c4ac351eeaf81d76e266c52d84e2f/multiprocess-0.70.16-py312-none-any.whl",hashes = {sha256 = "fc0544c531920dde3b00c29863377f87e1632601092ea2daca74e4beb40faa2e"}}, - {name = "multiprocess-0.70.16-py311-none-any.whl",url = "https://files.pythonhosted.org/packages/50/15/b56e50e8debaf439f44befec5b2af11db85f6e0f344c3113ae0be0593a91/multiprocess-0.70.16-py311-none-any.whl",hashes = {sha256 = "af4cabb0dac72abfb1e794fa7855c325fd2b55a10a44628a3c1ad3311c04127a"}}, - {name = "multiprocess-0.70.16-pp310-pypy310_pp73-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/ef/76/6e712a2623d146d314f17598df5de7224c85c0060ef63fd95cc15a25b3fa/multiprocess-0.70.16-pp310-pypy310_pp73-macosx_10_13_x86_64.whl",hashes = {sha256 = "476887be10e2f59ff183c006af746cb6f1fd0eadcfd4ef49e605cbe2659920ee"}}, - {name = "multiprocess-0.70.16-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/0f/ab/1e6e8009e380e22254ff539ebe117861e5bdb3bff1fc977920972237c6c7/multiprocess-0.70.16-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl",hashes = {sha256 = "d951bed82c8f73929ac82c61f01a7b5ce8f3e5ef40f5b52553b4f547ce2b08ec"}}, - {name = "multiprocess-0.70.16-py310-none-any.whl",url = "https://files.pythonhosted.org/packages/bc/f7/7ec7fddc92e50714ea3745631f79bd9c96424cb2702632521028e57d3a36/multiprocess-0.70.16-py310-none-any.whl",hashes = {sha256 = "c4a9944c67bd49f823687463660a2d6daae94c289adff97e0f9d696ba6371d02"}}, - {name = "multiprocess-0.70.16-pp39-pypy39_pp73-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/d8/94/8638a89f93c80df329116e6781a060506c7e91e1f4370dc831e9d17a041d/multiprocess-0.70.16-pp39-pypy39_pp73-macosx_10_13_x86_64.whl",hashes = {sha256 = "0dfd078c306e08d46d7a8d06fb120313d87aa43af60d66da43ffff40b44d2f41"}}, - {name = "multiprocess-0.70.16-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/89/21/222066f6bb8d8af287923ae3bd26cf4699a9ce020228ac273caca1de8250/multiprocess-0.70.16-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl",hashes = {sha256 = "e7b9d0f307cd9bd50851afaac0dba2cb6c44449efff697df7c7645f7d3f2be3a"}}, - {name = "multiprocess-0.70.16-py39-none-any.whl",url = "https://files.pythonhosted.org/packages/da/d9/f7f9379981e39b8c2511c9e0326d212accacb82f12fbfdc1aa2ce2a7b2b6/multiprocess-0.70.16-py39-none-any.whl",hashes = {sha256 = "a0bafd3ae1b732eac64be2e72038231c1ba97724b60b09400d68f229fcc2fbf3"}}, ] marker = "\"default\" in dependency_groups" @@ -2858,6 +2433,70 @@ marker = "\"dev\" in extras" [packages.tool.pdm] dependencies = [] +[[packages]] +name = "numba" +version = "0.62.1" +requires-python = ">=3.10" +sdist = {name = "numba-0.62.1.tar.gz", url = "https://files.pythonhosted.org/packages/a3/20/33dbdbfe60e5fd8e3dbfde299d106279a33d9f8308346022316781368591/numba-0.62.1.tar.gz", hashes = {sha256 = "7b774242aa890e34c21200a1fc62e5b5757d5286267e71103257f4e2af0d5161"}} +wheels = [ + {name = "numba-0.62.1-cp313-cp313-macosx_10_15_x86_64.whl",url = "https://files.pythonhosted.org/packages/22/76/501ea2c07c089ef1386868f33dff2978f43f51b854e34397b20fc55e0a58/numba-0.62.1-cp313-cp313-macosx_10_15_x86_64.whl",hashes = {sha256 = "b72489ba8411cc9fdcaa2458d8f7677751e94f0109eeb53e5becfdc818c64afb"}}, + {name = "numba-0.62.1-cp313-cp313-macosx_12_0_arm64.whl",url = "https://files.pythonhosted.org/packages/80/68/444986ed95350c0611d5c7b46828411c222ce41a0c76707c36425d27ce29/numba-0.62.1-cp313-cp313-macosx_12_0_arm64.whl",hashes = {sha256 = "44a1412095534a26fb5da2717bc755b57da5f3053965128fe3dc286652cc6a92"}}, + {name = "numba-0.62.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/78/7e/bf2e3634993d57f95305c7cee4c9c6cb3c9c78404ee7b49569a0dfecfe33/numba-0.62.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "8c9460b9e936c5bd2f0570e20a0a5909ee6e8b694fd958b210e3bde3a6dba2d7"}}, + {name = "numba-0.62.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/e8/b6/8a1723fff71f63bbb1354bdc60a1513a068acc0f5322f58da6f022d20247/numba-0.62.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "728f91a874192df22d74e3fd42c12900b7ce7190b1aad3574c6c61b08313e4c5"}}, + {name = "numba-0.62.1-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/9c/ec/9d414e7a80d6d1dc4af0e07c6bfe293ce0b04ea4d0ed6c45dad9bd6e72eb/numba-0.62.1-cp313-cp313-win_amd64.whl",hashes = {sha256 = "bbf3f88b461514287df66bc8d0307e949b09f2b6f67da92265094e8fa1282dd8"}}, + {name = "numba-0.62.1-cp312-cp312-macosx_10_15_x86_64.whl",url = "https://files.pythonhosted.org/packages/5e/fa/30fa6873e9f821c0ae755915a3ca444e6ff8d6a7b6860b669a3d33377ac7/numba-0.62.1-cp312-cp312-macosx_10_15_x86_64.whl",hashes = {sha256 = "1b743b32f8fa5fff22e19c2e906db2f0a340782caf024477b97801b918cf0494"}}, + {name = "numba-0.62.1-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/a9/d5/504ce8dc46e0dba2790c77e6b878ee65b60fe3e7d6d0006483ef6fde5a97/numba-0.62.1-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "90fa21b0142bcf08ad8e32a97d25d0b84b1e921bc9423f8dda07d3652860eef6"}}, + {name = "numba-0.62.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/50/5f/6a802741176c93f2ebe97ad90751894c7b0c922b52ba99a4395e79492205/numba-0.62.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "6ef84d0ac19f1bf80431347b6f4ce3c39b7ec13f48f233a48c01e2ec06ecbc59"}}, + {name = "numba-0.62.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/7e/df/efd21527d25150c4544eccc9d0b7260a5dec4b7e98b5a581990e05a133c0/numba-0.62.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "9315cc5e441300e0ca07c828a627d92a6802bcbf27c5487f31ae73783c58da53"}}, + {name = "numba-0.62.1-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/80/44/79bfdab12a02796bf4f1841630355c82b5a69933b1d50eb15c7fa37dabe8/numba-0.62.1-cp312-cp312-win_amd64.whl",hashes = {sha256 = "44e3aa6228039992f058f5ebfcfd372c83798e9464297bdad8cc79febcf7891e"}}, +] +marker = "python_version ~= \"3.12\"" + +[packages.tool.pdm] +dependencies = [ + "llvmlite<0.46,>=0.45.0dev0", + "numpy<2.4,>=1.22", +] + +[[packages]] +name = "llvmlite" +version = "0.45.1" +requires-python = ">=3.10" +sdist = {name = "llvmlite-0.45.1.tar.gz", url = "https://files.pythonhosted.org/packages/99/8d/5baf1cef7f9c084fb35a8afbde88074f0d6a727bc63ef764fe0e7543ba40/llvmlite-0.45.1.tar.gz", hashes = {sha256 = "09430bb9d0bb58fc45a45a57c7eae912850bedc095cd0810a57de109c69e1c32"}} +wheels = [ + {name = "llvmlite-0.45.1-cp313-cp313-macosx_10_15_x86_64.whl",url = "https://files.pythonhosted.org/packages/1d/e2/c185bb7e88514d5025f93c6c4092f6120c6cea8fe938974ec9860fb03bbb/llvmlite-0.45.1-cp313-cp313-macosx_10_15_x86_64.whl",hashes = {sha256 = "d9ea9e6f17569a4253515cc01dade70aba536476e3d750b2e18d81d7e670eb15"}}, + {name = "llvmlite-0.45.1-cp313-cp313-macosx_12_0_arm64.whl",url = "https://files.pythonhosted.org/packages/09/b8/b5437b9ecb2064e89ccf67dccae0d02cd38911705112dd0dcbfa9cd9a9de/llvmlite-0.45.1-cp313-cp313-macosx_12_0_arm64.whl",hashes = {sha256 = "c9f3cadee1630ce4ac18ea38adebf2a4f57a89bd2740ce83746876797f6e0bfb"}}, + {name = "llvmlite-0.45.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/f7/97/ad1a907c0173a90dd4df7228f24a3ec61058bc1a9ff8a0caec20a0cc622e/llvmlite-0.45.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "57c48bf2e1083eedbc9406fb83c4e6483017879714916fe8be8a72a9672c995a"}}, + {name = "llvmlite-0.45.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/32/d8/c99c8ac7a326e9735401ead3116f7685a7ec652691aeb2615aa732b1fc4a/llvmlite-0.45.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "3aa3dfceda4219ae39cf18806c60eeb518c1680ff834b8b311bd784160b9ce40"}}, + {name = "llvmlite-0.45.1-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/09/56/ed35668130e32dbfad2eb37356793b0a95f23494ab5be7d9bf5cb75850ee/llvmlite-0.45.1-cp313-cp313-win_amd64.whl",hashes = {sha256 = "080e6f8d0778a8239cd47686d402cb66eb165e421efa9391366a9b7e5810a38b"}}, + {name = "llvmlite-0.45.1-cp312-cp312-macosx_10_15_x86_64.whl",url = "https://files.pythonhosted.org/packages/e2/7c/82cbd5c656e8991bcc110c69d05913be2229302a92acb96109e166ae31fb/llvmlite-0.45.1-cp312-cp312-macosx_10_15_x86_64.whl",hashes = {sha256 = "28e763aba92fe9c72296911e040231d486447c01d4f90027c8e893d89d49b20e"}}, + {name = "llvmlite-0.45.1-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/9d/bc/5314005bb2c7ee9f33102c6456c18cc81745d7055155d1218f1624463774/llvmlite-0.45.1-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "1a53f4b74ee9fd30cb3d27d904dadece67a7575198bd80e687ee76474620735f"}}, + {name = "llvmlite-0.45.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/96/76/0f7154952f037cb320b83e1c952ec4a19d5d689cf7d27cb8a26887d7bbc1/llvmlite-0.45.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "5b3796b1b1e1c14dcae34285d2f4ea488402fbd2c400ccf7137603ca3800864f"}}, + {name = "llvmlite-0.45.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/00/b1/0b581942be2683ceb6862d558979e87387e14ad65a1e4db0e7dd671fa315/llvmlite-0.45.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "779e2f2ceefef0f4368548685f0b4adde34e5f4b457e90391f570a10b348d433"}}, + {name = "llvmlite-0.45.1-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/33/94/9ba4ebcf4d541a325fd8098ddc073b663af75cc8b065b6059848f7d4dce7/llvmlite-0.45.1-cp312-cp312-win_amd64.whl",hashes = {sha256 = "9e6c9949baf25d9aa9cd7cf0f6d011b9ca660dd17f5ba2b23bdbdb77cc86b116"}}, +] +marker = "python_version ~= \"3.12\"" + +[packages.tool.pdm] +dependencies = [] + +[[packages]] +name = "pooch" +version = "1.8.2" +requires-python = ">=3.7" +sdist = {name = "pooch-1.8.2.tar.gz", url = "https://files.pythonhosted.org/packages/c6/77/b3d3e00c696c16cf99af81ef7b1f5fe73bd2a307abca41bd7605429fe6e5/pooch-1.8.2.tar.gz", hashes = {sha256 = "76561f0de68a01da4df6af38e9955c4c9d1a5c90da73f7e40276a5728ec83d10"}} +wheels = [ + {name = "pooch-1.8.2-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/a8/87/77cc11c7a9ea9fd05503def69e3d18605852cd0d4b0d3b8f15bbeb3ef1d1/pooch-1.8.2-py3-none-any.whl",hashes = {sha256 = "3529a57096f7198778a5ceefd5ac3ef0e4d06a6ddaf9fc2d609b806f25302c47"}}, +] +marker = "\"default\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [ + "platformdirs>=2.5.0", + "packaging>=20.0", + "requests>=2.19.0", +] + [[packages]] name = "pyarrow" version = "20.0.0" @@ -2891,33 +2530,6 @@ wheels = [ {name = "pyarrow-20.0.0-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/b2/66/2d976c0c7158fd25591c8ca55aee026e6d5745a021915a1835578707feb3/pyarrow-20.0.0-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "89e030dc58fc760e4010148e6ff164d2f44441490280ef1e97a542375e41058e"}}, {name = "pyarrow-20.0.0-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/31/a9/dfb999c2fc6911201dcbf348247f9cc382a8990f9ab45c12eabfd7243a38/pyarrow-20.0.0-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "6102b4864d77102dbbb72965618e204e550135a940c2534711d5ffa787df2a5a"}}, {name = "pyarrow-20.0.0-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/a0/8e/9adee63dfa3911be2382fb4d92e4b2e7d82610f9d9f668493bebaa2af50f/pyarrow-20.0.0-cp312-cp312-win_amd64.whl",hashes = {sha256 = "96d6a0a37d9c98be08f5ed6a10831d88d52cac7b13f5287f1e0f625a0de8062b"}}, - {name = "pyarrow-20.0.0-cp311-cp311-macosx_12_0_arm64.whl",url = "https://files.pythonhosted.org/packages/47/a2/b7930824181ceadd0c63c1042d01fa4ef63eee233934826a7a2a9af6e463/pyarrow-20.0.0-cp311-cp311-macosx_12_0_arm64.whl",hashes = {sha256 = "24ca380585444cb2a31324c546a9a56abbe87e26069189e14bdba19c86c049f0"}}, - {name = "pyarrow-20.0.0-cp311-cp311-macosx_12_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/9b/18/c765770227d7f5bdfa8a69f64b49194352325c66a5c3bb5e332dfd5867d9/pyarrow-20.0.0-cp311-cp311-macosx_12_0_x86_64.whl",hashes = {sha256 = "95b330059ddfdc591a3225f2d272123be26c8fa76e8c9ee1a77aad507361cfdb"}}, - {name = "pyarrow-20.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/44/fb/dfb2dfdd3e488bb14f822d7335653092dde150cffc2da97de6e7500681f9/pyarrow-20.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "5f0fb1041267e9968c6d0d2ce3ff92e3928b243e2b6d11eeb84d9ac547308232"}}, - {name = "pyarrow-20.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/58/0d/08a95878d38808051a953e887332d4a76bc06c6ee04351918ee1155407eb/pyarrow-20.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "b8ff87cc837601532cc8242d2f7e09b4e02404de1b797aee747dd4ba4bd6313f"}}, - {name = "pyarrow-20.0.0-cp311-cp311-manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/f3/cd/efa271234dfe38f0271561086eedcad7bc0f2ddd1efba423916ff0883684/pyarrow-20.0.0-cp311-cp311-manylinux_2_28_aarch64.whl",hashes = {sha256 = "7a3a5dcf54286e6141d5114522cf31dd67a9e7c9133d150799f30ee302a7a1ab"}}, - {name = "pyarrow-20.0.0-cp311-cp311-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/46/1f/7f02009bc7fc8955c391defee5348f510e589a020e4b40ca05edcb847854/pyarrow-20.0.0-cp311-cp311-manylinux_2_28_x86_64.whl",hashes = {sha256 = "a6ad3e7758ecf559900261a4df985662df54fb7fdb55e8e3b3aa99b23d526b62"}}, - {name = "pyarrow-20.0.0-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/4f/92/692c562be4504c262089e86757a9048739fe1acb4024f92d39615e7bab3f/pyarrow-20.0.0-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "6bb830757103a6cb300a04610e08d9636f0cd223d32f388418ea893a3e655f1c"}}, - {name = "pyarrow-20.0.0-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/a4/ec/9f5c7e7c828d8e0a3c7ef50ee62eca38a7de2fa6eb1b8fa43685c9414fef/pyarrow-20.0.0-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "96e37f0766ecb4514a899d9a3554fadda770fb57ddf42b63d80f14bc20aa7db3"}}, - {name = "pyarrow-20.0.0-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/54/96/46613131b4727f10fd2ffa6d0d6f02efcc09a0e7374eff3b5771548aa95b/pyarrow-20.0.0-cp311-cp311-win_amd64.whl",hashes = {sha256 = "3346babb516f4b6fd790da99b98bed9708e3f02e734c84971faccb20736848dc"}}, - {name = "pyarrow-20.0.0-cp310-cp310-macosx_12_0_arm64.whl",url = "https://files.pythonhosted.org/packages/5b/23/77094eb8ee0dbe88441689cb6afc40ac312a1e15d3a7acc0586999518222/pyarrow-20.0.0-cp310-cp310-macosx_12_0_arm64.whl",hashes = {sha256 = "c7dd06fd7d7b410ca5dc839cc9d485d2bc4ae5240851bcd45d85105cc90a47d7"}}, - {name = "pyarrow-20.0.0-cp310-cp310-macosx_12_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/c3/d5/48cc573aff00d62913701d9fac478518f693b30c25f2c157550b0b2565cb/pyarrow-20.0.0-cp310-cp310-macosx_12_0_x86_64.whl",hashes = {sha256 = "d5382de8dc34c943249b01c19110783d0d64b207167c728461add1ecc2db88e4"}}, - {name = "pyarrow-20.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/37/df/4099b69a432b5cb412dd18adc2629975544d656df3d7fda6d73c5dba935d/pyarrow-20.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "6415a0d0174487456ddc9beaead703d0ded5966129fa4fd3114d76b5d1c5ceae"}}, - {name = "pyarrow-20.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/4c/27/99922a9ac1c9226f346e3a1e15e63dee6f623ed757ff2893f9d6994a69d3/pyarrow-20.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "15aa1b3b2587e74328a730457068dc6c89e6dcbf438d4369f572af9d320a25ee"}}, - {name = "pyarrow-20.0.0-cp310-cp310-manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/21/d1/71d91b2791b829c9e98f1e0d85be66ed93aff399f80abb99678511847eaa/pyarrow-20.0.0-cp310-cp310-manylinux_2_28_aarch64.whl",hashes = {sha256 = "5605919fbe67a7948c1f03b9f3727d82846c053cd2ce9303ace791855923fd20"}}, - {name = "pyarrow-20.0.0-cp310-cp310-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/f1/ca/ae10fba419a6e94329707487835ec721f5a95f3ac9168500bcf7aa3813c7/pyarrow-20.0.0-cp310-cp310-manylinux_2_28_x86_64.whl",hashes = {sha256 = "a5704f29a74b81673d266e5ec1fe376f060627c2e42c5c7651288ed4b0db29e9"}}, - {name = "pyarrow-20.0.0-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/7a/a6/aba40a2bf01b5d00cf9cd16d427a5da1fad0fb69b514ce8c8292ab80e968/pyarrow-20.0.0-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "00138f79ee1b5aca81e2bdedb91e3739b987245e11fa3c826f9e57c5d102fb75"}}, - {name = "pyarrow-20.0.0-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/93/6b/98b39650cd64f32bf2ec6d627a9bd24fcb3e4e6ea1873c5e1ea8a83b1a18/pyarrow-20.0.0-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "f2d67ac28f57a362f1a2c1e6fa98bfe2f03230f7e15927aecd067433b1e70ce8"}}, - {name = "pyarrow-20.0.0-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/ab/32/340238be1eb5037e7b5de7e640ee22334417239bc347eadefaf8c373936d/pyarrow-20.0.0-cp310-cp310-win_amd64.whl",hashes = {sha256 = "4a8b029a07956b8d7bd742ffca25374dd3f634b35e46cc7a7c3fa4c75b297191"}}, - {name = "pyarrow-20.0.0-cp39-cp39-macosx_12_0_arm64.whl",url = "https://files.pythonhosted.org/packages/10/53/421820fa125138c868729b930d4bc487af2c4b01b1c6104818aab7e98f13/pyarrow-20.0.0-cp39-cp39-macosx_12_0_arm64.whl",hashes = {sha256 = "1bcbe471ef3349be7714261dea28fe280db574f9d0f77eeccc195a2d161fd861"}}, - {name = "pyarrow-20.0.0-cp39-cp39-macosx_12_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/2e/70/fd75e03312b715e90d928fb91ed8d45c9b0520346e5231b1c69293afd4c7/pyarrow-20.0.0-cp39-cp39-macosx_12_0_x86_64.whl",hashes = {sha256 = "a18a14baef7d7ae49247e75641fd8bcbb39f44ed49a9fc4ec2f65d5031aa3b96"}}, - {name = "pyarrow-20.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/c4/e3/21e5758e46219fdedf5e6c800574dd9d17e962e80014cfe08d6d475be863/pyarrow-20.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "cb497649e505dc36542d0e68eca1a3c94ecbe9799cb67b578b55f2441a247fbc"}}, - {name = "pyarrow-20.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/ac/f5/ed6a4c4b11f9215092a35097a985485bb7d879cb79d93d203494e8604f4e/pyarrow-20.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "11529a2283cb1f6271d7c23e4a8f9f8b7fd173f7360776b668e509d712a02eec"}}, - {name = "pyarrow-20.0.0-cp39-cp39-manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/44/e5/466a63668ba25788ee8d38d55f853a60469ae7ad1cda343db9f3f45e0b0a/pyarrow-20.0.0-cp39-cp39-manylinux_2_28_aarch64.whl",hashes = {sha256 = "6fc1499ed3b4b57ee4e090e1cea6eb3584793fe3d1b4297bbf53f09b434991a5"}}, - {name = "pyarrow-20.0.0-cp39-cp39-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/e8/d7/4c4d4e4cf6e53e16a519366dfe9223ee4a7a38e6e28c1c0d372b38ba3fe7/pyarrow-20.0.0-cp39-cp39-manylinux_2_28_x86_64.whl",hashes = {sha256 = "db53390eaf8a4dab4dbd6d93c85c5cf002db24902dbff0ca7d988beb5c9dd15b"}}, - {name = "pyarrow-20.0.0-cp39-cp39-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/07/d5/79effb32585b7c18897d3047a2163034f3f9c944d12f7b2fd8df6a2edc70/pyarrow-20.0.0-cp39-cp39-musllinux_1_2_aarch64.whl",hashes = {sha256 = "851c6a8260ad387caf82d2bbf54759130534723e37083111d4ed481cb253cc0d"}}, - {name = "pyarrow-20.0.0-cp39-cp39-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/09/5c/f707603552c058b2e9129732de99a67befb1f13f008cc58856304a62c38b/pyarrow-20.0.0-cp39-cp39-musllinux_1_2_x86_64.whl",hashes = {sha256 = "e22f80b97a271f0a7d9cd07394a7d348f80d3ac63ed7cc38b6d1b696ab3b2619"}}, - {name = "pyarrow-20.0.0-cp39-cp39-win_amd64.whl",url = "https://files.pythonhosted.org/packages/26/cc/1eb6a01c1bbc787f596c270c46bcd2273e35154a84afcb1d0cb4cc72457e/pyarrow-20.0.0-cp39-cp39-win_amd64.whl",hashes = {sha256 = "9965a050048ab02409fb7cbbefeedba04d3d67f2cc899eff505cc084345959ca"}}, ] marker = "\"default\" in dependency_groups" @@ -2941,11 +2553,6 @@ wheels = [ {name = "pycryptodomex-3.23.0-cp313-cp313t-win32.whl",url = "https://files.pythonhosted.org/packages/ca/18/4ca89ac737230b52ac8ffaca42f9c6f1fd07c81a6cd821e91af79db60632/pycryptodomex-3.23.0-cp313-cp313t-win32.whl",hashes = {sha256 = "a9d446e844f08299236780f2efa9898c818fe7e02f17263866b8550c7d5fb328"}}, {name = "pycryptodomex-3.23.0-cp313-cp313t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/73/34/13e01c322db027682e00986873eca803f11c56ade9ba5bbf3225841ea2d4/pycryptodomex-3.23.0-cp313-cp313t-win_amd64.whl",hashes = {sha256 = "bc65bdd9fc8de7a35a74cab1c898cab391a4add33a8fe740bda00f5976ca4708"}}, {name = "pycryptodomex-3.23.0-cp313-cp313t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/54/68/9504c8796b1805d58f4425002bcca20f12880e6fa4dc2fc9a668705c7a08/pycryptodomex-3.23.0-cp313-cp313t-win_arm64.whl",hashes = {sha256 = "c885da45e70139464f082018ac527fdaad26f1657a99ee13eecdce0f0ca24ab4"}}, - {name = "pycryptodomex-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl",url = "https://files.pythonhosted.org/packages/f3/b8/3e76d948c3c4ac71335bbe75dac53e154b40b0f8f1f022dfa295257a0c96/pycryptodomex-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl",hashes = {sha256 = "ebfff755c360d674306e5891c564a274a47953562b42fb74a5c25b8fc1fb1cb5"}}, - {name = "pycryptodomex-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/6a/cf/80f4297a4820dfdfd1c88cf6c4666a200f204b3488103d027b5edd9176ec/pycryptodomex-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "eca54f4bb349d45afc17e3011ed4264ef1cc9e266699874cdd1349c504e64798"}}, - {name = "pycryptodomex-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/d1/42/1e969ee0ad19fe3134b0e1b856c39bd0b70d47a4d0e81c2a8b05727394c9/pycryptodomex-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "4f2596e643d4365e14d0879dc5aafe6355616c61c2176009270f3048f6d9a61f"}}, - {name = "pycryptodomex-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/6e/c3/1de4f7631fea8a992a44ba632aa40e0008764c0fb9bf2854b0acf78c2cf2/pycryptodomex-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "fdfac7cda115bca3a5abb2f9e43bc2fb66c2b65ab074913643803ca7083a79ea"}}, - {name = "pycryptodomex-3.23.0-pp310-pypy310_pp73-win_amd64.whl",url = "https://files.pythonhosted.org/packages/f2/5f/af7da8e6f1e42b52f44a24d08b8e4c726207434e2593732d39e7af5e7256/pycryptodomex-3.23.0-pp310-pypy310_pp73-win_amd64.whl",hashes = {sha256 = "14c37aaece158d0ace436f76a7bb19093db3b4deade9797abfc39ec6cd6cc2fe"}}, {name = "pycryptodomex-3.23.0-cp37-abi3-macosx_10_9_universal2.whl",url = "https://files.pythonhosted.org/packages/dd/9c/1a8f35daa39784ed8adf93a694e7e5dc15c23c741bbda06e1d45f8979e9e/pycryptodomex-3.23.0-cp37-abi3-macosx_10_9_universal2.whl",hashes = {sha256 = "06698f957fe1ab229a99ba2defeeae1c09af185baa909a31a5d1f9d42b1aaed6"}}, {name = "pycryptodomex-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/7a/62/f5221a191a97157d240cf6643747558759126c76ee92f29a3f4aee3197a5/pycryptodomex-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl",hashes = {sha256 = "b2c2537863eccef2d41061e82a881dcabb04944c5c06c5aa7110b577cc487545"}}, {name = "pycryptodomex-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/8c/fd/5a054543c8988d4ed7b612721d7e78a4b9bf36bc3c5ad45ef45c22d0060e/pycryptodomex-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "43c446e2ba8df8889e0e16f02211c25b4934898384c1ec1ec04d7889c0333587"}}, @@ -3018,6 +2625,55 @@ marker = "\"default\" in dependency_groups" [packages.tool.pdm] dependencies = [] +[[packages]] +name = "sanic-routing" +version = "23.12.0" +sdist = {name = "sanic-routing-23.12.0.tar.gz", url = "https://files.pythonhosted.org/packages/d1/5c/2a7edd14fbccca3719a8d680951d4b25f986752c781c61ccf156a6d1ebff/sanic-routing-23.12.0.tar.gz", hashes = {sha256 = "1dcadc62c443e48c852392dba03603f9862b6197fc4cba5bbefeb1ace0848b04"}} +wheels = [ + {name = "sanic_routing-23.12.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/cf/e3/3425c9a8773807ac2c01d6a56c8521733f09b627e5827e733c5cd36b9ac5/sanic_routing-23.12.0-py3-none-any.whl",hashes = {sha256 = "1558a72afcb9046ed3134a5edae02fc1552cff08f0fff2e8d5de0877ea43ed73"}}, +] +marker = "\"default\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [] + +[[packages]] +name = "scikit-learn" +version = "1.7.2" +requires-python = ">=3.10" +sdist = {name = "scikit_learn-1.7.2.tar.gz", url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hashes = {sha256 = "20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda"}} +wheels = [ + {name = "scikit_learn-1.7.2-cp314-cp314-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/d9/82/dee5acf66837852e8e68df6d8d3a6cb22d3df997b733b032f513d95205b7/scikit_learn-1.7.2-cp314-cp314-macosx_10_13_x86_64.whl",hashes = {sha256 = "fa8f63940e29c82d1e67a45d5297bdebbcb585f5a5a50c4914cc2e852ab77f33"}}, + {name = "scikit_learn-1.7.2-cp314-cp314-macosx_12_0_arm64.whl",url = "https://files.pythonhosted.org/packages/3c/30/9029e54e17b87cb7d50d51a5926429c683d5b4c1732f0507a6c3bed9bf65/scikit_learn-1.7.2-cp314-cp314-macosx_12_0_arm64.whl",hashes = {sha256 = "f95dc55b7902b91331fa4e5845dd5bde0580c9cd9612b1b2791b7e80c3d32615"}}, + {name = "scikit_learn-1.7.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/60/18/4a52c635c71b536879f4b971c2cedf32c35ee78f48367885ed8025d1f7ee/scikit_learn-1.7.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "9656e4a53e54578ad10a434dc1f993330568cfee176dff07112b8785fb413106"}}, + {name = "scikit_learn-1.7.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/99/7e/290362f6ab582128c53445458a5befd471ed1ea37953d5bcf80604619250/scikit_learn-1.7.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "96dc05a854add0e50d3f47a1ef21a10a595016da5b007c7d9cd9d0bffd1fcc61"}}, + {name = "scikit_learn-1.7.2-cp314-cp314-win_amd64.whl",url = "https://files.pythonhosted.org/packages/8e/87/24f541b6d62b1794939ae6422f8023703bbf6900378b2b34e0b4384dfefd/scikit_learn-1.7.2-cp314-cp314-win_amd64.whl",hashes = {sha256 = "bb24510ed3f9f61476181e4db51ce801e2ba37541def12dc9333b946fc7a9cf8"}}, + {name = "scikit_learn-1.7.2-cp313-cp313-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/ae/93/a3038cb0293037fd335f77f31fe053b89c72f17b1c8908c576c29d953e84/scikit_learn-1.7.2-cp313-cp313-macosx_10_13_x86_64.whl",hashes = {sha256 = "0b7dacaa05e5d76759fb071558a8b5130f4845166d88654a0f9bdf3eb57851b7"}}, + {name = "scikit_learn-1.7.2-cp313-cp313-macosx_12_0_arm64.whl",url = "https://files.pythonhosted.org/packages/40/dd/9a88879b0c1104259136146e4742026b52df8540c39fec21a6383f8292c7/scikit_learn-1.7.2-cp313-cp313-macosx_12_0_arm64.whl",hashes = {sha256 = "abebbd61ad9e1deed54cca45caea8ad5f79e1b93173dece40bb8e0c658dbe6fe"}}, + {name = "scikit_learn-1.7.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/46/af/c5e286471b7d10871b811b72ae794ac5fe2989c0a2df07f0ec723030f5f5/scikit_learn-1.7.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "502c18e39849c0ea1a5d681af1dbcf15f6cce601aebb657aabbfe84133c1907f"}}, + {name = "scikit_learn-1.7.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/f1/fd/df59faa53312d585023b2da27e866524ffb8faf87a68516c23896c718320/scikit_learn-1.7.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "7a4c328a71785382fe3fe676a9ecf2c86189249beff90bf85e22bdb7efaf9ae0"}}, + {name = "scikit_learn-1.7.2-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/a7/c7/03000262759d7b6f38c836ff9d512f438a70d8a8ddae68ee80de72dcfb63/scikit_learn-1.7.2-cp313-cp313-win_amd64.whl",hashes = {sha256 = "63a9afd6f7b229aad94618c01c252ce9e6fa97918c5ca19c9a17a087d819440c"}}, + {name = "scikit_learn-1.7.2-cp313-cp313t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/55/87/ef5eb1f267084532c8e4aef98a28b6ffe7425acbfd64b5e2f2e066bc29b3/scikit_learn-1.7.2-cp313-cp313t-macosx_10_13_x86_64.whl",hashes = {sha256 = "9acb6c5e867447b4e1390930e3944a005e2cb115922e693c08a323421a6966e8"}}, + {name = "scikit_learn-1.7.2-cp313-cp313t-macosx_12_0_arm64.whl",url = "https://files.pythonhosted.org/packages/93/f8/6c1e3fc14b10118068d7938878a9f3f4e6d7b74a8ddb1e5bed65159ccda8/scikit_learn-1.7.2-cp313-cp313t-macosx_12_0_arm64.whl",hashes = {sha256 = "2a41e2a0ef45063e654152ec9d8bcfc39f7afce35b08902bfe290c2498a67a6a"}}, + {name = "scikit_learn-1.7.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/83/87/066cafc896ee540c34becf95d30375fe5cbe93c3b75a0ee9aa852cd60021/scikit_learn-1.7.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "98335fb98509b73385b3ab2bd0639b1f610541d3988ee675c670371d6a87aa7c"}}, + {name = "scikit_learn-1.7.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/9c/2b/4903e1ccafa1f6453b1ab78413938c8800633988c838aa0be386cbb33072/scikit_learn-1.7.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "191e5550980d45449126e23ed1d5e9e24b2c68329ee1f691a3987476e115e09c"}}, + {name = "scikit_learn-1.7.2-cp313-cp313t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/b5/aa/8444be3cfb10451617ff9d177b3c190288f4563e6c50ff02728be67ad094/scikit_learn-1.7.2-cp313-cp313t-win_amd64.whl",hashes = {sha256 = "57dc4deb1d3762c75d685507fbd0bc17160144b2f2ba4ccea5dc285ab0d0e973"}}, + {name = "scikit_learn-1.7.2-cp312-cp312-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/a7/aa/3996e2196075689afb9fce0410ebdb4a09099d7964d061d7213700204409/scikit_learn-1.7.2-cp312-cp312-macosx_10_13_x86_64.whl",hashes = {sha256 = "8d91a97fa2b706943822398ab943cde71858a50245e31bc71dba62aab1d60a96"}}, + {name = "scikit_learn-1.7.2-cp312-cp312-macosx_12_0_arm64.whl",url = "https://files.pythonhosted.org/packages/43/5d/779320063e88af9c4a7c2cf463ff11c21ac9c8bd730c4a294b0000b666c9/scikit_learn-1.7.2-cp312-cp312-macosx_12_0_arm64.whl",hashes = {sha256 = "acbc0f5fd2edd3432a22c69bed78e837c70cf896cd7993d71d51ba6708507476"}}, + {name = "scikit_learn-1.7.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/5c/d0/0c577d9325b05594fdd33aa970bf53fb673f051a45496842caee13cfd7fe/scikit_learn-1.7.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "e5bf3d930aee75a65478df91ac1225ff89cd28e9ac7bd1196853a9229b6adb0b"}}, + {name = "scikit_learn-1.7.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/82/70/8bf44b933837ba8494ca0fc9a9ab60f1c13b062ad0197f60a56e2fc4c43e/scikit_learn-1.7.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "b4d6e9deed1a47aca9fe2f267ab8e8fe82ee20b4526b2c0cd9e135cea10feb44"}}, + {name = "scikit_learn-1.7.2-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/c6/99/ed35197a158f1fdc2fe7c3680e9c70d0128f662e1fee4ed495f4b5e13db0/scikit_learn-1.7.2-cp312-cp312-win_amd64.whl",hashes = {sha256 = "6088aa475f0785e01bcf8529f55280a3d7d298679f50c0bb70a2364a82d0b290"}}, +] +marker = "python_version ~= \"3.12\"" + +[packages.tool.pdm] +dependencies = [ + "numpy>=1.22.0", + "scipy>=1.8.0", + "joblib>=1.2.0", + "threadpoolctl>=3.1.0", +] + [[packages]] name = "snowballstemmer" version = "3.0.1" @@ -3031,6 +2687,111 @@ marker = "\"dev\" in extras" [packages.tool.pdm] dependencies = [] +[[packages]] +name = "soundfile" +version = "0.13.1" +sdist = {name = "soundfile-0.13.1.tar.gz", url = "https://files.pythonhosted.org/packages/e1/41/9b873a8c055582859b239be17902a85339bec6a30ad162f98c9b0288a2cc/soundfile-0.13.1.tar.gz", hashes = {sha256 = "b2c68dab1e30297317080a5b43df57e302584c49e2942defdde0acccc53f0e5b"}} +wheels = [ + {name = "soundfile-0.13.1-py2.py3-none-any.whl",url = "https://files.pythonhosted.org/packages/64/28/e2a36573ccbcf3d57c00626a21fe51989380636e821b341d36ccca0c1c3a/soundfile-0.13.1-py2.py3-none-any.whl",hashes = {sha256 = "a23c717560da2cf4c7b5ae1142514e0fd82d6bbd9dfc93a50423447142f2c445"}}, + {name = "soundfile-0.13.1-py2.py3-none-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/ea/ab/73e97a5b3cc46bba7ff8650a1504348fa1863a6f9d57d7001c6b67c5f20e/soundfile-0.13.1-py2.py3-none-macosx_10_9_x86_64.whl",hashes = {sha256 = "82dc664d19831933fe59adad199bf3945ad06d84bc111a5b4c0d3089a5b9ec33"}}, + {name = "soundfile-0.13.1-py2.py3-none-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/a0/e5/58fd1a8d7b26fc113af244f966ee3aecf03cb9293cb935daaddc1e455e18/soundfile-0.13.1-py2.py3-none-macosx_11_0_arm64.whl",hashes = {sha256 = "743f12c12c4054921e15736c6be09ac26b3b3d603aef6fd69f9dde68748f2593"}}, + {name = "soundfile-0.13.1-py2.py3-none-manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/58/ae/c0e4a53d77cf6e9a04179535766b3321b0b9ced5f70522e4caf9329f0046/soundfile-0.13.1-py2.py3-none-manylinux_2_28_aarch64.whl",hashes = {sha256 = "9c9e855f5a4d06ce4213f31918653ab7de0c5a8d8107cd2427e44b42df547deb"}}, + {name = "soundfile-0.13.1-py2.py3-none-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/57/5e/70bdd9579b35003a489fc850b5047beeda26328053ebadc1fb60f320f7db/soundfile-0.13.1-py2.py3-none-manylinux_2_28_x86_64.whl",hashes = {sha256 = "03267c4e493315294834a0870f31dbb3b28a95561b80b134f0bd3cf2d5f0e618"}}, + {name = "soundfile-0.13.1-py2.py3-none-win32.whl",url = "https://files.pythonhosted.org/packages/fe/df/8c11dc4dfceda14e3003bb81a0d0edcaaf0796dd7b4f826ea3e532146bba/soundfile-0.13.1-py2.py3-none-win32.whl",hashes = {sha256 = "c734564fab7c5ddf8e9be5bf70bab68042cd17e9c214c06e365e20d64f9a69d5"}}, + {name = "soundfile-0.13.1-py2.py3-none-win_amd64.whl",url = "https://files.pythonhosted.org/packages/14/e9/6b761de83277f2f02ded7e7ea6f07828ec78e4b229b80e4ca55dd205b9dc/soundfile-0.13.1-py2.py3-none-win_amd64.whl",hashes = {sha256 = "1e70a05a0626524a69e9f0f4dd2ec174b4e9567f4d8b6c11d38b5c289be36ee9"}}, +] +marker = "\"default\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [ + "cffi>=1.0", + "numpy", +] + +[[packages]] +name = "cffi" +version = "2.0.0" +requires-python = ">=3.9" +sdist = {name = "cffi-2.0.0.tar.gz", url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hashes = {sha256 = "44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}} +wheels = [ + {name = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl",hashes = {sha256 = "fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"}}, + {name = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"}}, + {name = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"}}, + {name = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl",url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl",hashes = {sha256 = "12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"}}, + {name = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl",url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl",hashes = {sha256 = "d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"}}, + {name = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"}}, + {name = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl",hashes = {sha256 = "737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"}}, + {name = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl",hashes = {sha256 = "38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"}}, + {name = "cffi-2.0.0-cp314-cp314-win32.whl",url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl",hashes = {sha256 = "087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"}}, + {name = "cffi-2.0.0-cp314-cp314-win_amd64.whl",url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl",hashes = {sha256 = "203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"}}, + {name = "cffi-2.0.0-cp314-cp314-win_arm64.whl",url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl",hashes = {sha256 = "dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"}}, + {name = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl",hashes = {sha256 = "9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"}}, + {name = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl",hashes = {sha256 = "7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"}}, + {name = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"}}, + {name = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl",url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl",hashes = {sha256 = "92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"}}, + {name = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl",url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl",hashes = {sha256 = "b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"}}, + {name = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"}}, + {name = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"}}, + {name = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"}}, + {name = "cffi-2.0.0-cp314-cp314t-win32.whl",url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl",hashes = {sha256 = "1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"}}, + {name = "cffi-2.0.0-cp314-cp314t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl",hashes = {sha256 = "d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"}}, + {name = "cffi-2.0.0-cp314-cp314t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl",hashes = {sha256 = "0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"}}, + {name = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl",hashes = {sha256 = "00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"}}, + {name = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"}}, + {name = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"}}, + {name = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"}}, + {name = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl",url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl",hashes = {sha256 = "f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"}}, + {name = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl",url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl",hashes = {sha256 = "dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"}}, + {name = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"}}, + {name = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"}}, + {name = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"}}, + {name = "cffi-2.0.0-cp313-cp313-win32.whl",url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl",hashes = {sha256 = "74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"}}, + {name = "cffi-2.0.0-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl",hashes = {sha256 = "19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"}}, + {name = "cffi-2.0.0-cp313-cp313-win_arm64.whl",url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl",hashes = {sha256 = "256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"}}, + {name = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl",hashes = {sha256 = "6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"}}, + {name = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"}}, + {name = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"}}, + {name = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"}}, + {name = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl",url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl",hashes = {sha256 = "1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"}}, + {name = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl",url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl",hashes = {sha256 = "81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"}}, + {name = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"}}, + {name = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"}}, + {name = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"}}, + {name = "cffi-2.0.0-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl",hashes = {sha256 = "da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"}}, + {name = "cffi-2.0.0-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl",hashes = {sha256 = "da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"}}, + {name = "cffi-2.0.0-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl",hashes = {sha256 = "4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"}}, +] +marker = "\"default\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [ + "pycparser; implementation_name != \"PyPy\"", +] + +[[packages]] +name = "soxr" +version = "1.0.0" +requires-python = ">=3.9" +sdist = {name = "soxr-1.0.0.tar.gz", url = "https://files.pythonhosted.org/packages/42/7e/f4b461944662ad75036df65277d6130f9411002bfb79e9df7dff40a31db9/soxr-1.0.0.tar.gz", hashes = {sha256 = "e07ee6c1d659bc6957034f4800c60cb8b98de798823e34d2a2bba1caa85a4509"}} +wheels = [ + {name = "soxr-1.0.0-cp314-cp314t-macosx_10_14_x86_64.whl",url = "https://files.pythonhosted.org/packages/99/77/d3b3c25b4f1b1aa4a73f669355edcaee7a52179d0c50407697200a0e55b9/soxr-1.0.0-cp314-cp314t-macosx_10_14_x86_64.whl",hashes = {sha256 = "392a5c70c04eb939c9c176bd6f654dec9a0eaa9ba33d8f1024ed63cf68cdba0a"}}, + {name = "soxr-1.0.0-cp314-cp314t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/8a/ee/3ca73e18781bb2aff92b809f1c17c356dfb9a1870652004bd432e79afbfa/soxr-1.0.0-cp314-cp314t-macosx_11_0_arm64.whl",hashes = {sha256 = "fdc41a1027ba46777186f26a8fba7893be913383414135577522da2fcc684490"}}, + {name = "soxr-1.0.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/bd/f0/eea8b5f587a2531657dc5081d2543a5a845f271a3bea1c0fdee5cebde021/soxr-1.0.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "449acd1dfaf10f0ce6dfd75c7e2ef984890df94008765a6742dafb42061c1a24"}}, + {name = "soxr-1.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/64/59/2430a48c705565eb09e78346950b586f253a11bd5313426ced3ecd9b0feb/soxr-1.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "38b35c99e408b8f440c9376a5e1dd48014857cd977c117bdaa4304865ae0edd0"}}, + {name = "soxr-1.0.0-cp314-cp314t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/3c/1b/f84a2570a74094e921bbad5450b2a22a85d58585916e131d9b98029c3e69/soxr-1.0.0-cp314-cp314t-win_amd64.whl",hashes = {sha256 = "a39b519acca2364aa726b24a6fd55acf29e4c8909102e0b858c23013c38328e5"}}, + {name = "soxr-1.0.0-cp312-abi3-macosx_10_14_x86_64.whl",url = "https://files.pythonhosted.org/packages/c5/c7/f92b81f1a151c13afb114f57799b86da9330bec844ea5a0d3fe6a8732678/soxr-1.0.0-cp312-abi3-macosx_10_14_x86_64.whl",hashes = {sha256 = "abecf4e39017f3fadb5e051637c272ae5778d838e5c3926a35db36a53e3a607f"}}, + {name = "soxr-1.0.0-cp312-abi3-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/ff/1d/c945fea9d83ea1f2be9d116b3674dbaef26ed090374a77c394b31e3b083b/soxr-1.0.0-cp312-abi3-macosx_11_0_arm64.whl",hashes = {sha256 = "e973d487ee46aa8023ca00a139db6e09af053a37a032fe22f9ff0cc2e19c94b4"}}, + {name = "soxr-1.0.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/b5/80/10640970998a1d2199bef6c4d92205f36968cddaf3e4d0e9fe35ddd405bd/soxr-1.0.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "e8ce273cca101aff3d8c387db5a5a41001ba76ef1837883438d3c652507a9ccc"}}, + {name = "soxr-1.0.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/b1/87/2726603c13c2126cb8ded9e57381b7377f4f0df6ba4408e1af5ddbfdc3dd/soxr-1.0.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "e8f2a69686f2856d37823bbb7b78c3d44904f311fe70ba49b893af11d6b6047b"}}, + {name = "soxr-1.0.0-cp312-abi3-win_amd64.whl",url = "https://files.pythonhosted.org/packages/ce/04/530252227f4d0721a5524a936336485dfb429bb206a66baf8e470384f4a2/soxr-1.0.0-cp312-abi3-win_amd64.whl",hashes = {sha256 = "2a3b77b115ae7c478eecdbd060ed4f61beda542dfb70639177ac263aceda42a2"}}, +] +marker = "\"default\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [ + "numpy", +] + [[packages]] name = "sphinxcontrib-htmlhelp" version = "2.1.0" @@ -3057,6 +2818,116 @@ marker = "\"dev\" in extras" [packages.tool.pdm] dependencies = [] +[[packages]] +name = "sympy" +version = "1.14.0" +requires-python = ">=3.9" +sdist = {name = "sympy-1.14.0.tar.gz", url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hashes = {sha256 = "d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517"}} +wheels = [ + {name = "sympy-1.14.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl",hashes = {sha256 = "e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5"}}, +] +marker = "\"default\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [ + "mpmath<1.4,>=1.1.0", +] + +[[packages]] +name = "mpmath" +version = "1.3.0" +sdist = {name = "mpmath-1.3.0.tar.gz", url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hashes = {sha256 = "7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f"}} +wheels = [ + {name = "mpmath-1.3.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl",hashes = {sha256 = "a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"}}, +] +marker = "\"default\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [] + +[[packages]] +name = "threadpoolctl" +version = "3.6.0" +requires-python = ">=3.9" +sdist = {name = "threadpoolctl-3.6.0.tar.gz", url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hashes = {sha256 = "8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e"}} +wheels = [ + {name = "threadpoolctl-3.6.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl",hashes = {sha256 = "43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb"}}, +] +marker = "\"default\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [] + +[[packages]] +name = "tracerite" +version = "1.1.3" +sdist = {name = "tracerite-1.1.3.tar.gz", url = "https://files.pythonhosted.org/packages/27/b2/37b825b881f23bc56384c3142214ccbe5d9de7e7c5fe3d155fa032738b98/tracerite-1.1.3.tar.gz", hashes = {sha256 = "119fc006f240aa03fffb41cf99cf82fda5c0449c7d4b6fe42c6340403578b31e"}} +wheels = [ + {name = "tracerite-1.1.3-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/e6/bf/c65d36ec5a93048dd55b3247be26059970daad72263e35ecace2f3188b2c/tracerite-1.1.3-py3-none-any.whl",hashes = {sha256 = "811d8e2e0fb563b77340eebe2e9f7b324acfe01e09ea58db8bcaecb24327c823"}}, +] +marker = "\"default\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [ + "html5tagger>=1.2.1", +] + +[[packages]] +name = "ujson" +version = "5.11.0" +requires-python = ">=3.9" +sdist = {name = "ujson-5.11.0.tar.gz", url = "https://files.pythonhosted.org/packages/43/d9/3f17e3c5773fb4941c68d9a37a47b1a79c9649d6c56aefbed87cc409d18a/ujson-5.11.0.tar.gz", hashes = {sha256 = "e204ae6f909f099ba6b6b942131cee359ddda2b6e4ea39c12eb8b991fe2010e0"}} +wheels = [ + {name = "ujson-5.11.0-cp314-cp314-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/28/08/4518146f4984d112764b1dfa6fb7bad691c44a401adadaa5e23ccd930053/ujson-5.11.0-cp314-cp314-macosx_10_13_x86_64.whl",hashes = {sha256 = "65724738c73645db88f70ba1f2e6fb678f913281804d5da2fd02c8c5839af302"}}, + {name = "ujson-5.11.0-cp314-cp314-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/29/37/2107b9a62168867a692654d8766b81bd2fd1e1ba13e2ec90555861e02b0c/ujson-5.11.0-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "29113c003ca33ab71b1b480bde952fbab2a0b6b03a4ee4c3d71687cdcbd1a29d"}}, + {name = "ujson-5.11.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/9b/f8/25583c70f83788edbe3ca62ce6c1b79eff465d78dec5eb2b2b56b3e98b33/ujson-5.11.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "c44c703842024d796b4c78542a6fcd5c3cb948b9fc2a73ee65b9c86a22ee3638"}}, + {name = "ujson-5.11.0-cp314-cp314-manylinux_2_24_i686.manylinux_2_28_i686.whl",url = "https://files.pythonhosted.org/packages/ed/ca/19b3a632933a09d696f10dc1b0dfa1d692e65ad507d12340116ce4f67967/ujson-5.11.0-cp314-cp314-manylinux_2_24_i686.manylinux_2_28_i686.whl",hashes = {sha256 = "e750c436fb90edf85585f5c62a35b35082502383840962c6983403d1bd96a02c"}}, + {name = "ujson-5.11.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/55/7a/4572af5324ad4b2bfdd2321e898a527050290147b4ea337a79a0e4e87ec7/ujson-5.11.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "f278b31a7c52eb0947b2db55a5133fbc46b6f0ef49972cd1a80843b72e135aba"}}, + {name = "ujson-5.11.0-cp314-cp314-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/7b/71/a2b8c19cf4e1efe53cf439cdf7198ac60ae15471d2f1040b490c1f0f831f/ujson-5.11.0-cp314-cp314-musllinux_1_2_aarch64.whl",hashes = {sha256 = "ab2cb8351d976e788669c8281465d44d4e94413718af497b4e7342d7b2f78018"}}, + {name = "ujson-5.11.0-cp314-cp314-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/7a/3e/7b98668cba3bb3735929c31b999b374ebc02c19dfa98dfebaeeb5c8597ca/ujson-5.11.0-cp314-cp314-musllinux_1_2_i686.whl",hashes = {sha256 = "090b4d11b380ae25453100b722d0609d5051ffe98f80ec52853ccf8249dfd840"}}, + {name = "ujson-5.11.0-cp314-cp314-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/a1/ea/8870f208c20b43571a5c409ebb2fe9b9dba5f494e9e60f9314ac01ea8f78/ujson-5.11.0-cp314-cp314-musllinux_1_2_x86_64.whl",hashes = {sha256 = "80017e870d882d5517d28995b62e4e518a894f932f1e242cbc802a2fd64d365c"}}, + {name = "ujson-5.11.0-cp314-cp314-win32.whl",url = "https://files.pythonhosted.org/packages/63/b6/c0e6607e37fa47929920a685a968c6b990a802dec65e9c5181e97845985d/ujson-5.11.0-cp314-cp314-win32.whl",hashes = {sha256 = "1d663b96eb34c93392e9caae19c099ec4133ba21654b081956613327f0e973ac"}}, + {name = "ujson-5.11.0-cp314-cp314-win_amd64.whl",url = "https://files.pythonhosted.org/packages/4e/56/f4fe86b4c9000affd63e9219e59b222dc48b01c534533093e798bf617a7e/ujson-5.11.0-cp314-cp314-win_amd64.whl",hashes = {sha256 = "849e65b696f0d242833f1df4182096cedc50d414215d1371fca85c541fbff629"}}, + {name = "ujson-5.11.0-cp314-cp314-win_arm64.whl",url = "https://files.pythonhosted.org/packages/0a/f3/669437f0280308db4783b12a6d88c00730b394327d8334cc7a32ef218e64/ujson-5.11.0-cp314-cp314-win_arm64.whl",hashes = {sha256 = "e73df8648c9470af2b6a6bf5250d4744ad2cf3d774dcf8c6e31f018bdd04d764"}}, + {name = "ujson-5.11.0-cp314-cp314t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/6e/cd/e9809b064a89fe5c4184649adeb13c1b98652db3f8518980b04227358574/ujson-5.11.0-cp314-cp314t-macosx_10_13_x86_64.whl",hashes = {sha256 = "de6e88f62796372fba1de973c11138f197d3e0e1d80bcb2b8aae1e826096d433"}}, + {name = "ujson-5.11.0-cp314-cp314t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/1b/be/ae26a6321179ebbb3a2e2685b9007c71bcda41ad7a77bbbe164005e956fc/ujson-5.11.0-cp314-cp314t-macosx_11_0_arm64.whl",hashes = {sha256 = "49e56ef8066f11b80d620985ae36869a3ff7e4b74c3b6129182ec5d1df0255f3"}}, + {name = "ujson-5.11.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/ae/e9/fb4a220ee6939db099f4cfeeae796ecb91e7584ad4d445d4ca7f994a9135/ujson-5.11.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "1a325fd2c3a056cf6c8e023f74a0c478dd282a93141356ae7f16d5309f5ff823"}}, + {name = "ujson-5.11.0-cp314-cp314t-manylinux_2_24_i686.manylinux_2_28_i686.whl",url = "https://files.pythonhosted.org/packages/bd/f8/fc4b952b8f5fea09ea3397a0bd0ad019e474b204cabcb947cead5d4d1ffc/ujson-5.11.0-cp314-cp314t-manylinux_2_24_i686.manylinux_2_28_i686.whl",hashes = {sha256 = "a0af6574fc1d9d53f4ff371f58c96673e6d988ed2b5bf666a6143c782fa007e9"}}, + {name = "ujson-5.11.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/2e/e5/af5491dfda4f8b77e24cf3da68ee0d1552f99a13e5c622f4cef1380925c3/ujson-5.11.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "10f29e71ecf4ecd93a6610bd8efa8e7b6467454a363c3d6416db65de883eb076"}}, + {name = "ujson-5.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/c4/09/0945349dd41f25cc8c38d78ace49f14c5052c5bbb7257d2f466fa7bdb533/ujson-5.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "1a0a9b76a89827a592656fe12e000cf4f12da9692f51a841a4a07aa4c7ecc41c"}}, + {name = "ujson-5.11.0-cp314-cp314t-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/49/44/8e04496acb3d5a1cbee3a54828d9652f67a37523efa3d3b18a347339680a/ujson-5.11.0-cp314-cp314t-musllinux_1_2_i686.whl",hashes = {sha256 = "b16930f6a0753cdc7d637b33b4e8f10d5e351e1fb83872ba6375f1e87be39746"}}, + {name = "ujson-5.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/64/ae/4bc825860d679a0f208a19af2f39206dfd804ace2403330fdc3170334a2f/ujson-5.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "04c41afc195fd477a59db3a84d5b83a871bd648ef371cf8c6f43072d89144eef"}}, + {name = "ujson-5.11.0-cp314-cp314t-win32.whl",url = "https://files.pythonhosted.org/packages/30/ed/5a057199fb0a5deabe0957073a1c1c1c02a3e99476cd03daee98ea21fa57/ujson-5.11.0-cp314-cp314t-win32.whl",hashes = {sha256 = "aa6d7a5e09217ff93234e050e3e380da62b084e26b9f2e277d2606406a2fc2e5"}}, + {name = "ujson-5.11.0-cp314-cp314t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/aa/03/b19c6176bdf1dc13ed84b886e99677a52764861b6cc023d5e7b6ebda249d/ujson-5.11.0-cp314-cp314t-win_amd64.whl",hashes = {sha256 = "48055e1061c1bb1f79e75b4ac39e821f3f35a9b82de17fce92c3140149009bec"}}, + {name = "ujson-5.11.0-cp314-cp314t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/5d/ca/a0413a3874b2dc1708b8796ca895bf363292f9c70b2e8ca482b7dbc0259d/ujson-5.11.0-cp314-cp314t-win_arm64.whl",hashes = {sha256 = "1194b943e951092db611011cb8dbdb6cf94a3b816ed07906e14d3bc6ce0e90ab"}}, + {name = "ujson-5.11.0-cp313-cp313-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/1c/ec/2de9dd371d52c377abc05d2b725645326c4562fc87296a8907c7bcdf2db7/ujson-5.11.0-cp313-cp313-macosx_10_13_x86_64.whl",hashes = {sha256 = "109f59885041b14ee9569bf0bb3f98579c3fa0652317b355669939e5fc5ede53"}}, + {name = "ujson-5.11.0-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/5b/a4/f611f816eac3a581d8a4372f6967c3ed41eddbae4008d1d77f223f1a4e0a/ujson-5.11.0-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "a31c6b8004438e8c20fc55ac1c0e07dad42941db24176fe9acf2815971f8e752"}}, + {name = "ujson-5.11.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/e9/c5/c161940967184de96f5cbbbcce45b562a4bf851d60f4c677704b1770136d/ujson-5.11.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "78c684fb21255b9b90320ba7e199780f653e03f6c2528663768965f4126a5b50"}}, + {name = "ujson-5.11.0-cp313-cp313-manylinux_2_24_i686.manylinux_2_28_i686.whl",url = "https://files.pythonhosted.org/packages/2b/d6/c7b2444238f5b2e2d0e3dab300b9ddc3606e4b1f0e4bed5a48157cebc792/ujson-5.11.0-cp313-cp313-manylinux_2_24_i686.manylinux_2_28_i686.whl",hashes = {sha256 = "4c9f5d6a27d035dd90a146f7761c2272cf7103de5127c9ab9c4cd39ea61e878a"}}, + {name = "ujson-5.11.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/fe/a3/292551f936d3d02d9af148f53e1bc04306b00a7cf1fcbb86fa0d1c887242/ujson-5.11.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "837da4d27fed5fdc1b630bd18f519744b23a0b5ada1bbde1a36ba463f2900c03"}}, + {name = "ujson-5.11.0-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/90/a6/82cfa70448831b1a9e73f882225980b5c689bf539ec6400b31656a60ea46/ujson-5.11.0-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "787aff4a84da301b7f3bac09bc696e2e5670df829c6f8ecf39916b4e7e24e701"}}, + {name = "ujson-5.11.0-cp313-cp313-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/84/5c/96e2266be50f21e9b27acaee8ca8f23ea0b85cb998c33d4f53147687839b/ujson-5.11.0-cp313-cp313-musllinux_1_2_i686.whl",hashes = {sha256 = "6dd703c3e86dc6f7044c5ac0b3ae079ed96bf297974598116aa5fb7f655c3a60"}}, + {name = "ujson-5.11.0-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/8d/20/78abe3d808cf3bb3e76f71fca46cd208317bf461c905d79f0d26b9df20f1/ujson-5.11.0-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "3772e4fe6b0c1e025ba3c50841a0ca4786825a4894c8411bf8d3afe3a8061328"}}, + {name = "ujson-5.11.0-cp313-cp313-win32.whl",url = "https://files.pythonhosted.org/packages/d8/50/8856e24bec5e2fc7f775d867aeb7a3f137359356200ac44658f1f2c834b2/ujson-5.11.0-cp313-cp313-win32.whl",hashes = {sha256 = "8fa2af7c1459204b7a42e98263b069bd535ea0cd978b4d6982f35af5a04a4241"}}, + {name = "ujson-5.11.0-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/5b/d8/1baee0f4179a4d0f5ce086832147b6cc9b7731c24ca08e14a3fdb8d39c32/ujson-5.11.0-cp313-cp313-win_amd64.whl",hashes = {sha256 = "34032aeca4510a7c7102bd5933f59a37f63891f30a0706fb46487ab6f0edf8f0"}}, + {name = "ujson-5.11.0-cp313-cp313-win_arm64.whl",url = "https://files.pythonhosted.org/packages/a9/8c/6d85ef5be82c6d66adced3ec5ef23353ed710a11f70b0b6a836878396334/ujson-5.11.0-cp313-cp313-win_arm64.whl",hashes = {sha256 = "ce076f2df2e1aa62b685086fbad67f2b1d3048369664b4cdccc50707325401f9"}}, + {name = "ujson-5.11.0-cp312-cp312-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/b9/ef/a9cb1fce38f699123ff012161599fb9f2ff3f8d482b4b18c43a2dc35073f/ujson-5.11.0-cp312-cp312-macosx_10_13_x86_64.whl",hashes = {sha256 = "7895f0d2d53bd6aea11743bd56e3cb82d729980636cd0ed9b89418bf66591702"}}, + {name = "ujson-5.11.0-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/b1/05/dba51a00eb30bd947791b173766cbed3492269c150a7771d2750000c965f/ujson-5.11.0-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "12b5e7e22a1fe01058000d1b317d3b65cc3daf61bd2ea7a2b76721fe160fa74d"}}, + {name = "ujson-5.11.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/03/3c/fd11a224f73fbffa299fb9644e425f38b38b30231f7923a088dd513aabb4/ujson-5.11.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "0180a480a7d099082501cad1fe85252e4d4bf926b40960fb3d9e87a3a6fbbc80"}}, + {name = "ujson-5.11.0-cp312-cp312-manylinux_2_24_i686.manylinux_2_28_i686.whl",url = "https://files.pythonhosted.org/packages/55/b9/405103cae24899df688a3431c776e00528bd4799e7d68820e7ebcf824f92/ujson-5.11.0-cp312-cp312-manylinux_2_24_i686.manylinux_2_28_i686.whl",hashes = {sha256 = "fa79fdb47701942c2132a9dd2297a1a85941d966d8c87bfd9e29b0cf423f26cc"}}, + {name = "ujson-5.11.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/17/7b/2dcbc2bbfdbf68f2368fb21ab0f6735e872290bb604c75f6e06b81edcb3f/ujson-5.11.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "8254e858437c00f17cb72e7a644fc42dad0ebb21ea981b71df6e84b1072aaa7c"}}, + {name = "ujson-5.11.0-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/d1/71/fea2ca18986a366c750767b694430d5ded6b20b6985fddca72f74af38a4c/ujson-5.11.0-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "1aa8a2ab482f09f6c10fba37112af5f957689a79ea598399c85009f2f29898b5"}}, + {name = "ujson-5.11.0-cp312-cp312-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/a3/bb/d4220bd7532eac6288d8115db51710fa2d7d271250797b0bfba9f1e755af/ujson-5.11.0-cp312-cp312-musllinux_1_2_i686.whl",hashes = {sha256 = "a638425d3c6eed0318df663df44480f4a40dc87cc7c6da44d221418312f6413b"}}, + {name = "ujson-5.11.0-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/80/47/226e540aa38878ce1194454385701d82df538ccb5ff8db2cf1641dde849a/ujson-5.11.0-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "7e3cff632c1d78023b15f7e3a81c3745cd3f94c044d1e8fa8efbd6b161997bbc"}}, + {name = "ujson-5.11.0-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/7e/81/546042f0b23c9040d61d46ea5ca76f0cc5e0d399180ddfb2ae976ebff5b5/ujson-5.11.0-cp312-cp312-win32.whl",hashes = {sha256 = "be6b0eaf92cae8cdee4d4c9e074bde43ef1c590ed5ba037ea26c9632fb479c88"}}, + {name = "ujson-5.11.0-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/44/1b/27c05dc8c9728f44875d74b5bfa948ce91f6c33349232619279f35c6e817/ujson-5.11.0-cp312-cp312-win_amd64.whl",hashes = {sha256 = "b7b136cc6abc7619124fd897ef75f8e63105298b5ca9bdf43ebd0e1fa0ee105f"}}, + {name = "ujson-5.11.0-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/22/2d/37b6557c97c3409c202c838aa9c960ca3896843b4295c4b7bb2bbd260664/ujson-5.11.0-cp312-cp312-win_arm64.whl",hashes = {sha256 = "6cd2df62f24c506a0ba322d5e4fe4466d47a9467b57e881ee15a31f7ecf68ff6"}}, +] +marker = "sys_platform != \"win32\" and implementation_name == \"cpython\" and \"default\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [] + [[packages]] name = "wcwidth" version = "0.2.13" @@ -3071,6 +2942,41 @@ dependencies = [ "backports-functools-lru-cache>=1.2.1; python_version < \"3.2\"", ] +[[packages]] +name = "websockets" +version = "15.0.1" +requires-python = ">=3.9" +sdist = {name = "websockets-15.0.1.tar.gz", url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hashes = {sha256 = "82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee"}} +wheels = [ + {name = "websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl",url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl",hashes = {sha256 = "ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931"}}, + {name = "websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl",hashes = {sha256 = "5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675"}}, + {name = "websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151"}}, + {name = "websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22"}}, + {name = "websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f"}}, + {name = "websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8"}}, + {name = "websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375"}}, + {name = "websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl",hashes = {sha256 = "756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d"}}, + {name = "websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4"}}, + {name = "websockets-15.0.1-cp313-cp313-win32.whl",url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl",hashes = {sha256 = "ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa"}}, + {name = "websockets-15.0.1-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl",hashes = {sha256 = "e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561"}}, + {name = "websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl",url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl",hashes = {sha256 = "3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3"}}, + {name = "websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl",hashes = {sha256 = "592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665"}}, + {name = "websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2"}}, + {name = "websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215"}}, + {name = "websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5"}}, + {name = "websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65"}}, + {name = "websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe"}}, + {name = "websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl",hashes = {sha256 = "5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4"}}, + {name = "websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597"}}, + {name = "websockets-15.0.1-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl",hashes = {sha256 = "c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9"}}, + {name = "websockets-15.0.1-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl",hashes = {sha256 = "fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7"}}, + {name = "websockets-15.0.1-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl",hashes = {sha256 = "f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f"}}, +] +marker = "\"default\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [] + [[packages]] name = "win32-setctime" version = "1.2.0" @@ -3085,14 +2991,54 @@ marker = "sys_platform == \"win32\" and \"default\" in dependency_groups" dependencies = [] [[packages]] -name = "zipp" -version = "3.23.0" -requires-python = ">=3.9" -sdist = {name = "zipp-3.23.0.tar.gz", url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hashes = {sha256 = "a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}} -wheels = [ - {name = "zipp-3.23.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl",hashes = {sha256 = "071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}}, +name = "wrapt" +version = "1.17.3" +requires-python = ">=3.8" +sdist = {name = "wrapt-1.17.3.tar.gz", url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hashes = {sha256 = "f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0"}} +wheels = [ + {name = "wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl",url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl",hashes = {sha256 = "cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39"}}, + {name = "wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl",hashes = {sha256 = "e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235"}}, + {name = "wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c"}}, + {name = "wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b"}}, + {name = "wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa"}}, + {name = "wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl",hashes = {sha256 = "373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7"}}, + {name = "wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl",hashes = {sha256 = "d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4"}}, + {name = "wrapt-1.17.3-cp314-cp314-win32.whl",url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl",hashes = {sha256 = "fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10"}}, + {name = "wrapt-1.17.3-cp314-cp314-win_amd64.whl",url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl",hashes = {sha256 = "e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6"}}, + {name = "wrapt-1.17.3-cp314-cp314-win_arm64.whl",url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl",hashes = {sha256 = "507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58"}}, + {name = "wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl",url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl",hashes = {sha256 = "ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a"}}, + {name = "wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl",hashes = {sha256 = "249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067"}}, + {name = "wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl",hashes = {sha256 = "5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454"}}, + {name = "wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e"}}, + {name = "wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f"}}, + {name = "wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056"}}, + {name = "wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804"}}, + {name = "wrapt-1.17.3-cp314-cp314t-win32.whl",url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl",hashes = {sha256 = "41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977"}}, + {name = "wrapt-1.17.3-cp314-cp314t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl",hashes = {sha256 = "73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116"}}, + {name = "wrapt-1.17.3-cp314-cp314t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl",hashes = {sha256 = "f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6"}}, + {name = "wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl",url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl",hashes = {sha256 = "a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0"}}, + {name = "wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl",hashes = {sha256 = "54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77"}}, + {name = "wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7"}}, + {name = "wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277"}}, + {name = "wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d"}}, + {name = "wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa"}}, + {name = "wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050"}}, + {name = "wrapt-1.17.3-cp313-cp313-win32.whl",url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl",hashes = {sha256 = "53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8"}}, + {name = "wrapt-1.17.3-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl",hashes = {sha256 = "1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb"}}, + {name = "wrapt-1.17.3-cp313-cp313-win_arm64.whl",url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl",hashes = {sha256 = "7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16"}}, + {name = "wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl",url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl",hashes = {sha256 = "ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0"}}, + {name = "wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl",hashes = {sha256 = "9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba"}}, + {name = "wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd"}}, + {name = "wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828"}}, + {name = "wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9"}}, + {name = "wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396"}}, + {name = "wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc"}}, + {name = "wrapt-1.17.3-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl",hashes = {sha256 = "4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe"}}, + {name = "wrapt-1.17.3-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl",hashes = {sha256 = "e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c"}}, + {name = "wrapt-1.17.3-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl",hashes = {sha256 = "604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6"}}, + {name = "wrapt-1.17.3-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl",hashes = {sha256 = "7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22"}}, ] -marker = "python_full_version < \"3.10.2\" and python_version >= \"3.9\" and \"dev\" in extras" +marker = "\"default\" in dependency_groups" [packages.tool.pdm] dependencies = [] @@ -3156,6 +3102,19 @@ dependencies = [ "uc-micro-py", ] +[[packages]] +name = "networkx" +version = "3.5" +requires-python = ">=3.11" +sdist = {name = "networkx-3.5.tar.gz", url = "https://files.pythonhosted.org/packages/6c/4f/ccdb8ad3a38e583f214547fd2f7ff1fc160c43a75af88e6aec213404b96a/networkx-3.5.tar.gz", hashes = {sha256 = "d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037"}} +wheels = [ + {name = "networkx-3.5-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl",hashes = {sha256 = "0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec"}}, +] +marker = "python_version ~= \"3.12\"" + +[packages.tool.pdm] +dependencies = [] + [[packages]] name = "pandas" version = "2.3.1" @@ -3182,27 +3141,6 @@ wheels = [ {name = "pandas-2.3.1-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/5b/14/cec7760d7c9507f11c97d64f29022e12a6cc4fc03ac694535e89f88ad2ec/pandas-2.3.1-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "56a342b231e8862c96bdb6ab97170e203ce511f4d0429589c8ede1ee8ece48b8"}}, {name = "pandas-2.3.1-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/50/b9/6e2d2c6728ed29fb3d4d4d302504fb66f1a543e37eb2e43f352a86365cdf/pandas-2.3.1-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "ca7ed14832bce68baef331f4d7f294411bed8efd032f8109d690df45e00c4679"}}, {name = "pandas-2.3.1-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/80/a5/3a92893e7399a691bad7664d977cb5e7c81cf666c81f89ea76ba2bff483d/pandas-2.3.1-cp312-cp312-win_amd64.whl",hashes = {sha256 = "ac942bfd0aca577bef61f2bc8da8147c4ef6879965ef883d8e8d5d2dc3e744b8"}}, - {name = "pandas-2.3.1-cp311-cp311-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/76/1c/ccf70029e927e473a4476c00e0d5b32e623bff27f0402d0a92b7fc29bb9f/pandas-2.3.1-cp311-cp311-macosx_10_9_x86_64.whl",hashes = {sha256 = "2b0540963d83431f5ce8870ea02a7430adca100cec8a050f0811f8e31035541b"}}, - {name = "pandas-2.3.1-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/ec/d3/3c37cb724d76a841f14b8f5fe57e5e3645207cc67370e4f84717e8bb7657/pandas-2.3.1-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "fe7317f578c6a153912bd2292f02e40c1d8f253e93c599e82620c7f69755c74f"}}, - {name = "pandas-2.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/8a/4c/367c98854a1251940edf54a4df0826dcacfb987f9068abf3e3064081a382/pandas-2.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "e6723a27ad7b244c0c79d8e7007092d7c8f0f11305770e2f4cd778b3ad5f9f85"}}, - {name = "pandas-2.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/07/5f/63760ff107bcf5146eee41b38b3985f9055e710a72fdd637b791dea3495c/pandas-2.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "3462c3735fe19f2638f2c3a40bd94ec2dc5ba13abbb032dd2fa1f540a075509d"}}, - {name = "pandas-2.3.1-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/15/53/f31a9b4dfe73fe4711c3a609bd8e60238022f48eacedc257cd13ae9327a7/pandas-2.3.1-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "98bcc8b5bf7afed22cc753a28bc4d9e26e078e777066bc53fac7904ddef9a678"}}, - {name = "pandas-2.3.1-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/e0/94/6fce6bf85b5056d065e0a7933cba2616dcb48596f7ba3c6341ec4bcc529d/pandas-2.3.1-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "4d544806b485ddf29e52d75b1f559142514e60ef58a832f74fb38e48d757b299"}}, - {name = "pandas-2.3.1-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/c8/7b/bdcb1ed8fccb63d04bdb7635161d0ec26596d92c9d7a6cce964e7876b6c1/pandas-2.3.1-cp311-cp311-win_amd64.whl",hashes = {sha256 = "b3cd4273d3cb3707b6fffd217204c52ed92859533e31dc03b7c5008aa933aaab"}}, - {name = "pandas-2.3.1-cp310-cp310-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/c4/ca/aa97b47287221fa37a49634532e520300088e290b20d690b21ce3e448143/pandas-2.3.1-cp310-cp310-macosx_10_9_x86_64.whl",hashes = {sha256 = "22c2e866f7209ebc3a8f08d75766566aae02bcc91d196935a1d9e59c7b990ac9"}}, - {name = "pandas-2.3.1-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/80/bf/7938dddc5f01e18e573dcfb0f1b8c9357d9b5fa6ffdee6e605b92efbdff2/pandas-2.3.1-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "3583d348546201aff730c8c47e49bc159833f971c2899d6097bce68b9112a4f1"}}, - {name = "pandas-2.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/ee/2f/9af748366763b2a494fed477f88051dbf06f56053d5c00eba652697e3f94/pandas-2.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "0f951fbb702dacd390561e0ea45cdd8ecfa7fb56935eb3dd78e306c19104b9b0"}}, - {name = "pandas-2.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/2c/95/79ab37aa4c25d1e7df953dde407bb9c3e4ae47d154bc0dd1692f3a6dcf8c/pandas-2.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "cd05b72ec02ebfb993569b4931b2e16fbb4d6ad6ce80224a3ee838387d83a191"}}, - {name = "pandas-2.3.1-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/75/a7/d65e5d8665c12c3c6ff5edd9709d5836ec9b6f80071b7f4a718c6106e86e/pandas-2.3.1-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "1b916a627919a247d865aed068eb65eb91a344b13f5b57ab9f610b7716c92de1"}}, - {name = "pandas-2.3.1-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/65/f3/4c1dbd754dbaa79dbf8b537800cb2fa1a6e534764fef50ab1f7533226c5c/pandas-2.3.1-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "fe67dc676818c186d5a3d5425250e40f179c2a89145df477dd82945eaea89e97"}}, - {name = "pandas-2.3.1-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/3f/d6/d7f5777162aa9b48ec3910bca5a58c9b5927cfd9cfde3aa64322f5ba4b9f/pandas-2.3.1-cp310-cp310-win_amd64.whl",hashes = {sha256 = "2eb789ae0274672acbd3c575b0598d213345660120a257b47b5dafdc618aec83"}}, - {name = "pandas-2.3.1-cp39-cp39-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/6e/21/ecf2df680982616459409b09962a8c2065330c7151dc6538069f3b634acf/pandas-2.3.1-cp39-cp39-macosx_10_9_x86_64.whl",hashes = {sha256 = "4645f770f98d656f11c69e81aeb21c6fca076a44bed3dcbb9396a4311bc7f6d8"}}, - {name = "pandas-2.3.1-cp39-cp39-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/1e/1a/dcb50e44b75419e96b276c9fb023b0f147b3c411be1cd517492aa2a184d4/pandas-2.3.1-cp39-cp39-macosx_11_0_arm64.whl",hashes = {sha256 = "342e59589cc454aaff7484d75b816a433350b3d7964d7847327edda4d532a2e3"}}, - {name = "pandas-2.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/2d/55/66cd2b679f6a27398380eac7574bc24746128f74626a3c02b978ea00e5ce/pandas-2.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "1d12f618d80379fde6af007f65f0c25bd3e40251dbd1636480dfffce2cf1e6da"}}, - {name = "pandas-2.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/ae/1c/5b9b263c80fd5e231b77df6f78cd7426d1d4ad3a4e858e85b7b3d93d0e9c/pandas-2.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "dd71c47a911da120d72ef173aeac0bf5241423f9bfea57320110a978457e069e"}}, - {name = "pandas-2.3.1-cp39-cp39-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/f7/74/7e817b31413fbb96366ea327d43d1926a9c48c58074e27e094e2839a0e36/pandas-2.3.1-cp39-cp39-musllinux_1_2_aarch64.whl",hashes = {sha256 = "09e3b1587f0f3b0913e21e8b32c3119174551deb4a4eba4a89bc7377947977e7"}}, - {name = "pandas-2.3.1-cp39-cp39-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/1f/0f/bc0a44b47eba2f22ae4235719a573d552ef7ad76ed3ea39ae62d554e040b/pandas-2.3.1-cp39-cp39-musllinux_1_2_x86_64.whl",hashes = {sha256 = "2323294c73ed50f612f67e2bf3ae45aea04dce5690778e08a09391897f35ff88"}}, - {name = "pandas-2.3.1-cp39-cp39-win_amd64.whl",url = "https://files.pythonhosted.org/packages/fa/cb/6c32f8fadefa4314b740fbe8f74f6a02423bd1549e7c930826df35ac3c1b/pandas-2.3.1-cp39-cp39-win_amd64.whl",hashes = {sha256 = "b4b0de34dc8499c2db34000ef8baad684cfa4cbd836ecee05f323ebfba348c7d"}}, ] marker = "\"default\" in dependency_groups" @@ -3269,6 +3207,19 @@ marker = "\"default\" in dependency_groups" [packages.tool.pdm] dependencies = [] +[[packages]] +name = "pycparser" +version = "2.23" +requires-python = ">=3.8" +sdist = {name = "pycparser-2.23.tar.gz", url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hashes = {sha256 = "78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"}} +wheels = [ + {name = "pycparser-2.23-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl",hashes = {sha256 = "e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"}}, +] +marker = "implementation_name != \"PyPy\" and \"default\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [] + [[packages]] name = "pyproject-hooks" version = "1.2.0" @@ -3321,33 +3272,6 @@ wheels = [ {name = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/db/5d/36619b61ffa2429eeaefaab4f3374666adf36ad8ac6330d855848d7d36fd/ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d"}}, {name = "ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/b1/82/85cb92f15a4231c89b95dfe08b09eb6adca929ef7df7e17ab59902b6f589/ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl",hashes = {sha256 = "e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5"}}, {name = "ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/d7/8f/c3654f6f1ddb75daf3922c3d8fc6005b1ab56671ad56ffb874d908bfa668/ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl",hashes = {sha256 = "0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4"}}, - {name = "ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl",url = "https://files.pythonhosted.org/packages/fb/8f/683c6ad562f558cbc4f7c029abcd9599148c51c54b5ef0f24f2638da9fbb/ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl",hashes = {sha256 = "4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6"}}, - {name = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/3c/d2/b79b7d695e2f21da020bd44c782490578f300dd44f0a4c57a92575758a76/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux2014_aarch64.whl",hashes = {sha256 = "d84318609196d6bd6da0edfa25cedfbabd8dbde5140a0a23af29ad4b8f91fb1e"}}, - {name = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/68/6e/264c50ce2a31473a9fdbf4fa66ca9b2b17c7455b31ef585462343818bd6c/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "bb43a269eb827806502c7c8efb7ae7e9e9d0573257a46e8e952f4d4caba4f31e"}}, - {name = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/86/29/88c2567bc893c84d88b4c48027367c3562ae69121d568e8a3f3a8d363f4d/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52"}}, - {name = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl",url = "https://files.pythonhosted.org/packages/11/46/879763c619b5470820f0cd6ca97d134771e502776bc2b844d2adb6e37753/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl",hashes = {sha256 = "cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642"}}, - {name = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/02/80/ece7e6034256a4186bbe50dee28cd032d816974941a6abf6a9d65e4228a7/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl",hashes = {sha256 = "7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2"}}, - {name = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/f0/ca/e4106ac7e80efbabdf4bf91d3d32fc424e41418458251712f5672eada9ce/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3"}}, - {name = "ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl",url = "https://files.pythonhosted.org/packages/67/58/b1f60a1d591b771298ffa0428237afb092c7f29ae23bad93420b1eb10703/ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl",hashes = {sha256 = "bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4"}}, - {name = "ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/b4/4f/b52f634c9548a9291a70dfce26ca7ebce388235c93588a1068028ea23fcc/ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl",hashes = {sha256 = "a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb"}}, - {name = "ruamel.yaml.clib-0.2.12-cp310-cp310-macosx_13_0_arm64.whl",url = "https://files.pythonhosted.org/packages/70/57/40a958e863e299f0c74ef32a3bde9f2d1ea8d69669368c0c502a0997f57f/ruamel.yaml.clib-0.2.12-cp310-cp310-macosx_13_0_arm64.whl",hashes = {sha256 = "11f891336688faf5156a36293a9c362bdc7c88f03a8a027c2c1d8e0bcde998e5"}}, - {name = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/98/a8/29a3eb437b12b95f50a6bcc3d7d7214301c6c529d8fdc227247fa84162b5/ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux2014_aarch64.whl",hashes = {sha256 = "a606ef75a60ecf3d924613892cc603b154178ee25abb3055db5062da811fd969"}}, - {name = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/35/6d/ae05a87a3ad540259c3ad88d71275cbd1c0f2d30ae04c65dcbfb6dcd4b9f/ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "fd5415dded15c3822597455bc02bcd66e81ef8b7a48cb71a33628fc9fdde39df"}}, - {name = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/7f/b7/20c6f3c0b656fe609675d69bc135c03aac9e3865912444be6339207b6648/ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76"}}, - {name = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_i686.whl",url = "https://files.pythonhosted.org/packages/cd/11/d12dbf683471f888d354dac59593873c2b45feb193c5e3e0f2ebf85e68b9/ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_i686.whl",hashes = {sha256 = "22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6"}}, - {name = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/72/14/4c268f5077db5c83f743ee1daeb236269fa8577133a5cfa49f8b382baf13/ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_x86_64.whl",hashes = {sha256 = "932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd"}}, - {name = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/30/fc/8cd12f189c6405a4c1cf37bd633aa740a9538c8e40497c231072d0fef5cf/ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "a52d48f4e7bf9005e8f0a89209bf9a73f7190ddf0489eee5eb51377385f59f2a"}}, - {name = "ruamel.yaml.clib-0.2.12-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/80/29/c0a017b704aaf3cbf704989785cd9c5d5b8ccec2dae6ac0c53833c84e677/ruamel.yaml.clib-0.2.12-cp310-cp310-win32.whl",hashes = {sha256 = "3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da"}}, - {name = "ruamel.yaml.clib-0.2.12-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/3a/65/fa39d74db4e2d0cd252355732d966a460a41cd01c6353b820a0952432839/ruamel.yaml.clib-0.2.12-cp310-cp310-win_amd64.whl",hashes = {sha256 = "ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28"}}, - {name = "ruamel.yaml.clib-0.2.12-cp39-cp39-macosx_12_0_arm64.whl",url = "https://files.pythonhosted.org/packages/e5/46/ccdef7a84ad745c37cb3d9a81790f28fbc9adf9c237dba682017b123294e/ruamel.yaml.clib-0.2.12-cp39-cp39-macosx_12_0_arm64.whl",hashes = {sha256 = "fc4b630cd3fa2cf7fce38afa91d7cfe844a9f75d7f0f36393fa98815e911d987"}}, - {name = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/29/09/932360f30ad1b7b79f08757e0a6fb8c5392a52cdcc182779158fe66d25ac/ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux2014_aarch64.whl",hashes = {sha256 = "bc5f1e1c28e966d61d2519f2a3d451ba989f9ea0f2307de7bc45baa526de9e45"}}, - {name = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/a2/2a/5b27602e7a4344c1334e26bf4739746206b7a60a8acdba33a61473468b73/ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "5a0e060aace4c24dcaf71023bbd7d42674e3b230f7e7b97317baf1e953e5b519"}}, - {name = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/da/1c/23497017c554fc06ff5701b29355522cff850f626337fff35d9ab352cb18/ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "e2f1c3765db32be59d18ab3953f43ab62a761327aafc1594a2a1fbe038b8b8a7"}}, - {name = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_i686.whl",url = "https://files.pythonhosted.org/packages/68/e6/f3d4ff3223f9ea49c3b7169ec0268e42bd49f87c70c0e3e853895e4a7ae2/ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_i686.whl",hashes = {sha256 = "d85252669dc32f98ebcd5d36768f5d4faeaeaa2d655ac0473be490ecdae3c285"}}, - {name = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/84/62/ead07043527642491e5011b143f44b81ef80f1025a96069b7210e0f2f0f3/ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_x86_64.whl",hashes = {sha256 = "e143ada795c341b56de9418c58d028989093ee611aa27ffb9b7f609c00d813ed"}}, - {name = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/52/b3/fe4d84446f7e4887e3bea7ceff0a7df23790b5ed625f830e79ace88ebefb/ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_2_aarch64.whl",hashes = {sha256 = "2c59aa6170b990d8d2719323e628aaf36f3bfbc1c26279c0eeeb24d05d2d11c7"}}, - {name = "ruamel.yaml.clib-0.2.12-cp39-cp39-win32.whl",url = "https://files.pythonhosted.org/packages/6e/b3/7feb99a00bfaa5c6868617bb7651308afde85e5a0b23cd187fe5de65feeb/ruamel.yaml.clib-0.2.12-cp39-cp39-win32.whl",hashes = {sha256 = "beffaed67936fbbeffd10966a4eb53c402fafd3d6833770516bf7314bc6ffa12"}}, - {name = "ruamel.yaml.clib-0.2.12-cp39-cp39-win_amd64.whl",url = "https://files.pythonhosted.org/packages/93/07/de635108684b7a5bb06e432b0930c5a04b6c59efe73bd966d8db3cc208f2/ruamel.yaml.clib-0.2.12-cp39-cp39-win_amd64.whl",hashes = {sha256 = "040ae85536960525ea62868b642bdb0c2cc6021c9f9d507810c0c604e66f5a7b"}}, ] marker = "platform_python_implementation == \"CPython\" and python_version < \"3.14\" and python_version >= \"3.9\" and \"dev\" in extras" @@ -3406,6 +3330,107 @@ marker = "\"dev\" in extras" [packages.tool.pdm] dependencies = [] +[[packages]] +name = "standard-aifc" +version = "3.13.0" +sdist = {name = "standard_aifc-3.13.0.tar.gz", url = "https://files.pythonhosted.org/packages/c4/53/6050dc3dde1671eb3db592c13b55a8005e5040131f7509cef0215212cb84/standard_aifc-3.13.0.tar.gz", hashes = {sha256 = "64e249c7cb4b3daf2fdba4e95721f811bde8bdfc43ad9f936589b7bb2fae2e43"}} +wheels = [ + {name = "standard_aifc-3.13.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/c3/52/5fbb203394cc852334d1575cc020f6bcec768d2265355984dfd361968f36/standard_aifc-3.13.0-py3-none-any.whl",hashes = {sha256 = "f7ae09cc57de1224a0dd8e3eb8f73830be7c3d0bc485de4c1f82b4a7f645ac66"}}, +] +marker = "python_version ~= \"3.13\"" + +[packages.tool.pdm] +dependencies = [ + "standard-chunk; python_version >= \"3.13\"", + "audioop-lts; python_version >= \"3.13\"", +] + +[[packages]] +name = "audioop-lts" +version = "0.2.2" +requires-python = ">=3.13" +sdist = {name = "audioop_lts-0.2.2.tar.gz", url = "https://files.pythonhosted.org/packages/38/53/946db57842a50b2da2e0c1e34bd37f36f5aadba1a929a3971c5d7841dbca/audioop_lts-0.2.2.tar.gz", hashes = {sha256 = "64d0c62d88e67b98a1a5e71987b7aa7b5bcffc7dcee65b635823dbdd0a8dbbd0"}} +wheels = [ + {name = "audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_universal2.whl",url = "https://files.pythonhosted.org/packages/5c/73/413b5a2804091e2c7d5def1d618e4837f1cb82464e230f827226278556b7/audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_universal2.whl",hashes = {sha256 = "f9ee9b52f5f857fbaf9d605a360884f034c92c1c23021fb90b2e39b8e64bede6"}}, + {name = "audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/ae/8c/daa3308dc6593944410c2c68306a5e217f5c05b70a12e70228e7dd42dc5c/audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_x86_64.whl",hashes = {sha256 = "49ee1a41738a23e98d98b937a0638357a2477bc99e61b0f768a8f654f45d9b7a"}}, + {name = "audioop_lts-0.2.2-cp314-cp314t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/4e/86/c2e0f627168fcf61781a8f72cab06b228fe1da4b9fa4ab39cfb791b5836b/audioop_lts-0.2.2-cp314-cp314t-macosx_11_0_arm64.whl",hashes = {sha256 = "5b00be98ccd0fc123dcfad31d50030d25fcf31488cde9e61692029cd7394733b"}}, + {name = "audioop_lts-0.2.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/c7/bd/35dce665255434f54e5307de39e31912a6f902d4572da7c37582809de14f/audioop_lts-0.2.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "a6d2e0f9f7a69403e388894d4ca5ada5c47230716a03f2847cfc7bd1ecb589d6"}}, + {name = "audioop_lts-0.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/2d/d2/deeb9f51def1437b3afa35aeb729d577c04bcd89394cb56f9239a9f50b6f/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "f9b0b8a03ef474f56d1a842af1a2e01398b8f7654009823c6d9e0ecff4d5cfbf"}}, + {name = "audioop_lts-0.2.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",url = "https://files.pythonhosted.org/packages/76/3b/09f8b35b227cee28cc8231e296a82759ed80c1a08e349811d69773c48426/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",hashes = {sha256 = "2b267b70747d82125f1a021506565bdc5609a2b24bcb4773c16d79d2bb260bbd"}}, + {name = "audioop_lts-0.2.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/0b/15/05b48a935cf3b130c248bfdbdea71ce6437f5394ee8533e0edd7cfd93d5e/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "0337d658f9b81f4cd0fdb1f47635070cc084871a3d4646d9de74fdf4e7c3d24a"}}, + {name = "audioop_lts-0.2.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/83/80/186b7fce6d35b68d3d739f228dc31d60b3412105854edb975aa155a58339/audioop_lts-0.2.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "167d3b62586faef8b6b2275c3218796b12621a60e43f7e9d5845d627b9c9b80e"}}, + {name = "audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/49/89/c78cc5ac6cb5828f17514fb12966e299c850bc885e80f8ad94e38d450886/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "0d9385e96f9f6da847f4d571ce3cb15b5091140edf3db97276872647ce37efd7"}}, + {name = "audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/4c/4b/6401888d0c010e586c2ca50fce4c903d70a6bb55928b16cfbdfd957a13da/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "48159d96962674eccdca9a3df280e864e8ac75e40a577cc97c5c42667ffabfc5"}}, + {name = "audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/de/f8/c874ca9bb447dae0e2ef2e231f6c4c2b0c39e31ae684d2420b0f9e97ee68/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_riscv64.whl",hashes = {sha256 = "8fefe5868cd082db1186f2837d64cfbfa78b548ea0d0543e9b28935ccce81ce9"}}, + {name = "audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/3e/c0/0323e66f3daebc13fd46b36b30c3be47e3fc4257eae44f1e77eb828c703f/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_s390x.whl",hashes = {sha256 = "58cf54380c3884fb49fdd37dfb7a772632b6701d28edd3e2904743c5e1773602"}}, + {name = "audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/98/6b/acc7734ac02d95ab791c10c3f17ffa3584ccb9ac5c18fd771c638ed6d1f5/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "088327f00488cdeed296edd9215ca159f3a5a5034741465789cad403fcf4bec0"}}, + {name = "audioop_lts-0.2.2-cp314-cp314t-win32.whl",url = "https://files.pythonhosted.org/packages/13/c3/c3dc3f564ce6877ecd2a05f8d751b9b27a8c320c2533a98b0c86349778d0/audioop_lts-0.2.2-cp314-cp314t-win32.whl",hashes = {sha256 = "068aa17a38b4e0e7de771c62c60bbca2455924b67a8814f3b0dee92b5820c0b3"}}, + {name = "audioop_lts-0.2.2-cp314-cp314t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/72/bb/b4608537e9ffcb86449091939d52d24a055216a36a8bf66b936af8c3e7ac/audioop_lts-0.2.2-cp314-cp314t-win_amd64.whl",hashes = {sha256 = "a5bf613e96f49712073de86f20dbdd4014ca18efd4d34ed18c75bd808337851b"}}, + {name = "audioop_lts-0.2.2-cp314-cp314t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/f6/22/91616fe707a5c5510de2cac9b046a30defe7007ba8a0c04f9c08f27df312/audioop_lts-0.2.2-cp314-cp314t-win_arm64.whl",hashes = {sha256 = "b492c3b040153e68b9fdaff5913305aaaba5bb433d8a7f73d5cf6a64ed3cc1dd"}}, + {name = "audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_universal2.whl",url = "https://files.pythonhosted.org/packages/58/a7/0a764f77b5c4ac58dc13c01a580f5d32ae8c74c92020b961556a43e26d02/audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_universal2.whl",hashes = {sha256 = "73f80bf4cd5d2ca7814da30a120de1f9408ee0619cc75da87d0641273d202a09"}}, + {name = "audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/aa/ed/ebebedde1a18848b085ad0fa54b66ceb95f1f94a3fc04f1cd1b5ccb0ed42/audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_x86_64.whl",hashes = {sha256 = "106753a83a25ee4d6f473f2be6b0966fc1c9af7e0017192f5531a3e7463dce58"}}, + {name = "audioop_lts-0.2.2-cp313-cp313t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/cb/6e/11ca8c21af79f15dbb1c7f8017952ee8c810c438ce4e2b25638dfef2b02c/audioop_lts-0.2.2-cp313-cp313t-macosx_11_0_arm64.whl",hashes = {sha256 = "fbdd522624141e40948ab3e8cdae6e04c748d78710e9f0f8d4dae2750831de19"}}, + {name = "audioop_lts-0.2.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/84/52/0022f93d56d85eec5da6b9da6a958a1ef09e80c39f2cc0a590c6af81dcbb/audioop_lts-0.2.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "143fad0311e8209ece30a8dbddab3b65ab419cbe8c0dde6e8828da25999be911"}}, + {name = "audioop_lts-0.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/87/1d/48a889855e67be8718adbc7a01f3c01d5743c325453a5e81cf3717664aad/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "dfbbc74ec68a0fd08cfec1f4b5e8cca3d3cd7de5501b01c4b5d209995033cde9"}}, + {name = "audioop_lts-0.2.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",url = "https://files.pythonhosted.org/packages/98/a6/94b7213190e8077547ffae75e13ed05edc488653c85aa5c41472c297d295/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",hashes = {sha256 = "cfcac6aa6f42397471e4943e0feb2244549db5c5d01efcd02725b96af417f3fe"}}, + {name = "audioop_lts-0.2.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/e9/e9/78450d7cb921ede0cfc33426d3a8023a3bda755883c95c868ee36db8d48d/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "752d76472d9804ac60f0078c79cdae8b956f293177acd2316cd1e15149aee132"}}, + {name = "audioop_lts-0.2.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/4f/e2/cd5439aad4f3e34ae1ee852025dc6aa8f67a82b97641e390bf7bd9891d3e/audioop_lts-0.2.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "83c381767e2cc10e93e40281a04852facc4cd9334550e0f392f72d1c0a9c5753"}}, + {name = "audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/68/4b/9d853e9076c43ebba0d411e8d2aa19061083349ac695a7d082540bad64d0/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "c0022283e9556e0f3643b7c3c03f05063ca72b3063291834cca43234f20c60bb"}}, + {name = "audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/58/26/4bae7f9d2f116ed5593989d0e521d679b0d583973d203384679323d8fa85/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "a2d4f1513d63c795e82948e1305f31a6d530626e5f9f2605408b300ae6095093"}}, + {name = "audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/b2/67/a9f4fb3e250dda9e9046f8866e9fa7d52664f8985e445c6b4ad6dfb55641/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_riscv64.whl",hashes = {sha256 = "c9c8e68d8b4a56fda8c025e538e639f8c5953f5073886b596c93ec9b620055e7"}}, + {name = "audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/70/f7/3de86562db0121956148bcb0fe5b506615e3bcf6e63c4357a612b910765a/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_s390x.whl",hashes = {sha256 = "96f19de485a2925314f5020e85911fb447ff5fbef56e8c7c6927851b95533a1c"}}, + {name = "audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/f1/32/fd772bf9078ae1001207d2df1eef3da05bea611a87dd0e8217989b2848fa/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "e541c3ef484852ef36545f66209444c48b28661e864ccadb29daddb6a4b8e5f5"}}, + {name = "audioop_lts-0.2.2-cp313-cp313t-win32.whl",url = "https://files.pythonhosted.org/packages/4f/41/affea7181592ab0ab560044632571a38edaf9130b84928177823fbf3176a/audioop_lts-0.2.2-cp313-cp313t-win32.whl",hashes = {sha256 = "d5e73fa573e273e4f2e5ff96f9043858a5e9311e94ffefd88a3186a910c70917"}}, + {name = "audioop_lts-0.2.2-cp313-cp313t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/28/2b/0372842877016641db8fc54d5c88596b542eec2f8f6c20a36fb6612bf9ee/audioop_lts-0.2.2-cp313-cp313t-win_amd64.whl",hashes = {sha256 = "9191d68659eda01e448188f60364c7763a7ca6653ed3f87ebb165822153a8547"}}, + {name = "audioop_lts-0.2.2-cp313-cp313t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/ee/ca/baf2b9cc7e96c179bb4a54f30fcd83e6ecb340031bde68f486403f943768/audioop_lts-0.2.2-cp313-cp313t-win_arm64.whl",hashes = {sha256 = "c174e322bb5783c099aaf87faeb240c8d210686b04bd61dfd05a8e5a83d88969"}}, + {name = "audioop_lts-0.2.2-cp313-abi3-macosx_10_13_universal2.whl",url = "https://files.pythonhosted.org/packages/de/d4/94d277ca941de5a507b07f0b592f199c22454eeaec8f008a286b3fbbacd6/audioop_lts-0.2.2-cp313-abi3-macosx_10_13_universal2.whl",hashes = {sha256 = "fd3d4602dc64914d462924a08c1a9816435a2155d74f325853c1f1ac3b2d9800"}}, + {name = "audioop_lts-0.2.2-cp313-abi3-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/f8/5a/656d1c2da4b555920ce4177167bfeb8623d98765594af59702c8873f60ec/audioop_lts-0.2.2-cp313-abi3-macosx_10_13_x86_64.whl",hashes = {sha256 = "550c114a8df0aafe9a05442a1162dfc8fec37e9af1d625ae6060fed6e756f303"}}, + {name = "audioop_lts-0.2.2-cp313-abi3-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/1b/83/ea581e364ce7b0d41456fb79d6ee0ad482beda61faf0cab20cbd4c63a541/audioop_lts-0.2.2-cp313-abi3-macosx_11_0_arm64.whl",hashes = {sha256 = "9a13dc409f2564de15dd68be65b462ba0dde01b19663720c68c1140c782d1d75"}}, + {name = "audioop_lts-0.2.2-cp313-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/b8/3b/e8964210b5e216e5041593b7d33e97ee65967f17c282e8510d19c666dab4/audioop_lts-0.2.2-cp313-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "51c916108c56aa6e426ce611946f901badac950ee2ddaf302b7ed35d9958970d"}}, + {name = "audioop_lts-0.2.2-cp313-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/c7/2e/0a1c52faf10d51def20531a59ce4c706cb7952323b11709e10de324d6493/audioop_lts-0.2.2-cp313-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "47eba38322370347b1c47024defbd36374a211e8dd5b0dcbce7b34fdb6f8847b"}}, + {name = "audioop_lts-0.2.2-cp313-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",url = "https://files.pythonhosted.org/packages/75/e8/cd95eef479656cb75ab05dfece8c1f8c395d17a7c651d88f8e6e291a63ab/audioop_lts-0.2.2-cp313-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",hashes = {sha256 = "ba7c3a7e5f23e215cb271516197030c32aef2e754252c4c70a50aaff7031a2c8"}}, + {name = "audioop_lts-0.2.2-cp313-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/5c/1e/a0c42570b74f83efa5cca34905b3eef03f7ab09fe5637015df538a7f3345/audioop_lts-0.2.2-cp313-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "def246fe9e180626731b26e89816e79aae2276f825420a07b4a647abaa84becc"}}, + {name = "audioop_lts-0.2.2-cp313-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/50/d5/8a0ae607ca07dbb34027bac8db805498ee7bfecc05fd2c148cc1ed7646e7/audioop_lts-0.2.2-cp313-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "e160bf9df356d841bb6c180eeeea1834085464626dc1b68fa4e1d59070affdc3"}}, + {name = "audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/12/17/0d28c46179e7910bfb0bb62760ccb33edb5de973052cb2230b662c14ca2e/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_aarch64.whl",hashes = {sha256 = "4b4cd51a57b698b2d06cb9993b7ac8dfe89a3b2878e96bc7948e9f19ff51dba6"}}, + {name = "audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/84/ba/bd5d3806641564f2024e97ca98ea8f8811d4e01d9b9f9831474bc9e14f9e/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "4a53aa7c16a60a6857e6b0b165261436396ef7293f8b5c9c828a3a203147ed4a"}}, + {name = "audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/f9/5e/435ce8d5642f1f7679540d1e73c1c42d933331c0976eb397d1717d7f01a3/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_riscv64.whl",hashes = {sha256 = "3fc38008969796f0f689f1453722a0f463da1b8a6fbee11987830bfbb664f623"}}, + {name = "audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/ae/3b/b909e76b606cbfd53875693ec8c156e93e15a1366a012f0b7e4fb52d3c34/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_s390x.whl",hashes = {sha256 = "15ab25dd3e620790f40e9ead897f91e79c0d3ce65fe193c8ed6c26cffdd24be7"}}, + {name = "audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/30/e7/8f1603b4572d79b775f2140d7952f200f5e6c62904585d08a01f0a70393a/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_x86_64.whl",hashes = {sha256 = "03f061a1915538fd96272bac9551841859dbb2e3bf73ebe4a23ef043766f5449"}}, + {name = "audioop_lts-0.2.2-cp313-abi3-win32.whl",url = "https://files.pythonhosted.org/packages/b5/96/c37846df657ccdda62ba1ae2b6534fa90e2e1b1742ca8dcf8ebd38c53801/audioop_lts-0.2.2-cp313-abi3-win32.whl",hashes = {sha256 = "3bcddaaf6cc5935a300a8387c99f7a7fbbe212a11568ec6cf6e4bc458c048636"}}, + {name = "audioop_lts-0.2.2-cp313-abi3-win_amd64.whl",url = "https://files.pythonhosted.org/packages/34/a5/9d78fdb5b844a83da8a71226c7bdae7cc638861085fff7a1d707cb4823fa/audioop_lts-0.2.2-cp313-abi3-win_amd64.whl",hashes = {sha256 = "a2c2a947fae7d1062ef08c4e369e0ba2086049a5e598fda41122535557012e9e"}}, + {name = "audioop_lts-0.2.2-cp313-abi3-win_arm64.whl",url = "https://files.pythonhosted.org/packages/34/25/20d8fde083123e90c61b51afb547bb0ea7e77bab50d98c0ab243d02a0e43/audioop_lts-0.2.2-cp313-abi3-win_arm64.whl",hashes = {sha256 = "5f93a5db13927a37d2d09637ccca4b2b6b48c19cd9eda7b17a2e9f77edee6a6f"}}, +] +marker = "python_version ~= \"3.13\"" + +[packages.tool.pdm] +dependencies = [] + +[[packages]] +name = "standard-chunk" +version = "3.13.0" +sdist = {name = "standard_chunk-3.13.0.tar.gz", url = "https://files.pythonhosted.org/packages/43/06/ce1bb165c1f111c7d23a1ad17204d67224baa69725bb6857a264db61beaf/standard_chunk-3.13.0.tar.gz", hashes = {sha256 = "4ac345d37d7e686d2755e01836b8d98eda0d1a3ee90375e597ae43aaf064d654"}} +wheels = [ + {name = "standard_chunk-3.13.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/7a/90/a5c1084d87767d787a6caba615aa50dc587229646308d9420c960cb5e4c0/standard_chunk-3.13.0-py3-none-any.whl",hashes = {sha256 = "17880a26c285189c644bd5bd8f8ed2bdb795d216e3293e6dbe55bbd848e2982c"}}, +] +marker = "python_version ~= \"3.13\"" + +[packages.tool.pdm] +dependencies = [] + +[[packages]] +name = "standard-sunau" +version = "3.13.0" +sdist = {name = "standard_sunau-3.13.0.tar.gz", url = "https://files.pythonhosted.org/packages/66/e3/ce8d38cb2d70e05ffeddc28bb09bad77cfef979eb0a299c9117f7ed4e6a9/standard_sunau-3.13.0.tar.gz", hashes = {sha256 = "b319a1ac95a09a2378a8442f403c66f4fd4b36616d6df6ae82b8e536ee790908"}} +wheels = [ + {name = "standard_sunau-3.13.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/34/ae/e3707f6c1bc6f7aa0df600ba8075bfb8a19252140cd595335be60e25f9ee/standard_sunau-3.13.0-py3-none-any.whl",hashes = {sha256 = "53af624a9529c41062f4c2fd33837f297f3baa196b0cfceffea6555654602622"}}, +] +marker = "python_version ~= \"3.13\"" + +[packages.tool.pdm] +dependencies = [ + "audioop-lts; python_version >= \"3.13\"", +] + [[packages]] name = "uc-micro-py" version = "1.0.3" @@ -3455,61 +3480,6 @@ wheels = [ {name = "xxhash-3.5.0-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/78/e3/dd76659b2811b3fd06892a8beb850e1996b63e9235af5a86ea348f053e9e/xxhash-3.5.0-cp312-cp312-win32.whl",hashes = {sha256 = "f7b58d1fd3551b8c80a971199543379be1cee3d0d409e1f6d8b01c1a2eebf1f8"}}, {name = "xxhash-3.5.0-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/d9/6b/1c443fe6cfeb4ad1dcf231cdec96eb94fb43d6498b4469ed8b51f8b59a37/xxhash-3.5.0-cp312-cp312-win_amd64.whl",hashes = {sha256 = "fa0cafd3a2af231b4e113fba24a65d7922af91aeb23774a8b78228e6cd785e3e"}}, {name = "xxhash-3.5.0-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/0f/eb/04405305f290173acc0350eba6d2f1a794b57925df0398861a20fbafa415/xxhash-3.5.0-cp312-cp312-win_arm64.whl",hashes = {sha256 = "586886c7e89cb9828bcd8a5686b12e161368e0064d040e225e72607b43858ba2"}}, - {name = "xxhash-3.5.0-cp311-cp311-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/b8/c7/afed0f131fbda960ff15eee7f304fa0eeb2d58770fade99897984852ef23/xxhash-3.5.0-cp311-cp311-macosx_10_9_x86_64.whl",hashes = {sha256 = "02c2e816896dc6f85922ced60097bcf6f008dedfc5073dcba32f9c8dd786f3c1"}}, - {name = "xxhash-3.5.0-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/8c/0c/7c3bc6d87e5235672fcc2fb42fd5ad79fe1033925f71bf549ee068c7d1ca/xxhash-3.5.0-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "6027dcd885e21581e46d3c7f682cfb2b870942feeed58a21c29583512c3f09f8"}}, - {name = "xxhash-3.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/04/9e/01067981d98069eec1c20201f8c145367698e9056f8bc295346e4ea32dd1/xxhash-3.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "1308fa542bbdbf2fa85e9e66b1077eea3a88bef38ee8a06270b4298a7a62a166"}}, - {name = "xxhash-3.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/d4/09/d4996de4059c3ce5342b6e1e6a77c9d6c91acce31f6ed979891872dd162b/xxhash-3.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "c28b2fdcee797e1c1961cd3bcd3d545cab22ad202c846235197935e1df2f8ef7"}}, - {name = "xxhash-3.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/62/f5/6d2dc9f8d55a7ce0f5e7bfef916e67536f01b85d32a9fbf137d4cadbee38/xxhash-3.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "924361811732ddad75ff23e90efd9ccfda4f664132feecb90895bade6a1b4623"}}, - {name = "xxhash-3.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/d9/72/9256303f10e41ab004799a4aa74b80b3c5977d6383ae4550548b24bd1971/xxhash-3.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "89997aa1c4b6a5b1e5b588979d1da048a3c6f15e55c11d117a56b75c84531f5a"}}, - {name = "xxhash-3.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/34/92/1a3a29acd08248a34b0e6a94f4e0ed9b8379a4ff471f1668e4dce7bdbaa8/xxhash-3.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "685c4f4e8c59837de103344eb1c8a3851f670309eb5c361f746805c5471b8c88"}}, - {name = "xxhash-3.5.0-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/53/ad/7fa1a109663366de42f724a1cdb8e796a260dbac45047bce153bc1e18abf/xxhash-3.5.0-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "dbd2ecfbfee70bc1a4acb7461fa6af7748ec2ab08ac0fa298f281c51518f982c"}}, - {name = "xxhash-3.5.0-cp311-cp311-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/35/02/137300e24203bf2b2a49b48ce898ecce6fd01789c0fcd9c686c0a002d129/xxhash-3.5.0-cp311-cp311-musllinux_1_2_i686.whl",hashes = {sha256 = "25b5a51dc3dfb20a10833c8eee25903fd2e14059e9afcd329c9da20609a307b2"}}, - {name = "xxhash-3.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/23/03/aeceb273933d7eee248c4322b98b8e971f06cc3880e5f7602c94e5578af5/xxhash-3.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "a8fb786fb754ef6ff8c120cb96629fb518f8eb5a61a16aac3a979a9dbd40a084"}}, - {name = "xxhash-3.5.0-cp311-cp311-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/e3/64/ed82ec09489474cbb35c716b189ddc1521d8b3de12b1b5ab41ce7f70253c/xxhash-3.5.0-cp311-cp311-musllinux_1_2_s390x.whl",hashes = {sha256 = "a905ad00ad1e1c34fe4e9d7c1d949ab09c6fa90c919860c1534ff479f40fd12d"}}, - {name = "xxhash-3.5.0-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/71/43/6db4c02dcb488ad4e03bc86d70506c3d40a384ee73c9b5c93338eb1f3c23/xxhash-3.5.0-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "963be41bcd49f53af6d795f65c0da9b4cc518c0dd9c47145c98f61cb464f4839"}}, - {name = "xxhash-3.5.0-cp311-cp311-win32.whl",url = "https://files.pythonhosted.org/packages/22/6d/db4abec29e7a567455344433d095fdb39c97db6955bb4a2c432e486b4d28/xxhash-3.5.0-cp311-cp311-win32.whl",hashes = {sha256 = "109b436096d0a2dd039c355fa3414160ec4d843dfecc64a14077332a00aeb7da"}}, - {name = "xxhash-3.5.0-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/52/1c/fa3b61c0cf03e1da4767213672efe186b1dfa4fc901a4a694fb184a513d1/xxhash-3.5.0-cp311-cp311-win_amd64.whl",hashes = {sha256 = "b702f806693201ad6c0a05ddbbe4c8f359626d0b3305f766077d51388a6bac58"}}, - {name = "xxhash-3.5.0-cp311-cp311-win_arm64.whl",url = "https://files.pythonhosted.org/packages/6b/8e/9e6fc572acf6e1cc7ccb01973c213f895cb8668a9d4c2b58a99350da14b7/xxhash-3.5.0-cp311-cp311-win_arm64.whl",hashes = {sha256 = "c4dcb4120d0cc3cc448624147dba64e9021b278c63e34a38789b688fd0da9bf3"}}, - {name = "xxhash-3.5.0-cp310-cp310-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/bb/8a/0e9feca390d512d293afd844d31670e25608c4a901e10202aa98785eab09/xxhash-3.5.0-cp310-cp310-macosx_10_9_x86_64.whl",hashes = {sha256 = "ece616532c499ee9afbb83078b1b952beffef121d989841f7f4b3dc5ac0fd212"}}, - {name = "xxhash-3.5.0-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/16/e6/be5aa49580cd064a18200ab78e29b88b1127e1a8c7955eb8ecf81f2626eb/xxhash-3.5.0-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "3171f693dbc2cef6477054a665dc255d996646b4023fe56cb4db80e26f4cc520"}}, - {name = "xxhash-3.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/20/ee/b8a99ebbc6d1113b3a3f09e747fa318c3cde5b04bd9c197688fadf0eeae8/xxhash-3.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "7c5d3e570ef46adaf93fc81b44aca6002b5a4d8ca11bd0580c07eac537f36680"}}, - {name = "xxhash-3.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/58/62/15d10582ef159283a5c2b47f6d799fc3303fe3911d5bb0bcc820e1ef7ff4/xxhash-3.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "7cb29a034301e2982df8b1fe6328a84f4b676106a13e9135a0d7e0c3e9f806da"}}, - {name = "xxhash-3.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/23/41/61202663ea9b1bd8e53673b8ec9e2619989353dba8cfb68e59a9cbd9ffe3/xxhash-3.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "5d0d307d27099bb0cbeea7260eb39ed4fdb99c5542e21e94bb6fd29e49c57a23"}}, - {name = "xxhash-3.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/f2/07/d9a3059f702dec5b3b703737afb6dda32f304f6e9da181a229dafd052c29/xxhash-3.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "c0342aafd421795d740e514bc9858ebddfc705a75a8c5046ac56d85fe97bf196"}}, - {name = "xxhash-3.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/eb/58/27caadf78226ecf1d62dbd0c01d152ed381c14c1ee4ad01f0d460fc40eac/xxhash-3.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "3dbbd9892c5ebffeca1ed620cf0ade13eb55a0d8c84e0751a6653adc6ac40d0c"}}, - {name = "xxhash-3.5.0-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/b1/08/32d558ce23e1e068453c39aed7b3c1cdc690c177873ec0ca3a90d5808765/xxhash-3.5.0-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "4cc2d67fdb4d057730c75a64c5923abfa17775ae234a71b0200346bfb0a7f482"}}, - {name = "xxhash-3.5.0-cp310-cp310-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/3f/d4/2b971e2d2b0a61045f842b622ef11e94096cf1f12cd448b6fd426e80e0e2/xxhash-3.5.0-cp310-cp310-musllinux_1_2_i686.whl",hashes = {sha256 = "ec28adb204b759306a3d64358a5e5c07d7b1dd0ccbce04aa76cb9377b7b70296"}}, - {name = "xxhash-3.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/19/ae/6a6438864a8c4c39915d7b65effd85392ebe22710412902487e51769146d/xxhash-3.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "1328f6d8cca2b86acb14104e381225a3d7b42c92c4b86ceae814e5c400dbb415"}}, - {name = "xxhash-3.5.0-cp310-cp310-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/48/7d/b3c27c27d1fc868094d02fe4498ccce8cec9fcc591825c01d6bcb0b4fc49/xxhash-3.5.0-cp310-cp310-musllinux_1_2_s390x.whl",hashes = {sha256 = "8d47ebd9f5d9607fd039c1fbf4994e3b071ea23eff42f4ecef246ab2b7334198"}}, - {name = "xxhash-3.5.0-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/a1/05/918f9e7d2fbbd334b829997045d341d6239b563c44e683b9a7ef8fe50f5d/xxhash-3.5.0-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "b96d559e0fcddd3343c510a0fe2b127fbff16bf346dd76280b82292567523442"}}, - {name = "xxhash-3.5.0-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/08/29/dfe393805b2f86bfc47c290b275f0b7c189dc2f4e136fd4754f32eb18a8d/xxhash-3.5.0-cp310-cp310-win32.whl",hashes = {sha256 = "61c722ed8d49ac9bc26c7071eeaa1f6ff24053d553146d5df031802deffd03da"}}, - {name = "xxhash-3.5.0-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/7b/d7/aa0b22c4ebb7c3ccb993d4c565132abc641cd11164f8952d89eb6a501909/xxhash-3.5.0-cp310-cp310-win_amd64.whl",hashes = {sha256 = "9bed5144c6923cc902cd14bb8963f2d5e034def4486ab0bbe1f58f03f042f9a9"}}, - {name = "xxhash-3.5.0-cp310-cp310-win_arm64.whl",url = "https://files.pythonhosted.org/packages/69/12/f969b81541ee91b55f1ce469d7ab55079593c80d04fd01691b550e535000/xxhash-3.5.0-cp310-cp310-win_arm64.whl",hashes = {sha256 = "893074d651cf25c1cc14e3bea4fceefd67f2921b1bb8e40fcfeba56820de80c6"}}, - {name = "xxhash-3.5.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl",url = "https://files.pythonhosted.org/packages/ab/9a/233606bada5bd6f50b2b72c45de3d9868ad551e83893d2ac86dc7bb8553a/xxhash-3.5.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl",hashes = {sha256 = "2014c5b3ff15e64feecb6b713af12093f75b7926049e26a580e94dcad3c73d8c"}}, - {name = "xxhash-3.5.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/0c/67/f75276ca39e2c6604e3bee6c84e9db8a56a4973fde9bf35989787cf6e8aa/xxhash-3.5.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "fab81ef75003eda96239a23eda4e4543cedc22e34c373edcaf744e721a163986"}}, - {name = "xxhash-3.5.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/0f/f8/f6c61fd794229cc3848d144f73754a0c107854372d7261419dcbbd286299/xxhash-3.5.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "4e2febf914ace002132aa09169cc572e0d8959d0f305f93d5828c4836f9bc5a6"}}, - {name = "xxhash-3.5.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/79/d3/c029c99801526f859e6b38d34ab87c08993bf3dcea34b11275775001638a/xxhash-3.5.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "5d3a10609c51da2a1c0ea0293fc3968ca0a18bd73838455b5bca3069d7f8e32b"}}, - {name = "xxhash-3.5.0-pp310-pypy310_pp73-win_amd64.whl",url = "https://files.pythonhosted.org/packages/62/e3/bef7b82c1997579c94de9ac5ea7626d01ae5858aa22bf4fcb38bf220cb3e/xxhash-3.5.0-pp310-pypy310_pp73-win_amd64.whl",hashes = {sha256 = "5a74f23335b9689b66eb6dbe2a931a88fcd7a4c2cc4b1cb0edba8ce381c7a1da"}}, - {name = "xxhash-3.5.0-cp39-cp39-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/d4/f6/531dd6858adf8877675270b9d6989b6dacfd1c2d7135b17584fc29866df3/xxhash-3.5.0-cp39-cp39-macosx_10_9_x86_64.whl",hashes = {sha256 = "bfc8cdd7f33d57f0468b0614ae634cc38ab9202c6957a60e31d285a71ebe0301"}}, - {name = "xxhash-3.5.0-cp39-cp39-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/7c/a8/b2a42b6c9ae46e233f474f3d307c2e7bca8d9817650babeca048d2ad01d6/xxhash-3.5.0-cp39-cp39-macosx_11_0_arm64.whl",hashes = {sha256 = "e0c48b6300cd0b0106bf49169c3e0536408dfbeb1ccb53180068a18b03c662ab"}}, - {name = "xxhash-3.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/b4/92/9ac297e3487818f429bcf369c1c6a097edf5b56ed6fc1feff4c1882e87ef/xxhash-3.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "fe1a92cfbaa0a1253e339ccec42dbe6db262615e52df591b68726ab10338003f"}}, - {name = "xxhash-3.5.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/86/48/c1426dd3c86fc4a52f983301867463472f6a9013fb32d15991e60c9919b6/xxhash-3.5.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "33513d6cc3ed3b559134fb307aae9bdd94d7e7c02907b37896a6c45ff9ce51bd"}}, - {name = "xxhash-3.5.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/f3/de/0ab8c79993765c94fc0d0c1a22b454483c58a0161e1b562f58b654f47660/xxhash-3.5.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "eefc37f6138f522e771ac6db71a6d4838ec7933939676f3753eafd7d3f4c40bc"}}, - {name = "xxhash-3.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/b4/b4/332647451ed7d2c021294b7c1e9c144dbb5586b1fb214ad4f5a404642835/xxhash-3.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "a606c8070ada8aa2a88e181773fa1ef17ba65ce5dd168b9d08038e2a61b33754"}}, - {name = "xxhash-3.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/f4/1c/a42c0a6cac752f84f7b44a90d1a9fa9047cf70bdba5198a304fde7cc471f/xxhash-3.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "42eca420c8fa072cc1dd62597635d140e78e384a79bb4944f825fbef8bfeeef6"}}, - {name = "xxhash-3.5.0-cp39-cp39-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/c4/d7/04e1b0daae9dc9b02c73c1664cc8aa527498c3f66ccbc586eeb25bbe9f14/xxhash-3.5.0-cp39-cp39-musllinux_1_2_aarch64.whl",hashes = {sha256 = "604253b2143e13218ff1ef0b59ce67f18b8bd1c4205d2ffda22b09b426386898"}}, - {name = "xxhash-3.5.0-cp39-cp39-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/c4/f4/05e15e67505228fc19ee98a79e427b3a0b9695f5567cd66ced5d66389883/xxhash-3.5.0-cp39-cp39-musllinux_1_2_i686.whl",hashes = {sha256 = "6e93a5ad22f434d7876665444a97e713a8f60b5b1a3521e8df11b98309bff833"}}, - {name = "xxhash-3.5.0-cp39-cp39-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/94/fb/e9028d3645bba5412a09de13ee36df276a567e60bdb31d499dafa46d76ae/xxhash-3.5.0-cp39-cp39-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "7a46e1d6d2817ba8024de44c4fd79913a90e5f7265434cef97026215b7d30df6"}}, - {name = "xxhash-3.5.0-cp39-cp39-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/02/2c/18c6a622429368274739372d2f86c8125413ec169025c7d8ffb051784bba/xxhash-3.5.0-cp39-cp39-musllinux_1_2_s390x.whl",hashes = {sha256 = "30eb2efe6503c379b7ab99c81ba4a779748e3830241f032ab46bd182bf5873af"}}, - {name = "xxhash-3.5.0-cp39-cp39-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/72/bb/5b55c391084a0321c3809632a018b9b657e59d5966289664f85a645942ac/xxhash-3.5.0-cp39-cp39-musllinux_1_2_x86_64.whl",hashes = {sha256 = "c8aa771ff2c13dd9cda8166d685d7333d389fae30a4d2bb39d63ab5775de8606"}}, - {name = "xxhash-3.5.0-cp39-cp39-win32.whl",url = "https://files.pythonhosted.org/packages/86/2b/915049db13401792fec159f57e4f4a5ca7a9768e83ef71d6645b9d0cd749/xxhash-3.5.0-cp39-cp39-win32.whl",hashes = {sha256 = "5ed9ebc46f24cf91034544b26b131241b699edbfc99ec5e7f8f3d02d6eb7fba4"}}, - {name = "xxhash-3.5.0-cp39-cp39-win_amd64.whl",url = "https://files.pythonhosted.org/packages/d5/87/382ef7b24917d7cf4c540ee30f29b283bc87ac5893d2f89b23ea3cdf7d77/xxhash-3.5.0-cp39-cp39-win_amd64.whl",hashes = {sha256 = "220f3f896c6b8d0316f63f16c077d52c412619e475f9372333474ee15133a558"}}, - {name = "xxhash-3.5.0-cp39-cp39-win_arm64.whl",url = "https://files.pythonhosted.org/packages/e2/47/d06b24e2d9c3dcabccfd734d11b5bbebfdf59ceac2c61509d8205dd20ac6/xxhash-3.5.0-cp39-cp39-win_arm64.whl",hashes = {sha256 = "a7b1d8315d9b5e9f89eb2933b73afae6ec9597a258d52190944437158b49d38e"}}, - {name = "xxhash-3.5.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl",url = "https://files.pythonhosted.org/packages/c2/56/30d3df421814947f9d782b20c9b7e5e957f3791cbd89874578011daafcbd/xxhash-3.5.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl",hashes = {sha256 = "531af8845aaadcadf951b7e0c1345c6b9c68a990eeb74ff9acd8501a0ad6a1c9"}}, - {name = "xxhash-3.5.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/82/dd/3c42a1f022ad0d82c852d3cb65493ebac03dcfa8c994465a5fb052b00e3c/xxhash-3.5.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "7ce379bcaa9fcc00f19affa7773084dd09f5b59947b3fb47a1ceb0179f91aaa1"}}, - {name = "xxhash-3.5.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/b2/40/8f902ab3bebda228a9b4de69eba988280285a7f7f167b942bc20bb562df9/xxhash-3.5.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "fd1b2281d01723f076df3c8188f43f2472248a6b63118b036e641243656b1b0f"}}, - {name = "xxhash-3.5.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/db/87/bd06beb8ccaa0e9e577c9b909a49cfa5c5cd2ca46034342d72dd9ce5bc56/xxhash-3.5.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "9c770750cc80e8694492244bca7251385188bc5597b6a39d98a9f30e8da984e0"}}, - {name = "xxhash-3.5.0-pp39-pypy39_pp73-win_amd64.whl",url = "https://files.pythonhosted.org/packages/bb/f8/505385e2fbd753ddcaafd5550eabe86f6232cbebabad3b2508d411b19153/xxhash-3.5.0-pp39-pypy39_pp73-win_amd64.whl",hashes = {sha256 = "b150b8467852e1bd844387459aa6fbe11d7f38b56e901f9f3b3e6aba0d660240"}}, ] marker = "\"default\" in dependency_groups" @@ -3522,6 +3492,18 @@ version = "1.13.1" requires-python = ">=3.9" sdist = {name = "scipy-1.13.1.tar.gz", url = "https://files.pythonhosted.org/packages/ae/00/48c2f661e2816ccf2ecd77982f6605b2950afe60f60a52b4cbbc2504aa8f/scipy-1.13.1.tar.gz", hashes = {sha256 = "095a87a0312b08dfd6a6155cbbd310a8c51800fc931b8c0b84003014b874ed3c"}} wheels = [ + {name = "scipy-1.13.1-cp311-cp311-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/b4/15/4a4bb1b15bbd2cd2786c4f46e76b871b28799b67891f23f455323a0cdcfb/scipy-1.13.1-cp311-cp311-macosx_10_9_x86_64.whl",hashes = {sha256 = "27e52b09c0d3a1d5b63e1105f24177e544a222b43611aaf5bc44d4a0979e32f9"}}, + {name = "scipy-1.13.1-cp311-cp311-macosx_12_0_arm64.whl",url = "https://files.pythonhosted.org/packages/ba/92/42476de1af309c27710004f5cdebc27bec62c204db42e05b23a302cb0c9a/scipy-1.13.1-cp311-cp311-macosx_12_0_arm64.whl",hashes = {sha256 = "54f430b00f0133e2224c3ba42b805bfd0086fe488835effa33fa291561932326"}}, + {name = "scipy-1.13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/80/ba/8be64fe225360a4beb6840f3cbee494c107c0887f33350d0a47d55400b01/scipy-1.13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "e89369d27f9e7b0884ae559a3a956e77c02114cc60a6058b4e5011572eea9299"}}, + {name = "scipy-1.13.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/36/07/035d22ff9795129c5a847c64cb43c1fa9188826b59344fee28a3ab02e283/scipy-1.13.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "a78b4b3345f1b6f68a763c6e25c0c9a23a9fd0f39f5f3d200efe8feda560a5fa"}}, + {name = "scipy-1.13.1-cp311-cp311-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/d9/10/f9b43de37e5ed91facc0cfff31d45ed0104f359e4f9a68416cbf4e790241/scipy-1.13.1-cp311-cp311-musllinux_1_1_x86_64.whl",hashes = {sha256 = "45484bee6d65633752c490404513b9ef02475b4284c4cfab0ef946def50b3f59"}}, + {name = "scipy-1.13.1-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/4a/48/4513a1a5623a23e95f94abd675ed91cfb19989c58e9f6f7d03990f6caf3d/scipy-1.13.1-cp311-cp311-win_amd64.whl",hashes = {sha256 = "5713f62f781eebd8d597eb3f88b8bf9274e79eeabf63afb4a737abc6c84ad37b"}}, + {name = "scipy-1.13.1-cp310-cp310-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/33/59/41b2529908c002ade869623b87eecff3e11e3ce62e996d0bdcb536984187/scipy-1.13.1-cp310-cp310-macosx_10_9_x86_64.whl",hashes = {sha256 = "20335853b85e9a49ff7572ab453794298bcf0354d8068c5f6775a0eabf350aca"}}, + {name = "scipy-1.13.1-cp310-cp310-macosx_12_0_arm64.whl",url = "https://files.pythonhosted.org/packages/d5/33/f1307601f492f764062ce7dd471a14750f3360e33cd0f8c614dae208492c/scipy-1.13.1-cp310-cp310-macosx_12_0_arm64.whl",hashes = {sha256 = "d605e9c23906d1994f55ace80e0125c587f96c020037ea6aa98d01b4bd2e222f"}}, + {name = "scipy-1.13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/c0/66/9cd4f501dd5ea03e4a4572ecd874936d0da296bd04d1c45ae1a4a75d9c3a/scipy-1.13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "cfa31f1def5c819b19ecc3a8b52d28ffdcc7ed52bb20c9a7589669dd3c250989"}}, + {name = "scipy-1.13.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/a3/ba/7255e5dc82a65adbe83771c72f384d99c43063648456796436c9a5585ec3/scipy-1.13.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "f26264b282b9da0952a024ae34710c2aff7d27480ee91a2e82b7b7073c24722f"}}, + {name = "scipy-1.13.1-cp310-cp310-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/49/a5/bb9ded8326e9f0cdfdc412eeda1054b914dfea952bda2097d174f8832cc0/scipy-1.13.1-cp310-cp310-musllinux_1_1_x86_64.whl",hashes = {sha256 = "eccfa1906eacc02de42d70ef4aecea45415f5be17e72b61bafcfd329bdc52e94"}}, + {name = "scipy-1.13.1-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/12/30/df7a8fcc08f9b4a83f5f27cfaaa7d43f9a2d2ad0b6562cced433e5b04e31/scipy-1.13.1-cp310-cp310-win_amd64.whl",hashes = {sha256 = "2831f0dc9c5ea9edd6e51e6e769b655f08ec6db6e2e10f86ef39bd32eb11da54"}}, {name = "scipy-1.13.1-cp39-cp39-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/7f/29/c2ea58c9731b9ecb30b6738113a95d147e83922986b34c685b8f6eefde21/scipy-1.13.1-cp39-cp39-macosx_10_9_x86_64.whl",hashes = {sha256 = "436bbb42a94a8aeef855d755ce5a465479c721e9d684de76bf61a62e7c2b81d5"}}, {name = "scipy-1.13.1-cp39-cp39-macosx_12_0_arm64.whl",url = "https://files.pythonhosted.org/packages/5c/c0/e71b94b20ccf9effb38d7147c0064c08c622309fd487b1b677771a97d18c/scipy-1.13.1-cp39-cp39-macosx_12_0_arm64.whl",hashes = {sha256 = "8335549ebbca860c52bf3d02f80784e91a004b71b059e3eea9678ba994796a24"}}, {name = "scipy-1.13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/6d/0f/aaa55b06d474817cea311e7b10aab2ea1fd5d43bc6a2861ccc9caec9f418/scipy-1.13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "d533654b7d221a6a97304ab63c41c96473ff04459e404b83275b60aa8f4b7004"}}, @@ -3529,7 +3511,7 @@ wheels = [ {name = "scipy-1.13.1-cp39-cp39-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/8d/02/1165905f14962174e6569076bcc3315809ae1291ed14de6448cc151eedfd/scipy-1.13.1-cp39-cp39-musllinux_1_1_x86_64.whl",hashes = {sha256 = "a014c2b3697bde71724244f63de2476925596c24285c7a637364761f8710891c"}}, {name = "scipy-1.13.1-cp39-cp39-win_amd64.whl",url = "https://files.pythonhosted.org/packages/3e/77/dab54fe647a08ee4253963bcd8f9cf17509c8ca64d6335141422fe2e2114/scipy-1.13.1-cp39-cp39-win_amd64.whl",hashes = {sha256 = "392e4ec766654852c25ebad4f64e4e584cf19820b980bc04960bca0b0cd6eaa2"}}, ] -marker = "python_version < \"3.10\" and python_version >= \"3.9\" and \"dev\" in extras" +marker = "python_version < \"3.12\" and python_version >= \"3.9\" and \"default\" in dependency_groups or python_version < \"3.12\" and python_version >= \"3.9\" and \"dev\" in extras" [packages.tool.pdm] dependencies = [ @@ -3542,6 +3524,26 @@ version = "2.0.2" requires-python = ">=3.9" sdist = {name = "numpy-2.0.2.tar.gz", url = "https://files.pythonhosted.org/packages/a9/75/10dd1f8116a8b796cb2c737b674e02d02e80454bda953fa7e65d8c12b016/numpy-2.0.2.tar.gz", hashes = {sha256 = "883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78"}} wheels = [ + {name = "numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/8b/cf/034500fb83041aa0286e0fb16e7c76e5c8b67c0711bb6e9e9737a717d5fe/numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl",hashes = {sha256 = "49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448"}}, + {name = "numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/4a/d9/32de45561811a4b87fbdee23b5797394e3d1504b4a7cf40c10199848893e/numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195"}}, + {name = "numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/c1/ca/2f384720020c7b244d22508cb7ab23d95f179fcfff33c31a6eeba8d6c512/numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl",hashes = {sha256 = "807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57"}}, + {name = "numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/0e/78/a3e4f9fb6aa4e6fdca0c5428e8ba039408514388cf62d89651aade838269/numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl",hashes = {sha256 = "8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a"}}, + {name = "numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/a0/72/cfc3a1beb2caf4efc9d0b38a15fe34025230da27e1c08cc2eb9bfb1c7231/numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669"}}, + {name = "numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/ba/a8/c17acf65a931ce551fee11b72e8de63bf7e8a6f0e21add4c937c83563538/numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951"}}, + {name = "numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/ba/86/8767f3d54f6ae0165749f84648da9dcc8cd78ab65d415494962c86fac80f/numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl",hashes = {sha256 = "9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9"}}, + {name = "numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/df/87/f76450e6e1c14e5bb1eae6836478b1028e096fd02e85c1c37674606ab752/numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15"}}, + {name = "numpy-2.0.2-cp311-cp311-win32.whl",url = "https://files.pythonhosted.org/packages/5c/ca/0f0f328e1e59f73754f06e1adfb909de43726d4f24c6a3f8805f34f2b0fa/numpy-2.0.2-cp311-cp311-win32.whl",hashes = {sha256 = "a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4"}}, + {name = "numpy-2.0.2-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/eb/57/3a3f14d3a759dcf9bf6e9eda905794726b758819df4663f217d658a58695/numpy-2.0.2-cp311-cp311-win_amd64.whl",hashes = {sha256 = "286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc"}}, + {name = "numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/21/91/3495b3237510f79f5d81f2508f9f13fea78ebfdf07538fc7444badda173d/numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl",hashes = {sha256 = "51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece"}}, + {name = "numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/05/33/26178c7d437a87082d11019292dce6d3fe6f0e9026b7b2309cbf3e489b1d/numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04"}}, + {name = "numpy-2.0.2-cp310-cp310-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/ec/31/cc46e13bf07644efc7a4bf68df2df5fb2a1a88d0cd0da9ddc84dc0033e51/numpy-2.0.2-cp310-cp310-macosx_14_0_arm64.whl",hashes = {sha256 = "8c5713284ce4e282544c68d1c3b2c7161d38c256d2eefc93c1d683cf47683e66"}}, + {name = "numpy-2.0.2-cp310-cp310-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/6e/16/7bfcebf27bb4f9d7ec67332ffebee4d1bf085c84246552d52dbb548600e7/numpy-2.0.2-cp310-cp310-macosx_14_0_x86_64.whl",hashes = {sha256 = "becfae3ddd30736fe1889a37f1f580e245ba79a5855bff5f2a29cb3ccc22dd7b"}}, + {name = "numpy-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/f9/a3/561c531c0e8bf082c5bef509d00d56f82e0ea7e1e3e3a7fc8fa78742a6e5/numpy-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "2da5960c3cf0df7eafefd806d4e612c5e19358de82cb3c343631188991566ccd"}}, + {name = "numpy-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/fa/66/f7177ab331876200ac7563a580140643d1179c8b4b6a6b0fc9838de2a9b8/numpy-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "496f71341824ed9f3d2fd36cf3ac57ae2e0165c143b55c3a035ee219413f3318"}}, + {name = "numpy-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/25/7f/0b209498009ad6453e4efc2c65bcdf0ae08a182b2b7877d7ab38a92dc542/numpy-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl",hashes = {sha256 = "a61ec659f68ae254e4d237816e33171497e978140353c0c2038d46e63282d0c8"}}, + {name = "numpy-2.0.2-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/3e/df/2619393b1e1b565cd2d4c4403bdd979621e2c4dea1f8532754b2598ed63b/numpy-2.0.2-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "d731a1c6116ba289c1e9ee714b08a8ff882944d4ad631fd411106a30f083c326"}}, + {name = "numpy-2.0.2-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/22/ad/77e921b9f256d5da36424ffb711ae79ca3f451ff8489eeca544d0701d74a/numpy-2.0.2-cp310-cp310-win32.whl",hashes = {sha256 = "984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97"}}, + {name = "numpy-2.0.2-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/10/05/3442317535028bc29cf0c0dd4c191a4481e8376e9f0db6bcf29703cadae6/numpy-2.0.2-cp310-cp310-win_amd64.whl",hashes = {sha256 = "c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131"}}, {name = "numpy-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/43/c1/41c8f6df3162b0c6ffd4437d729115704bd43363de0090c7f913cfbc2d89/numpy-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl",hashes = {sha256 = "9059e10581ce4093f735ed23f3b9d283b9d517ff46009ddd485f1747eb22653c"}}, {name = "numpy-2.0.2-cp39-cp39-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/39/bc/fd298f308dcd232b56a4031fd6ddf11c43f9917fbc937e53762f7b5a3bb1/numpy-2.0.2-cp39-cp39-macosx_11_0_arm64.whl",hashes = {sha256 = "423e89b23490805d2a5a96fe40ec507407b8ee786d66f7328be214f9679df6dd"}}, {name = "numpy-2.0.2-cp39-cp39-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/96/ff/06d1aa3eeb1c614eda245c1ba4fb88c483bee6520d361641331872ac4b82/numpy-2.0.2-cp39-cp39-macosx_14_0_arm64.whl",hashes = {sha256 = "2b2955fa6f11907cf7a70dab0d0755159bca87755e831e47932367fc8f2f2d0b"}}, @@ -3557,17 +3559,212 @@ wheels = [ {name = "numpy-2.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/12/46/de1fbd0c1b5ccaa7f9a005b66761533e2f6a3e560096682683a223631fe9/numpy-2.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "26df23238872200f63518dd2aa984cfca675d82469535dc7162dc2ee52d9dd5c"}}, {name = "numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl",url = "https://files.pythonhosted.org/packages/cc/dc/d330a6faefd92b446ec0f0dfea4c3207bb1fef3c4771d19cf4543efd2c78/numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl",hashes = {sha256 = "a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385"}}, ] -marker = "python_version < \"3.10\" and python_version >= \"3.9\" and \"default\" in dependency_groups or python_version < \"3.10\" and python_version >= \"3.9\" and \"dev\" in extras" +marker = "python_version < \"3.12\" and python_version >= \"3.9\" and \"default\" in dependency_groups or python_version < \"3.12\" and python_version >= \"3.9\" and \"dev\" in extras" + +[packages.tool.pdm] +dependencies = [] + +[[packages]] +name = "tomli" +version = "2.2.1" +requires-python = ">=3.8" +sdist = {name = "tomli-2.2.1.tar.gz", url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hashes = {sha256 = "cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}} +wheels = [ + {name = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl",hashes = {sha256 = "678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}}, + {name = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}}, + {name = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}}, + {name = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}}, + {name = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}}, + {name = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}}, + {name = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl",hashes = {sha256 = "e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}}, + {name = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}}, + {name = "tomli-2.2.1-cp311-cp311-win32.whl",url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl",hashes = {sha256 = "465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}}, + {name = "tomli-2.2.1-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl",hashes = {sha256 = "2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}}, + {name = "tomli-2.2.1-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl",hashes = {sha256 = "cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}}, +] +marker = "python_version < \"3.11\" and python_version >= \"3.9\" and \"dev\" in extras" + +[packages.tool.pdm] +dependencies = [] + +[[packages]] +name = "importlib-metadata" +version = "8.7.0" +requires-python = ">=3.9" +sdist = {name = "importlib_metadata-8.7.0.tar.gz", url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hashes = {sha256 = "d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000"}} +wheels = [ + {name = "importlib_metadata-8.7.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl",hashes = {sha256 = "e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd"}}, +] +marker = "python_full_version < \"3.10.2\" and python_version >= \"3.9\" and \"dev\" in extras" + +[packages.tool.pdm] +dependencies = [ + "zipp>=3.20", + "typing-extensions>=3.6.4; python_version < \"3.8\"", +] + +[[packages]] +name = "backports-asyncio-runner" +version = "1.2.0" +requires-python = "<3.11,>=3.8" +sdist = {name = "backports_asyncio_runner-1.2.0.tar.gz", url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hashes = {sha256 = "a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162"}} +wheels = [ + {name = "backports_asyncio_runner-1.2.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl",hashes = {sha256 = "0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5"}}, +] +marker = "python_version < \"3.11\" and python_version >= \"3.9\" and \"dev\" in extras" + +[packages.tool.pdm] +dependencies = [] + +[[packages]] +name = "async-timeout" +version = "5.0.1" +requires-python = ">=3.8" +sdist = {name = "async_timeout-5.0.1.tar.gz", url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hashes = {sha256 = "d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}} +wheels = [ + {name = "async_timeout-5.0.1-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl",hashes = {sha256 = "39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}}, +] +marker = "python_version < \"3.11\" and python_version >= \"3.9\" and \"default\" in dependency_groups or python_version < \"3.11\" and python_version >= \"3.9\" and \"dev\" in extras" + +[packages.tool.pdm] +dependencies = [] + +[[packages]] +name = "exceptiongroup" +version = "1.3.0" +requires-python = ">=3.7" +sdist = {name = "exceptiongroup-1.3.0.tar.gz", url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hashes = {sha256 = "b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}} +wheels = [ + {name = "exceptiongroup-1.3.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl",hashes = {sha256 = "4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}}, +] +marker = "python_version < \"3.11\" and python_version >= \"3.9\" and \"default\" in dependency_groups or python_version < \"3.11\" and python_version >= \"3.9\" and \"dev\" in extras" + +[packages.tool.pdm] +dependencies = [ + "typing-extensions>=4.6.0; python_version < \"3.13\"", +] + +[[packages]] +name = "numba" +version = "0.60.0" +requires-python = ">=3.9" +sdist = {name = "numba-0.60.0.tar.gz", url = "https://files.pythonhosted.org/packages/3c/93/2849300a9184775ba274aba6f82f303343669b0592b7bb0849ea713dabb0/numba-0.60.0.tar.gz", hashes = {sha256 = "5df6158e5584eece5fc83294b949fd30b9f1125df7708862205217e068aabf16"}} +wheels = [ + {name = "numba-0.60.0-cp311-cp311-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/98/ad/df18d492a8f00d29a30db307904b9b296e37507034eedb523876f3a2e13e/numba-0.60.0-cp311-cp311-macosx_10_9_x86_64.whl",hashes = {sha256 = "a17b70fc9e380ee29c42717e8cc0bfaa5556c416d94f9aa96ba13acb41bdece8"}}, + {name = "numba-0.60.0-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/9a/51/a4dc2c01ce7a850b8e56ff6d5381d047a5daea83d12bad08aa071d34b2ee/numba-0.60.0-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "3fb02b344a2a80efa6f677aa5c40cd5dd452e1b35f8d1c2af0dfd9ada9978e4b"}}, + {name = "numba-0.60.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/f9/4c/8889ac94c0b33dca80bed11564b8c6d9ea14d7f094e674c58e5c5b05859b/numba-0.60.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "5f4fde652ea604ea3c86508a3fb31556a6157b2c76c8b51b1d45eb40c8598703"}}, + {name = "numba-0.60.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/57/03/2b4245b05b71c0cee667e6a0b51606dfa7f4157c9093d71c6b208385a611/numba-0.60.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "4142d7ac0210cc86432b818338a2bc368dc773a2f5cf1e32ff7c5b378bd63ee8"}}, + {name = "numba-0.60.0-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/79/89/2d924ca60dbf949f18a6fec223a2445f5f428d9a5f97a6b29c2122319015/numba-0.60.0-cp311-cp311-win_amd64.whl",hashes = {sha256 = "cac02c041e9b5bc8cf8f2034ff6f0dbafccd1ae9590dc146b3a02a45e53af4e2"}}, + {name = "numba-0.60.0-cp310-cp310-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/f7/cf/baa13a7e3556d73d9e38021e6d6aa4aeb30d8b94545aa8b70d0f24a1ccc4/numba-0.60.0-cp310-cp310-macosx_10_9_x86_64.whl",hashes = {sha256 = "5d761de835cd38fb400d2c26bb103a2726f548dc30368853121d66201672e651"}}, + {name = "numba-0.60.0-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/ac/ba/4b57fa498564457c3cc9fc9e570a6b08e6086c74220f24baaf04e54b995f/numba-0.60.0-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "159e618ef213fba758837f9837fb402bbe65326e60ba0633dbe6c7f274d42c1b"}}, + {name = "numba-0.60.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/28/98/7ea97ee75870a54f938a8c70f7e0be4495ba5349c5f9db09d467c4a5d5b7/numba-0.60.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "1527dc578b95c7c4ff248792ec33d097ba6bef9eda466c948b68dfc995c25781"}}, + {name = "numba-0.60.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/79/58/cb4ac5b8f7ec64200460aef1fed88258fb872ceef504ab1f989d2ff0f684/numba-0.60.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "fe0b28abb8d70f8160798f4de9d486143200f34458d34c4a214114e445d7124e"}}, + {name = "numba-0.60.0-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/1c/b0/c61a93ca947d12233ff45de506ddbf52af3f752066a0b8be4d27426e16da/numba-0.60.0-cp310-cp310-win_amd64.whl",hashes = {sha256 = "19407ced081d7e2e4b8d8c36aa57b7452e0283871c296e12d798852bc7d7f198"}}, + {name = "numba-0.60.0-cp39-cp39-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/68/1a/87c53f836cdf557083248c3f47212271f220280ff766538795e77c8c6bbf/numba-0.60.0-cp39-cp39-macosx_10_9_x86_64.whl",hashes = {sha256 = "01ef4cd7d83abe087d644eaa3d95831b777aa21d441a23703d649e06b8e06b74"}}, + {name = "numba-0.60.0-cp39-cp39-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/28/14/a5baa1f2edea7b49afa4dc1bb1b126645198cf1075186853b5b497be826e/numba-0.60.0-cp39-cp39-macosx_11_0_arm64.whl",hashes = {sha256 = "819a3dfd4630d95fd574036f99e47212a1af41cbcb019bf8afac63ff56834449"}}, + {name = "numba-0.60.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/3b/bd/f1985719ff34e37e07bb18f9d3acd17e5a21da255f550c8eae031e2ddf5f/numba-0.60.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "0b983bd6ad82fe868493012487f34eae8bf7dd94654951404114f23c3466d34b"}}, + {name = "numba-0.60.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/54/9b/cd73d3f6617ddc8398a63ef97d8dc9139a9879b9ca8a7ca4b8789056ea46/numba-0.60.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "c151748cd269ddeab66334bd754817ffc0cabd9433acb0f551697e5151917d25"}}, + {name = "numba-0.60.0-cp39-cp39-win_amd64.whl",url = "https://files.pythonhosted.org/packages/01/01/8b7b670c77c5ea0e47e283d82332969bf672ab6410d0b2610cac5b7a3ded/numba-0.60.0-cp39-cp39-win_amd64.whl",hashes = {sha256 = "3031547a015710140e8c87226b4cfe927cac199835e5bf7d4fe5cb64e814e3ab"}}, +] +marker = "python_version < \"3.12\" and python_version >= \"3.9\" and \"default\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [ + "llvmlite<0.44,>=0.43.0dev0", + "numpy<2.1,>=1.22", +] + +[[packages]] +name = "llvmlite" +version = "0.43.0" +requires-python = ">=3.9" +sdist = {name = "llvmlite-0.43.0.tar.gz", url = "https://files.pythonhosted.org/packages/9f/3d/f513755f285db51ab363a53e898b85562e950f79a2e6767a364530c2f645/llvmlite-0.43.0.tar.gz", hashes = {sha256 = "ae2b5b5c3ef67354824fb75517c8db5fbe93bc02cd9671f3c62271626bc041d5"}} +wheels = [ + {name = "llvmlite-0.43.0-cp311-cp311-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/95/8c/de3276d773ab6ce3ad676df5fab5aac19696b2956319d65d7dd88fb10f19/llvmlite-0.43.0-cp311-cp311-macosx_10_9_x86_64.whl",hashes = {sha256 = "3e8d0618cb9bfe40ac38a9633f2493d4d4e9fcc2f438d39a4e854f39cc0f5f98"}}, + {name = "llvmlite-0.43.0-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/ee/e1/38deed89ced4cf378c61e232265cfe933ccde56ae83c901aa68b477d14b1/llvmlite-0.43.0-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "e0a9a1a39d4bf3517f2af9d23d479b4175ead205c592ceeb8b89af48a327ea57"}}, + {name = "llvmlite-0.43.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/2f/b2/4429433eb2dc8379e2cb582502dca074c23837f8fd009907f78a24de4c25/llvmlite-0.43.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "c1da416ab53e4f7f3bc8d4eeba36d801cc1894b9fbfbf2022b29b6bad34a7df2"}}, + {name = "llvmlite-0.43.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/6b/99/5d00a7d671b1ba1751fc9f19d3b36f3300774c6eebe2bcdb5f6191763eb4/llvmlite-0.43.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "977525a1e5f4059316b183fb4fd34fa858c9eade31f165427a3977c95e3ee749"}}, + {name = "llvmlite-0.43.0-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/20/ab/ed5ed3688c6ba4f0b8d789da19fd8e30a9cf7fc5852effe311bc5aefe73e/llvmlite-0.43.0-cp311-cp311-win_amd64.whl",hashes = {sha256 = "d5bd550001d26450bd90777736c69d68c487d17bf371438f975229b2b8241a91"}}, + {name = "llvmlite-0.43.0-cp310-cp310-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/23/ff/6ca7e98998b573b4bd6566f15c35e5c8bea829663a6df0c7aa55ab559da9/llvmlite-0.43.0-cp310-cp310-macosx_10_9_x86_64.whl",hashes = {sha256 = "a289af9a1687c6cf463478f0fa8e8aa3b6fb813317b0d70bf1ed0759eab6f761"}}, + {name = "llvmlite-0.43.0-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/ca/5c/a27f9257f86f0cda3f764ff21d9f4217b9f6a0d45e7a39ecfa7905f524ce/llvmlite-0.43.0-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "6d4fd101f571a31acb1559ae1af30f30b1dc4b3186669f92ad780e17c81e91bc"}}, + {name = "llvmlite-0.43.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/7e/3c/4410f670ad0a911227ea2ecfcba9f672a77cf1924df5280c4562032ec32d/llvmlite-0.43.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "7d434ec7e2ce3cc8f452d1cd9a28591745de022f931d67be688a737320dfcead"}}, + {name = "llvmlite-0.43.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/c6/21/2ffbab5714e72f2483207b4a1de79b2eecd9debbf666ff4e7067bcc5c134/llvmlite-0.43.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "6912a87782acdff6eb8bf01675ed01d60ca1f2551f8176a300a886f09e836a6a"}}, + {name = "llvmlite-0.43.0-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/f2/26/b5478037c453554a61625ef1125f7e12bb1429ae11c6376f47beba9b0179/llvmlite-0.43.0-cp310-cp310-win_amd64.whl",hashes = {sha256 = "14f0e4bf2fd2d9a75a3534111e8ebeb08eda2f33e9bdd6dfa13282afacdde0ed"}}, + {name = "llvmlite-0.43.0-cp39-cp39-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/2a/73/12925b1bbb3c2beb6d96f892ef5b4d742c34f00ddb9f4a125e9e87b22f52/llvmlite-0.43.0-cp39-cp39-macosx_10_9_x86_64.whl",hashes = {sha256 = "9cd2a7376f7b3367019b664c21f0c61766219faa3b03731113ead75107f3b66c"}}, + {name = "llvmlite-0.43.0-cp39-cp39-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/cc/61/58c70aa0808a8cba825a7d98cc65bef4801b99328fba80837bfcb5fc767f/llvmlite-0.43.0-cp39-cp39-macosx_11_0_arm64.whl",hashes = {sha256 = "18e9953c748b105668487b7c81a3e97b046d8abf95c4ddc0cd3c94f4e4651ae8"}}, + {name = "llvmlite-0.43.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/c8/c6/9324eb5de2ba9d99cbed853d85ba7a318652a48e077797bec27cf40f911d/llvmlite-0.43.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "74937acd22dc11b33946b67dca7680e6d103d6e90eeaaaf932603bec6fe7b03a"}}, + {name = "llvmlite-0.43.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/e0/d0/889e9705107db7b1ec0767b03f15d7b95b4c4f9fdf91928ab1c7e9ffacf6/llvmlite-0.43.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "bc9efc739cc6ed760f795806f67889923f7274276f0eb45092a1473e40d9b867"}}, + {name = "llvmlite-0.43.0-cp39-cp39-win_amd64.whl",url = "https://files.pythonhosted.org/packages/df/41/73cc26a2634b538cfe813f618c91e7e9960b8c163f8f0c94a2b0f008b9da/llvmlite-0.43.0-cp39-cp39-win_amd64.whl",hashes = {sha256 = "47e147cdda9037f94b399bf03bfd8a6b6b1f2f90be94a454e3386f006455a9b4"}}, +] +marker = "python_version < \"3.12\" and python_version >= \"3.9\" and \"default\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [] + +[[packages]] +name = "scikit-learn" +version = "1.6.1" +requires-python = ">=3.9" +sdist = {name = "scikit_learn-1.6.1.tar.gz", url = "https://files.pythonhosted.org/packages/9e/a5/4ae3b3a0755f7b35a280ac90b28817d1f380318973cff14075ab41ef50d9/scikit_learn-1.6.1.tar.gz", hashes = {sha256 = "b4fc2525eca2c69a59260f583c56a7557c6ccdf8deafdba6e060f94c1c59738e"}} +wheels = [ + {name = "scikit_learn-1.6.1-cp311-cp311-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/6c/2a/e291c29670795406a824567d1dfc91db7b699799a002fdaa452bceea8f6e/scikit_learn-1.6.1-cp311-cp311-macosx_10_9_x86_64.whl",hashes = {sha256 = "72abc587c75234935e97d09aa4913a82f7b03ee0b74111dcc2881cba3c5a7b33"}}, + {name = "scikit_learn-1.6.1-cp311-cp311-macosx_12_0_arm64.whl",url = "https://files.pythonhosted.org/packages/25/92/ee1d7a00bb6b8c55755d4984fd82608603a3cc59959245068ce32e7fb808/scikit_learn-1.6.1-cp311-cp311-macosx_12_0_arm64.whl",hashes = {sha256 = "b3b00cdc8f1317b5f33191df1386c0befd16625f49d979fe77a8d44cae82410d"}}, + {name = "scikit_learn-1.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/30/cd/ed4399485ef364bb25f388ab438e3724e60dc218c547a407b6e90ccccaef/scikit_learn-1.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "dc4765af3386811c3ca21638f63b9cf5ecf66261cc4815c1db3f1e7dc7b79db2"}}, + {name = "scikit_learn-1.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/a8/f3/62fc9a5a659bb58a03cdd7e258956a5824bdc9b4bb3c5d932f55880be569/scikit_learn-1.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "25fc636bdaf1cc2f4a124a116312d837148b5e10872147bdaf4887926b8c03d8"}}, + {name = "scikit_learn-1.6.1-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/a1/a6/c5b78606743a1f28eae8f11973de6613a5ee87366796583fb74c67d54939/scikit_learn-1.6.1-cp311-cp311-win_amd64.whl",hashes = {sha256 = "fa909b1a36e000a03c382aade0bd2063fd5680ff8b8e501660c0f59f021a6415"}}, + {name = "scikit_learn-1.6.1-cp310-cp310-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/2e/3a/f4597eb41049110b21ebcbb0bcb43e4035017545daa5eedcfeb45c08b9c5/scikit_learn-1.6.1-cp310-cp310-macosx_10_9_x86_64.whl",hashes = {sha256 = "d056391530ccd1e501056160e3c9673b4da4805eb67eb2bdf4e983e1f9c9204e"}}, + {name = "scikit_learn-1.6.1-cp310-cp310-macosx_12_0_arm64.whl",url = "https://files.pythonhosted.org/packages/37/19/0423e5e1fd1c6ec5be2352ba05a537a473c1677f8188b9306097d684b327/scikit_learn-1.6.1-cp310-cp310-macosx_12_0_arm64.whl",hashes = {sha256 = "0c8d036eb937dbb568c6242fa598d551d88fb4399c0344d95c001980ec1c7d36"}}, + {name = "scikit_learn-1.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/70/95/d5cb2297a835b0f5fc9a77042b0a2d029866379091ab8b3f52cc62277808/scikit_learn-1.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "8634c4bd21a2a813e0a7e3900464e6d593162a29dd35d25bdf0103b3fce60ed5"}}, + {name = "scikit_learn-1.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/b7/91/ab3c697188f224d658969f678be86b0968ccc52774c8ab4a86a07be13c25/scikit_learn-1.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "775da975a471c4f6f467725dff0ced5c7ac7bda5e9316b260225b48475279a1b"}}, + {name = "scikit_learn-1.6.1-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/17/04/d5d556b6c88886c092cc989433b2bab62488e0f0dafe616a1d5c9cb0efb1/scikit_learn-1.6.1-cp310-cp310-win_amd64.whl",hashes = {sha256 = "8a600c31592bd7dab31e1c61b9bbd6dea1b3433e67d264d17ce1017dbdce8002"}}, + {name = "scikit_learn-1.6.1-cp39-cp39-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/d2/37/b305b759cc65829fe1b8853ff3e308b12cdd9d8884aa27840835560f2b42/scikit_learn-1.6.1-cp39-cp39-macosx_10_9_x86_64.whl",hashes = {sha256 = "6849dd3234e87f55dce1db34c89a810b489ead832aaf4d4550b7ea85628be6c1"}}, + {name = "scikit_learn-1.6.1-cp39-cp39-macosx_12_0_arm64.whl",url = "https://files.pythonhosted.org/packages/83/74/f64379a4ed5879d9db744fe37cfe1978c07c66684d2439c3060d19a536d8/scikit_learn-1.6.1-cp39-cp39-macosx_12_0_arm64.whl",hashes = {sha256 = "e7be3fa5d2eb9be7d77c3734ff1d599151bb523674be9b834e8da6abe132f44e"}}, + {name = "scikit_learn-1.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/fd/dc/d5457e03dc9c971ce2b0d750e33148dd060fefb8b7dc71acd6054e4bb51b/scikit_learn-1.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "44a17798172df1d3c1065e8fcf9019183f06c87609b49a124ebdf57ae6cb0107"}}, + {name = "scikit_learn-1.6.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/79/35/b1d2188967c3204c78fa79c9263668cf1b98060e8e58d1a730fe5b2317bb/scikit_learn-1.6.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "b8b7a3b86e411e4bce21186e1c180d792f3d99223dcfa3b4f597ecc92fa1a422"}}, + {name = "scikit_learn-1.6.1-cp39-cp39-win_amd64.whl",url = "https://files.pythonhosted.org/packages/fb/d8/8d603bdd26601f4b07e2363032b8565ab82eb857f93d86d0f7956fcf4523/scikit_learn-1.6.1-cp39-cp39-win_amd64.whl",hashes = {sha256 = "7a73d457070e3318e32bdb3aa79a8d990474f19035464dfd8bede2883ab5dc3b"}}, +] +marker = "python_version < \"3.12\" and python_version >= \"3.9\" and \"default\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [ + "numpy>=1.19.5", + "scipy>=1.6.0", + "joblib>=1.2.0", + "threadpoolctl>=3.1.0", +] + +[[packages]] +name = "zipp" +version = "3.23.0" +requires-python = ">=3.9" +sdist = {name = "zipp-3.23.0.tar.gz", url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hashes = {sha256 = "a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}} +wheels = [ + {name = "zipp-3.23.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl",hashes = {sha256 = "071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}}, +] +marker = "python_full_version < \"3.10.2\" and python_version >= \"3.9\" and \"dev\" in extras" + +[packages.tool.pdm] +dependencies = [] + +[[packages]] +name = "networkx" +version = "3.2.1" +requires-python = ">=3.9" +sdist = {name = "networkx-3.2.1.tar.gz", url = "https://files.pythonhosted.org/packages/c4/80/a84676339aaae2f1cfdf9f418701dd634aef9cc76f708ef55c36ff39c3ca/networkx-3.2.1.tar.gz", hashes = {sha256 = "9f1bb5cf3409bf324e0a722c20bdb4c20ee39bf1c30ce8ae499c8502b0b5e0c6"}} +wheels = [ + {name = "networkx-3.2.1-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/d5/f0/8fbc882ca80cf077f1b246c0e3c3465f7f415439bdea6b899f6b19f61f70/networkx-3.2.1-py3-none-any.whl",hashes = {sha256 = "f18c69adc97877c42332c170849c96cefa91881c99a7cb3e95b7c659ebdc1ec2"}}, +] +marker = "python_version < \"3.12\" and python_version >= \"3.9\" and \"default\" in dependency_groups" [packages.tool.pdm] dependencies = [] [tool.pdm] -hashes = {sha256 = "4909a619da3f004dcd3f4ece16e07e96f81709904464fc614e66b456c5f8c73e"} +hashes = {sha256 = "555b4bbcb733817760de0902c1c4437a46a7d37708dba2168675e50f454e6361"} strategy = ["inherit_metadata", "static_urls"] [[tool.pdm.targets]] -requires_python = "~=3.10" +requires_python = "~=3.12" [[tool.pdm.targets]] -requires_python = ">=3.9,<3.10" +requires_python = ">=3.9,<3.12" diff --git a/pyproject.toml b/pyproject.toml index fbe054ad..7237e66d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,13 @@ include = ["*"] [tool.pdm] distribution = true +[[tool.pdm.source]] +name = "torch" +type = "find_links" +#url = "https://download.pytorch.org/whl/cpu/torch_stable.html" +url = "https://download.pytorch.org/whl/cpu/torch/" +include_packages = ["torch"] + # ************************************************ # ********** Project Metadata ********** @@ -64,6 +71,8 @@ dependencies = [ "sanic", "transformers", "uvloop>=0.18", + "librosa>=0.11.0", + "torch>=2.8.0", ] [project.optional-dependencies] diff --git a/src/guidellm/data/deserializers/__init__.py b/src/guidellm/data/deserializers/__init__.py index fdee12ce..1062f2b7 100644 --- a/src/guidellm/data/deserializers/__init__.py +++ b/src/guidellm/data/deserializers/__init__.py @@ -25,6 +25,7 @@ SyntheticTextDatasetConfig, SyntheticTextDatasetDeserializer, SyntheticTextGenerator, + SyntheticTextPrefixBucketConfig, ) __all__ = [ @@ -46,6 +47,7 @@ "SyntheticTextDatasetConfig", "SyntheticTextDatasetDeserializer", "SyntheticTextGenerator", + "SyntheticTextPrefixBucketConfig", "TarFileDatasetDeserializer", "TextFileDatasetDeserializer", ] diff --git a/src/guidellm/data/deserializers/synthetic.py b/src/guidellm/data/deserializers/synthetic.py index 2335596d..a071eeea 100644 --- a/src/guidellm/data/deserializers/synthetic.py +++ b/src/guidellm/data/deserializers/synthetic.py @@ -1,13 +1,15 @@ from __future__ import annotations +import math from collections.abc import Iterator from pathlib import Path -from typing import Any, Callable +from random import Random +from typing import Any, Callable, Self import yaml from datasets import Features, IterableDataset, Value from faker import Faker -from pydantic import Field +from pydantic import ConfigDict, Field, model_validator from transformers import PreTrainedTokenizerBase from guidellm.data.deserializers.deserializer import ( @@ -21,10 +23,37 @@ "SyntheticTextDatasetConfig", "SyntheticTextDatasetDeserializer", "SyntheticTextGenerator", + "SyntheticTextPrefixBucketConfig", ] +class SyntheticTextPrefixBucketConfig(StandardBaseModel): + bucket_weight: int = Field( + description="Weight of this bucket in the overall distribution.", + gt=0, + default=100, + ) + prefix_count: int = Field( + description="The number of unique prefixes to generate for this bucket.", + ge=1, + default=1, + ) + prefix_tokens: int = Field( + description="The number of prefix tokens per-prompt for this bucket.", + ge=0, + default=0, + ) + + class SyntheticTextDatasetConfig(StandardBaseModel): + model_config = ConfigDict( + extra="allow", + ) + + prefix_buckets: list[SyntheticTextPrefixBucketConfig] | None = Field( + description="Buckets for the prefix tokens distribution.", + default=None, + ) prompt_tokens: int = Field( description="The average number of text tokens generated for prompts.", gt=0, @@ -68,6 +97,26 @@ class SyntheticTextDatasetConfig(StandardBaseModel): default="data:prideandprejudice.txt.gz", ) + @model_validator(mode="after") + def check_prefix_options(self) -> Self: + prefix_count = self.__pydantic_extra__.get("prefix_count", None) # type: ignore[attr-defined] + prefix_tokens = self.__pydantic_extra__.get("prefix_count", None) # type: ignore[attr-defined] + if prefix_count is not None or prefix_tokens is not None: + if self.prefix_buckets: + raise ValueError( + "prefix_buckets is mutually exclusive" + " with prefix_count and prefix_tokens" + ) + + self.prefix_buckets = [ + SyntheticTextPrefixBucketConfig( + prefix_count=prefix_count or 1, + prefix_tokens=prefix_tokens or 0, + ) + ] + + return self + class SyntheticTextGenerator: def __init__( @@ -104,20 +153,27 @@ def __iter__(self) -> Iterator[dict[str, Any]]: ) ) + # Create a shared prefix if specified + rand = Random(self.random_seed + 3) + prefix_iter = self._create_prefix_iter(faker, rand) + while True: prompt_tokens_count = next(prompt_tokens_sampler) output_tokens_count = next(output_tokens_sampler) yield { + "prefix": next(prefix_iter), "prompt": self._create_prompt( - prompt_tokens_count, samples_generated, faker + prompt_tokens_count, faker, f"{samples_generated} " ), "prompt_tokens_count": prompt_tokens_count, "output_tokens_count": output_tokens_count, } samples_generated += 1 - def _create_prompt(self, prompt_tokens_count: int, index: int, faker: Faker) -> str: + def _create_prompt( + self, prompt_tokens_count: int, faker: Faker, unique: str = "" + ) -> str: prompt_token_ids = [] avg_chars_per_token = 5 margin_of_safety = 1.5 @@ -128,13 +184,42 @@ def _create_prompt(self, prompt_tokens_count: int, index: int, faker: Faker) -> num_chars = ( prompt_tokens_count * avg_chars_per_token * margin_of_safety * attempts ) - text = f"{index} " + faker.text(max_nb_chars=num_chars) + text = unique + faker.text(max_nb_chars=num_chars) prompt_token_ids = self.processor.encode(text) return self.processor.decode( prompt_token_ids[:prompt_tokens_count], skip_special_tokens=True ) + def _create_prefix_iter(self, faker: Faker, rand: Random) -> Iterator[str]: + if not self.config.prefix_buckets: + while True: + yield "" + + # Increase weights to ensure an integer number of samples per per-prefix + least_common_prefix_count = math.lcm( + *(bucket.prefix_count for bucket in self.config.prefix_buckets) + ) + unnorm_weights = [ + least_common_prefix_count * bucket.bucket_weight // bucket.prefix_count + for bucket in self.config.prefix_buckets + ] + # Use GCD to reduce the weights to smallest integer ratio + common_divisor = math.gcd(*unnorm_weights) + + # Create prefix list maintaining the correct distribution + prefixes = [] + for bucket, weight in zip(self.config.prefix_buckets, unnorm_weights): + bucket_prefixes = [ + self._create_prompt(bucket.prefix_tokens, faker) + for _ in range(bucket.prefix_count) + ] + sample_count = weight // common_divisor + prefixes.extend(bucket_prefixes * sample_count) + + while True: + yield rand.choice(prefixes) + @DatasetDeserializerFactory.register("synthetic_text") class SyntheticTextDatasetDeserializer(DatasetDeserializer): @@ -166,6 +251,7 @@ def __call__( ), features=Features( { + "prefix": Value("string"), "prompt": Value("string"), "prompt_tokens_count": Value("int32"), "output_tokens_count": Value("int32"), diff --git a/src/guidellm/data/formatters/templates.py b/src/guidellm/data/formatters/templates.py index 2cf6e2f3..52db73b1 100644 --- a/src/guidellm/data/formatters/templates.py +++ b/src/guidellm/data/formatters/templates.py @@ -22,11 +22,7 @@ class JinjaTemplatesRegistry(RegistryMixin[Union[Template, str]]): textwrap.dedent(""" {% set obj = { "json_body": { - "prompt": ( - text_column[0] - if text_column and text_column|length == 1 - else text_column - ) + "prompt": prefix_column[0]|default("") + text_column[0] } } %} @@ -52,6 +48,10 @@ class JinjaTemplatesRegistry(RegistryMixin[Union[Template, str]]): {% set obj = { "json_body": { "messages": [ + { + "role": "system", + "content": prefix_column[0]|default("") + }, { "role": "user", "content": [] @@ -61,11 +61,11 @@ class JinjaTemplatesRegistry(RegistryMixin[Union[Template, str]]): } %} {%- for item in text_column or [] %} - {% do obj["json_body"].messages[0].content.append({"type": "text", "text": item}) %} + {% do obj["json_body"].messages[1].content.append({"type": "text", "text": item}) %} {%- endfor %} {%- for item in image_column or [] %} - {% do obj["json_body"].messages[0].content.append({ + {% do obj["json_body"].messages[1].content.append({ "type": "image_url", "image_url": encode_image( item, @@ -78,7 +78,7 @@ class JinjaTemplatesRegistry(RegistryMixin[Union[Template, str]]): {%- endfor %} {%- for item in video_column or [] %} - {% do obj["json_body"].messages[0].content.append({ + {% do obj["json_body"].messages[1].content.append({ "type": "video_url", "video_url": encode_video( item, diff --git a/src/guidellm/data/objects.py b/src/guidellm/data/objects.py index 04c5407d..b4a38719 100644 --- a/src/guidellm/data/objects.py +++ b/src/guidellm/data/objects.py @@ -31,6 +31,7 @@ GenerativeDatasetColumnType = Literal[ "prompt_tokens_count_column", "output_tokens_count_column", + "prefix_column", "text_column", "image_column", "video_column", @@ -195,6 +196,7 @@ class GenerativeDatasetArgs(StandardBaseDict): split: str | None = None prompt_tokens_count_column: str | None = None output_tokens_count_column: str | None = None + prefix_column: str | None = None text_column: str | list[str] | None = None image_column: str | list[str] | None = None video_column: str | list[str] | None = None diff --git a/src/guidellm/data/utils.py b/src/guidellm/data/utils.py index 7d53a054..d2fa1f9c 100644 --- a/src/guidellm/data/utils.py +++ b/src/guidellm/data/utils.py @@ -80,6 +80,11 @@ DEFAULT_COLUMN_NAMES: dict[str, list[str]] = { "prompt_tokens_count": ["prompt_tokens_count", "input_tokens_count"], "output_tokens_count": ["output_tokens_count", "completion_tokens_count"], + "prefix_column": [ + "system_prompt", + "system", + "prefix", + ], "text_column": [ "prompt", "instruction", diff --git a/tests/unit/dataset/__init__.py b/tests/unit/data/__init__.py similarity index 100% rename from tests/unit/dataset/__init__.py rename to tests/unit/data/__init__.py diff --git a/tests/unit/data/deserializers/__init__.py b/tests/unit/data/deserializers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/data/deserializers/test_synthetic.py b/tests/unit/data/deserializers/test_synthetic.py new file mode 100644 index 00000000..58b76aee --- /dev/null +++ b/tests/unit/data/deserializers/test_synthetic.py @@ -0,0 +1,587 @@ +""" +Unit tests for guidellm.data.deserializers.synthetic module. +""" + +import json +import tempfile +from pathlib import Path +from unittest.mock import Mock + +import pytest +import yaml +from datasets import IterableDataset + +from guidellm.data.deserializers.deserializer import DataNotSupportedError +from guidellm.data.deserializers.synthetic import ( + SyntheticTextDatasetConfig, + SyntheticTextDatasetDeserializer, + SyntheticTextGenerator, + SyntheticTextPrefixBucketConfig, +) + + +class TestPrefixBucketConfig: + """Test cases for PrefixBucketConfig class. + + ### WRITTEN BY AI ### + """ + + @pytest.mark.smoke + def test_creation_with_valid_params(self): + """Test creating PrefixBucketConfig with valid parameters. + + ### WRITTEN BY AI ### + """ + config = SyntheticTextPrefixBucketConfig( + bucket_weight=100, prefix_count=1, prefix_tokens=5 + ) + + assert config.bucket_weight == 100 + assert config.prefix_count == 1 + assert config.prefix_tokens == 5 + + @pytest.mark.sanity + def test_creation_with_negative_values(self): + """Test creating PrefixBucketConfig with negative values raises ValueError. + + ### WRITTEN BY AI ### + """ + with pytest.raises(ValueError): + SyntheticTextPrefixBucketConfig( + bucket_weight=-10, prefix_count=1, prefix_tokens=5 + ) + + with pytest.raises(ValueError): + SyntheticTextPrefixBucketConfig( + bucket_weight=100, prefix_count=-1, prefix_tokens=5 + ) + + with pytest.raises(ValueError): + SyntheticTextPrefixBucketConfig( + bucket_weight=100, prefix_count=1, prefix_tokens=-5 + ) + + @pytest.mark.regression + def test_prefix_bucket_zero_weight_error(self): + """Test that zero total weight raises an error. + + ### WRITTEN BY AI ### + """ + # Test validation error for creating PrefixBucketConfig with weight=0 + with pytest.raises(ValueError): + SyntheticTextPrefixBucketConfig( + bucket_weight=0, prefix_count=1, prefix_tokens=2 + ) + + @pytest.mark.sanity + def test_prefix_bucket_config_validation(self): + """Test PrefixBucketConfig validation. + + ### WRITTEN BY AI ### + """ + # Test valid config + valid_config = SyntheticTextPrefixBucketConfig( + bucket_weight=50, prefix_count=2, prefix_tokens=3 + ) + assert valid_config.bucket_weight == 50 + assert valid_config.prefix_count == 2 + assert valid_config.prefix_tokens == 3 + + # Test invalid bucket_weight + with pytest.raises(ValueError): + SyntheticTextPrefixBucketConfig( + bucket_weight=0, prefix_count=1, prefix_tokens=2 + ) + + # Test invalid prefix_count + with pytest.raises(ValueError): + SyntheticTextPrefixBucketConfig( + bucket_weight=100, prefix_count=0, prefix_tokens=2 + ) + + # Test invalid prefix_tokens + with pytest.raises(ValueError): + SyntheticTextPrefixBucketConfig( + bucket_weight=100, prefix_count=1, prefix_tokens=-1 + ) + + +class TestSyntheticDatasetConfig: + """Test cases for SyntheticDatasetConfig class. + + ### WRITTEN BY AI ### + """ + + @pytest.mark.smoke + def test_config_creation_with_all_params(self): + """Test creating config with all parameters specified. + + ### WRITTEN BY AI ### + """ + prefix_bucket = SyntheticTextPrefixBucketConfig( + bucket_weight=100, prefix_count=1, prefix_tokens=5 + ) + + config = SyntheticTextDatasetConfig( + prefix_buckets=[prefix_bucket], + prompt_tokens=100, + prompt_tokens_stdev=10, + prompt_tokens_min=50, + prompt_tokens_max=150, + output_tokens=30, + output_tokens_stdev=5, + output_tokens_min=20, + output_tokens_max=40, + source="custom_text.txt", + ) + + assert config.prefix_buckets[0].prefix_tokens == 5 # type: ignore [index] + assert config.prompt_tokens == 100 + assert config.prompt_tokens_stdev == 10 + assert config.prompt_tokens_min == 50 + assert config.prompt_tokens_max == 150 + assert config.output_tokens == 30 + assert config.output_tokens_stdev == 5 + assert config.output_tokens_min == 20 + assert config.output_tokens_max == 40 + assert config.source == "custom_text.txt" + + @pytest.mark.regression + def test_parse_json_string(self): + """Test parsing JSON string configuration. + + ### WRITTEN BY AI ### + """ + json_str = json.dumps( + { + "prompt_tokens": 75, + "output_tokens": 25, + "source": "test.txt", + "prefix_buckets": [ + {"bucket_weight": 100, "prefix_count": 1, "prefix_tokens": 10} + ], + } + ) + + config = SyntheticTextDatasetConfig.model_validate_json(json_str) + + assert config.prompt_tokens == 75 + assert config.output_tokens == 25 + assert config.source == "test.txt" + assert config.prefix_buckets[0].prefix_tokens == 10 # type: ignore [index] + + @pytest.mark.sanity + def test_validation_positive_values(self): + """Test that negative or zero values are rejected. + + ### WRITTEN BY AI ### + """ + with pytest.raises(ValueError): + SyntheticTextDatasetConfig(prompt_tokens=0, output_tokens=20) + + with pytest.raises(ValueError): + SyntheticTextDatasetConfig(prompt_tokens=20, output_tokens=0) + + # Test negative prefix tokens via PrefixBucketConfig validation + with pytest.raises(ValueError): + SyntheticTextPrefixBucketConfig(prefix_tokens=-1) + + @pytest.mark.regression + def test_validation_optional_positive_values(self): + """Test that optional parameters reject negative values. + + ### WRITTEN BY AI ### + """ + with pytest.raises(ValueError): + SyntheticTextDatasetConfig( + prompt_tokens=20, output_tokens=10, prompt_tokens_stdev=-1 + ) + + with pytest.raises(ValueError): + SyntheticTextDatasetConfig( + prompt_tokens=20, output_tokens=10, prompt_tokens_min=-1 + ) + + with pytest.raises(ValueError): + SyntheticTextDatasetConfig( + prompt_tokens=20, output_tokens=10, output_tokens_max=0 + ) + + +class TestSyntheticTextGenerator: + """Test cases for SyntheticTextGenerator class. + + ### WRITTEN BY AI ### + """ + + @pytest.fixture + def mock_tokenizer(self): + """Fixture to provide a mocked tokenizer. + + ### WRITTEN BY AI ### + """ + tokenizer = Mock() + tokenizer.encode.side_effect = lambda text: list(range(len(text.split()))) + tokenizer.decode.side_effect = ( + lambda tokens, skip_special_tokens=False: " ".join( + f"token_{t}" for t in tokens[:5] + ) + ) + return tokenizer + + @pytest.fixture + def simple_config(self): + """Fixture for simple configuration. + + ### WRITTEN BY AI ### + """ + return SyntheticTextDatasetConfig( + prompt_tokens=15, + output_tokens=10, + source="The quick brown fox jumps over the lazy dog.", + ) + + @pytest.fixture + def config_with_prefix(self): + """Fixture for configuration with prefix tokens. + + ### WRITTEN BY AI ### + """ + prefix_bucket = SyntheticTextPrefixBucketConfig( + bucket_weight=100, prefix_count=1, prefix_tokens=3 + ) + + return SyntheticTextDatasetConfig( + prefix_buckets=[prefix_bucket], + prompt_tokens=15, + output_tokens=10, + source="The quick brown fox jumps over the lazy dog.", + ) + + @pytest.mark.smoke + def test_generator_initialization(self, simple_config, mock_tokenizer): + """Test generator initialization. + + ### WRITTEN BY AI ### + """ + generator = SyntheticTextGenerator( + simple_config, mock_tokenizer, random_seed=42 + ) + + assert generator.config == simple_config + assert generator.processor == mock_tokenizer + assert generator.random_seed == 42 + + @pytest.mark.smoke + def test_basic_iteration(self, simple_config, mock_tokenizer): + """Test basic iteration functionality. + + ### WRITTEN BY AI ### + """ + generator = SyntheticTextGenerator( + simple_config, mock_tokenizer, random_seed=42 + ) + + items = [] + for i, item in enumerate(generator): + items.append(item) + if i >= 4: # Only get 5 items + break + + # Verify we get the expected number of items + assert len(items) == 5 + + # Verify each item has the required keys + for item in items: + assert "prefix" in item + assert "prompt" in item + assert "prompt_tokens_count" in item + assert "output_tokens_count" in item + assert isinstance(item["prefix"], str) + assert isinstance(item["prompt"], str) + assert isinstance(item["prompt_tokens_count"], int) + assert isinstance(item["output_tokens_count"], int) + + @pytest.mark.sanity + def test_create_prompt_method(self, simple_config, mock_tokenizer): + """Test _create_prompt method. + + ### WRITTEN BY AI ### + """ + from faker import Faker + + generator = SyntheticTextGenerator( + simple_config, mock_tokenizer, random_seed=42 + ) + faker = Faker() + faker.seed_instance(42) + + # Test normal case + result = generator._create_prompt(5, faker, "unique_prefix ") + assert isinstance(result, str) + # The result should be the decoded tokens (token_0 token_1 etc.) due to our mock + assert "token_" in result + + # Test zero tokens + result = generator._create_prompt(0, faker) + assert result == "" + + @pytest.mark.regression + def test_prefix_tokens_integration(self, config_with_prefix, mock_tokenizer): + """Test integration with prefix tokens. + + ### WRITTEN BY AI ### + """ + generator = SyntheticTextGenerator( + config_with_prefix, mock_tokenizer, random_seed=42 + ) + + items = [] + for i, item in enumerate(generator): + items.append(item) + if i >= 2: # Only get 3 items + break + + # Verify prefix is present in items + for item in items: + assert isinstance(item["prefix"], str) + + @pytest.mark.regression + def test_random_seeding_consistency(self, simple_config, mock_tokenizer): + """Test that same seed produces consistent results. + + ### WRITTEN BY AI ### + """ + # Create two generators with same seed + generator1 = SyntheticTextGenerator( + simple_config, mock_tokenizer, random_seed=42 + ) + generator2 = SyntheticTextGenerator( + simple_config, mock_tokenizer, random_seed=42 + ) + + items1 = [] + items2 = [] + for i, (item1, item2) in enumerate(zip(generator1, generator2)): + items1.append(item1) + items2.append(item2) + if i >= 2: # Only get 3 items + break + + # With same seed and deterministic mocks, results should be identical + assert len(items1) == len(items2) + for item1, item2 in zip(items1, items2): + assert item1["prompt_tokens_count"] == item2["prompt_tokens_count"] + assert item1["output_tokens_count"] == item2["output_tokens_count"] + + +class TestSyntheticDatasetDeserializer: + """Test cases for SyntheticDatasetDeserializer class. + + ### WRITTEN BY AI ### + """ + + @pytest.fixture + def mock_tokenizer(self): + """Fixture to provide a mocked tokenizer. + + ### WRITTEN BY AI ### + """ + tokenizer = Mock() + tokenizer.encode.side_effect = lambda text: list(range(len(text.split()))) + tokenizer.decode.side_effect = ( + lambda tokens, skip_special_tokens=False: " ".join( + f"token_{t}" for t in tokens[:5] + ) + ) + return tokenizer + + @pytest.mark.sanity + def test_load_config_file_yaml(self): + """Test loading YAML config file. + + ### WRITTEN BY AI ### + """ + config_data = { + "prompt_tokens": 60, + "output_tokens": 15, + "source": "yaml_test.txt", + "prefix_buckets": [ + {"bucket_weight": 100, "prefix_count": 1, "prefix_tokens": 3} + ], + } + + with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: + yaml.dump(config_data, f) + yaml_path = f.name + + try: + deserializer = SyntheticTextDatasetDeserializer() + config = deserializer._load_config_file(yaml_path) + + assert config.prompt_tokens == 60 + assert config.output_tokens == 15 + assert config.source == "yaml_test.txt" + assert config.prefix_buckets[0].prefix_tokens == 3 # type: ignore [index] + finally: + Path(yaml_path).unlink() + + @pytest.mark.sanity + def test_load_config_file_config_extension(self): + """Test loading .config file. + + ### WRITTEN BY AI ### + """ + config_data = { + "prompt_tokens": 90, + "output_tokens": 35, + "prefix_buckets": [ + {"bucket_weight": 100, "prefix_count": 1, "prefix_tokens": 2} + ], + } + + with tempfile.NamedTemporaryFile(mode="w", suffix=".config", delete=False) as f: + yaml.dump(config_data, f) + config_path = f.name + + try: + deserializer = SyntheticTextDatasetDeserializer() + config = deserializer._load_config_file(config_path) + + assert config.prompt_tokens == 90 + assert config.output_tokens == 35 + assert config.prefix_buckets[0].prefix_tokens == 2 # type: ignore [index] + finally: + Path(config_path).unlink() + + @pytest.mark.smoke + def test_load_config_str_json(self): + """Test loading JSON string config. + + ### WRITTEN BY AI ### + """ + json_str = '{"prompt_tokens": 50, "output_tokens": 25}' + deserializer = SyntheticTextDatasetDeserializer() + config = deserializer._load_config_str(json_str) + + assert config.prompt_tokens == 50 + assert config.output_tokens == 25 + + @pytest.mark.smoke + def test_load_config_str_key_value(self): + """Test loading key-value string config. + + ### WRITTEN BY AI ### + """ + kv_str = "prompt_tokens=50,output_tokens=25" + deserializer = SyntheticTextDatasetDeserializer() + config = deserializer._load_config_str(kv_str) + + assert config.prompt_tokens == 50 + assert config.output_tokens == 25 + + @pytest.mark.sanity + def test_load_config_str_invalid_format(self): + """Test loading invalid format raises DataNotSupportedError. + + ### WRITTEN BY AI ### + """ + deserializer = SyntheticTextDatasetDeserializer() + with pytest.raises(DataNotSupportedError, match="Unsupported string data"): + deserializer._load_config_str("invalid_format_string") + + @pytest.mark.regression + def test_load_config_file_non_existent(self): + """Test loading non-existent file returns None. + + ### WRITTEN BY AI ### + """ + deserializer = SyntheticTextDatasetDeserializer() + config = deserializer._load_config_file("/non/existent/path.config") + assert config is None + + @pytest.mark.regression + def test_load_config_str_non_string(self): + """Test loading non-string returns None. + + ### WRITTEN BY AI ### + """ + deserializer = SyntheticTextDatasetDeserializer() + config = deserializer._load_config_str(123) + assert config is None + + @pytest.mark.smoke + def test_call_with_config_object(self, mock_tokenizer): + """Test calling deserializer with SyntheticTextDatasetConfig. + + ### WRITTEN BY AI ### + """ + config = SyntheticTextDatasetConfig(prompt_tokens=50, output_tokens=25) + deserializer = SyntheticTextDatasetDeserializer() + + result = deserializer( + data=config, + data_kwargs={}, + processor_factory=lambda: mock_tokenizer, + random_seed=42, + ) + + assert isinstance(result, IterableDataset) + + @pytest.mark.regression + def test_call_with_unsupported_data(self, mock_tokenizer): + """Test calling deserializer with unsupported data raises error. + + ### WRITTEN BY AI ### + """ + deserializer = SyntheticTextDatasetDeserializer() + + with pytest.raises(DataNotSupportedError, match="Unsupported data"): + deserializer( + data=123, + data_kwargs={}, + processor_factory=lambda: mock_tokenizer, + random_seed=42, + ) + + @pytest.mark.regression + def test_call_with_json_string(self, mock_tokenizer): + """Test calling deserializer with JSON string. + + ### WRITTEN BY AI ### + """ + json_str = '{"prompt_tokens": 50, "output_tokens": 25}' + deserializer = SyntheticTextDatasetDeserializer() + + result = deserializer( + data=json_str, + data_kwargs={}, + processor_factory=lambda: mock_tokenizer, + random_seed=42, + ) + + assert isinstance(result, IterableDataset) + + @pytest.mark.regression + def test_call_with_config_file(self, mock_tokenizer): + """Test calling deserializer with config file. + + ### WRITTEN BY AI ### + """ + config_data = {"prompt_tokens": 65, "output_tokens": 45} + + with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: + yaml.dump(config_data, f) + config_path = f.name + + try: + deserializer = SyntheticTextDatasetDeserializer() + result = deserializer( + data=config_path, + data_kwargs={}, + processor_factory=lambda: mock_tokenizer, + random_seed=42, + ) + assert isinstance(result, IterableDataset) + finally: + Path(config_path).unlink() diff --git a/tests/unit/dataset/test_synthetic.py b/tests/unit/dataset/test_synthetic.py deleted file mode 100644 index e3110fa3..00000000 --- a/tests/unit/dataset/test_synthetic.py +++ /dev/null @@ -1,873 +0,0 @@ -""" -Unit tests for guidellm.dataset.synthetic module. -""" - -import json -import tempfile -from pathlib import Path -from unittest.mock import Mock, patch - -import pytest -import yaml - -from guidellm.dataset.synthetic import ( - SyntheticDatasetConfig, - SyntheticDatasetCreator, - SyntheticTextItemsGenerator, -) - - -class TestSyntheticDatasetConfig: - """Test cases for SyntheticDatasetConfig class. - - ### WRITTEN BY AI ### - """ - - @pytest.mark.smoke - def test_config_creation_with_all_params(self): - """Test creating config with all parameters specified. - - ### WRITTEN BY AI ### - """ - config = SyntheticDatasetConfig( - prefix_tokens=5, - prompt_tokens=100, - prompt_tokens_stdev=10, - prompt_tokens_min=50, - prompt_tokens_max=150, - output_tokens=30, - output_tokens_stdev=5, - output_tokens_min=20, - output_tokens_max=40, - samples=500, - source="custom_text.txt", - ) - - assert config.prefix_tokens == 5 - assert config.prompt_tokens == 100 - assert config.prompt_tokens_stdev == 10 - assert config.prompt_tokens_min == 50 - assert config.prompt_tokens_max == 150 - assert config.output_tokens == 30 - assert config.output_tokens_stdev == 5 - assert config.output_tokens_min == 20 - assert config.output_tokens_max == 40 - assert config.samples == 500 - assert config.source == "custom_text.txt" - - @pytest.mark.regression - def test_parse_json_string(self): - """Test parsing JSON string configuration. - - ### WRITTEN BY AI ### - """ - json_str = json.dumps( - { - "prompt_tokens": 75, - "output_tokens": 25, - "samples": 200, - "source": "test.txt", - "prefix_tokens": 10, - } - ) - - config = SyntheticDatasetConfig.parse_str(json_str) - - assert config.prompt_tokens == 75 - assert config.output_tokens == 25 - assert config.samples == 200 - assert config.source == "test.txt" - assert config.prefix_tokens == 10 - - @pytest.mark.regression - def test_parse_key_value_pairs(self): - """Test parsing key-value pairs configuration. - - ### WRITTEN BY AI ### - """ - kv_str = "prompt_tokens=80,output_tokens=30,samples=300,source=data.txt,prefix_tokens=5" # noqa: E501 - - config = SyntheticDatasetConfig.parse_str(kv_str) - - assert config.prompt_tokens == 80 - assert config.output_tokens == 30 - assert config.samples == 300 - assert config.source == "data.txt" - assert config.prefix_tokens == 5 - - @pytest.mark.sanity - def test_parse_yaml_file(self): - """Test parsing YAML file configuration. - - ### WRITTEN BY AI ### - """ - config_data = { - "prompt_tokens": 60, - "output_tokens": 15, - "samples": 100, - "source": "yaml_test.txt", - "prefix_tokens": 3, - } - - with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: - yaml.dump(config_data, f) - yaml_path = f.name - - try: - config = SyntheticDatasetConfig.parse_str(yaml_path) - - assert config.prompt_tokens == 60 - assert config.output_tokens == 15 - assert config.samples == 100 - assert config.source == "yaml_test.txt" - assert config.prefix_tokens == 3 - finally: - Path(yaml_path).unlink() - - @pytest.mark.sanity - def test_parse_config_file(self): - """Test parsing .config file. - - ### WRITTEN BY AI ### - """ - config_data = { - "prompt_tokens": 90, - "output_tokens": 35, - "samples": 150, - "prefix_tokens": 2, - } - - with tempfile.NamedTemporaryFile(mode="w", suffix=".config", delete=False) as f: - yaml.dump(config_data, f) - config_path = f.name - - try: - config = SyntheticDatasetConfig.parse_str(config_path) - - assert config.prompt_tokens == 90 - assert config.output_tokens == 35 - assert config.samples == 150 - assert config.prefix_tokens == 2 - finally: - Path(config_path).unlink() - - @pytest.mark.regression - def test_parse_path_object(self): - """Test parsing with Path object. - - ### WRITTEN BY AI ### - """ - config_data = {"prompt_tokens": 45, "output_tokens": 25} - - with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: - yaml.dump(config_data, f) - yaml_path = Path(f.name) - - try: - config = SyntheticDatasetConfig.parse_str(yaml_path) - assert config.prompt_tokens == 45 - assert config.output_tokens == 25 - finally: - yaml_path.unlink() - - @pytest.mark.sanity - def test_parse_invalid_format(self): - """Test parsing invalid format raises ValueError. - - ### WRITTEN BY AI ### - """ - with pytest.raises(ValueError, match="Unsupported data format"): - SyntheticDatasetConfig.parse_str("invalid_format_string") - - @pytest.mark.sanity - def test_validation_positive_values(self): - """Test that negative or zero values are rejected. - - ### WRITTEN BY AI ### - """ - with pytest.raises(ValueError): - SyntheticDatasetConfig(prompt_tokens=0, output_tokens=20) - - with pytest.raises(ValueError): - SyntheticDatasetConfig(prompt_tokens=20, output_tokens=0) - - with pytest.raises(ValueError): - SyntheticDatasetConfig(prompt_tokens=20, output_tokens=10, samples=0) - - with pytest.raises(ValueError): - SyntheticDatasetConfig(prompt_tokens=20, output_tokens=10, prefix_tokens=-1) - - @pytest.mark.regression - def test_validation_optional_positive_values(self): - """Test that optional parameters reject negative values. - - ### WRITTEN BY AI ### - """ - with pytest.raises(ValueError): - SyntheticDatasetConfig( - prompt_tokens=20, output_tokens=10, prompt_tokens_stdev=-1 - ) - - with pytest.raises(ValueError): - SyntheticDatasetConfig( - prompt_tokens=20, output_tokens=10, prompt_tokens_min=-1 - ) - - with pytest.raises(ValueError): - SyntheticDatasetConfig( - prompt_tokens=20, output_tokens=10, output_tokens_max=0 - ) - - @pytest.mark.regression - def test_parse_json_method_directly(self): - """Test parse_json static method directly. - - ### WRITTEN BY AI ### - """ - json_data = {"prompt_tokens": 100, "output_tokens": 50} - json_str = json.dumps(json_data) - - config = SyntheticDatasetConfig.parse_json(json_str) - - assert config.prompt_tokens == 100 - assert config.output_tokens == 50 - - @pytest.mark.regression - def test_parse_key_value_pairs_method_directly(self): - """Test parse_key_value_pairs static method directly. - - ### WRITTEN BY AI ### - """ - kv_str = "prompt_tokens=75,output_tokens=35" - - config = SyntheticDatasetConfig.parse_key_value_pairs(kv_str) - - assert config.prompt_tokens == 75 - assert config.output_tokens == 35 - - @pytest.mark.regression - def test_parse_config_file_method_directly(self): - """Test parse_config_file static method directly. - - ### WRITTEN BY AI ### - """ - config_data = {"prompt_tokens": 65, "output_tokens": 45} - - with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: - yaml.dump(config_data, f) - config_path = f.name - - try: - config = SyntheticDatasetConfig.parse_config_file(config_path) - assert config.prompt_tokens == 65 - assert config.output_tokens == 45 - finally: - Path(config_path).unlink() - - -class TestSyntheticTextItemsGenerator: - """Test cases for SyntheticTextItemsGenerator class. - - ### WRITTEN BY AI ### - """ - - @pytest.fixture - def mock_tokenizer(self): - """Fixture to provide a mocked tokenizer. - - ### WRITTEN BY AI ### - """ - tokenizer = Mock() - tokenizer.get_vocab.return_value = {f"token_{i}": i for i in range(1000)} - tokenizer.encode.side_effect = lambda text: [1, 2, 3] * (len(text) // 10 + 1) - tokenizer.decode.side_effect = ( - lambda tokens, skip_special_tokens=False: " ".join( - f"token_{t}" for t in tokens[:5] - ) - ) - return tokenizer - - @pytest.fixture - def simple_config(self): - """Fixture for simple configuration. - - ### WRITTEN BY AI ### - """ - return SyntheticDatasetConfig( - prompt_tokens=15, - output_tokens=10, - samples=5, - source="The quick brown fox jumps over the lazy dog.", - ) - - @pytest.fixture - def config_with_prefix(self): - """Fixture for configuration with prefix tokens. - - ### WRITTEN BY AI ### - """ - return SyntheticDatasetConfig( - prefix_tokens=3, - prompt_tokens=15, - output_tokens=10, - samples=5, - source="The quick brown fox jumps over the lazy dog.", - ) - - @pytest.fixture - def complex_config(self): - """Fixture for complex configuration with variance. - - ### WRITTEN BY AI ### - """ - return SyntheticDatasetConfig( - prompt_tokens=20, - prompt_tokens_stdev=5, - prompt_tokens_min=10, - prompt_tokens_max=30, - output_tokens=15, - output_tokens_stdev=3, - output_tokens_min=10, - output_tokens_max=20, - samples=10, - source="The quick brown fox jumps over the lazy dog.", - ) - - @pytest.mark.smoke - @patch("guidellm.dataset.synthetic.EndlessTextCreator") - def test_generator_initialization( - self, mock_text_creator, simple_config, mock_tokenizer - ): - """Test generator initialization. - - ### WRITTEN BY AI ### - """ - generator = SyntheticTextItemsGenerator( - simple_config, mock_tokenizer, random_seed=42 - ) - - assert generator.config == simple_config - assert generator.processor == mock_tokenizer - assert generator.random_seed == 42 - mock_text_creator.assert_called_once_with(data=simple_config.source) - - @pytest.mark.smoke - @patch("guidellm.dataset.synthetic.EndlessTextCreator") - @patch("guidellm.dataset.synthetic.IntegerRangeSampler") - def test_basic_iteration( - self, mock_sampler, mock_text_creator, simple_config, mock_tokenizer - ): - """Test basic iteration functionality. - - ### WRITTEN BY AI ### - """ - # Setup mocks - mock_text_creator_instance = Mock() - mock_text_creator_instance.words = ["word1", "word2", "word3"] * 100 - mock_text_creator_instance.create_text.return_value = "sample text" - mock_text_creator.return_value = mock_text_creator_instance - - # Mock IntegerRangeSampler to return iterators - def mock_sampler_side_effect(*args, **kwargs): - mock_instance = Mock() - mock_instance.__iter__ = Mock(return_value=iter([15, 15, 15, 15, 15])) - return mock_instance - - mock_sampler.side_effect = mock_sampler_side_effect - - generator = SyntheticTextItemsGenerator( - simple_config, mock_tokenizer, random_seed=42 - ) - - items = list(generator) - - # Verify we get the expected number of items - assert len(items) == simple_config.samples - - # Verify each item has the required keys - for item in items: - assert "prompt" in item - assert "prompt_tokens_count" in item - assert "output_tokens_count" in item - assert isinstance(item["prompt"], str) - assert isinstance(item["prompt_tokens_count"], int) - assert isinstance(item["output_tokens_count"], int) - - @pytest.mark.sanity - @patch("guidellm.dataset.synthetic.EndlessTextCreator") - def test_create_prompt_method( - self, mock_text_creator, simple_config, mock_tokenizer - ): - """Test _create_prompt method. - - ### WRITTEN BY AI ### - """ - mock_text_creator_instance = Mock() - mock_text_creator_instance.words = ["word"] * 100 - mock_text_creator_instance.create_text.return_value = "test text" - mock_text_creator.return_value = mock_text_creator_instance - - mock_tokenizer.encode.return_value = [1, 2, 3] - - generator = SyntheticTextItemsGenerator( - simple_config, mock_tokenizer, random_seed=42 - ) - - # Test normal case - result = generator._create_prompt(5, 0, 42) - assert result == [42, 1, 2, 3] - - # Test zero tokens - result = generator._create_prompt(0, 0, 42) - assert result == [] - - # Test without unique prefix - result = generator._create_prompt(3, 0) - assert result == [1, 2, 3] - - @pytest.mark.regression - @patch("guidellm.dataset.synthetic.EndlessTextCreator") - def test_create_prompt_binary_search( - self, mock_text_creator, simple_config, mock_tokenizer - ): - """Test binary search logic in _create_prompt. - - ### WRITTEN BY AI ### - """ - mock_text_creator_instance = Mock() - mock_text_creator_instance.words = ["word"] * 1000 - mock_text_creator_instance.create_text.side_effect = lambda start, length: ( - "text " * max(1, length // 4) - ).strip() - mock_text_creator.return_value = mock_text_creator_instance - - # Mock tokenizer to return different lengths based on input - def mock_encode(text): - return [1] * len(text.split()) - - mock_tokenizer.encode.side_effect = mock_encode - - generator = SyntheticTextItemsGenerator( - simple_config, mock_tokenizer, random_seed=42 - ) - - # Test that binary search finds appropriate length - result = generator._create_prompt(5, 0, 42) - assert len(result) >= 4 # Should include prefix + some tokens - - @pytest.mark.sanity - @patch("guidellm.dataset.synthetic.EndlessTextCreator") - @patch("guidellm.dataset.synthetic.IntegerRangeSampler") - def test_prefix_tokens_integration( - self, mock_sampler, mock_text_creator, config_with_prefix, mock_tokenizer - ): - """Test integration with prefix tokens. - - ### WRITTEN BY AI ### - """ - # Setup mocks - mock_text_creator_instance = Mock() - mock_text_creator_instance.words = ["word"] * 100 - mock_text_creator_instance.create_text.return_value = "sample text" - mock_text_creator.return_value = mock_text_creator_instance - - mock_sampler_instance = Mock() - mock_sampler_instance.__iter__ = Mock(return_value=iter([15, 15, 15, 15, 15])) - mock_sampler.return_value = mock_sampler_instance - - generator = SyntheticTextItemsGenerator( - config_with_prefix, mock_tokenizer, random_seed=42 - ) - - items = list(generator) - - # Verify prompt_tokens_count includes prefix - for item in items: - assert item["prompt_tokens_count"] == config_with_prefix.prefix_tokens + 15 - - @pytest.mark.regression - @patch("guidellm.dataset.synthetic.EndlessTextCreator") - @patch("guidellm.dataset.synthetic.IntegerRangeSampler") - def test_random_seeding_consistency( - self, mock_sampler, mock_text_creator, simple_config, mock_tokenizer - ): - """Test that same seed produces consistent results. - - ### WRITTEN BY AI ### - """ - # Setup mocks - mock_text_creator_instance = Mock() - mock_text_creator_instance.words = ["word"] * 100 - mock_text_creator_instance.create_text.return_value = "sample text" - mock_text_creator.return_value = mock_text_creator_instance - - # Create consistent mock sampler behavior - call_count = 0 - - def mock_sampler_side_effect(*args, **kwargs): - nonlocal call_count - mock_instance = Mock() - # Return same sequence for both prompt and output tokens - if call_count % 2 == 0: # prompt tokens - mock_instance.__iter__ = Mock(return_value=iter([15, 16, 17, 18, 19])) - else: # output tokens - mock_instance.__iter__ = Mock(return_value=iter([10, 11, 12, 13, 14])) - call_count += 1 - return mock_instance - - mock_sampler.side_effect = mock_sampler_side_effect - - # Create two generators with same seed - generator1 = SyntheticTextItemsGenerator( - simple_config, mock_tokenizer, random_seed=42 - ) - generator2 = SyntheticTextItemsGenerator( - simple_config, mock_tokenizer, random_seed=42 - ) - - items1 = list(generator1) - items2 = list(generator2) - - # Results should be identical with same seed - assert len(items1) == len(items2) - for item1, item2 in zip(items1, items2): - assert item1["prompt"] == item2["prompt"] - assert item1["prompt_tokens_count"] == item2["prompt_tokens_count"] - assert item1["output_tokens_count"] == item2["output_tokens_count"] - - @pytest.mark.regression - @patch("guidellm.dataset.synthetic.EndlessTextCreator") - @patch("guidellm.dataset.synthetic.IntegerRangeSampler") - def test_variance_configuration( - self, mock_sampler, mock_text_creator, complex_config, mock_tokenizer - ): - """Test that variance configuration is properly used. - - ### WRITTEN BY AI ### - """ - # Setup mocks - mock_text_creator_instance = Mock() - mock_text_creator_instance.words = ["word"] * 100 - mock_text_creator_instance.create_text.return_value = "sample text" - mock_text_creator.return_value = mock_text_creator_instance - - # Fix tokenizer mock to handle the create_text return properly - mock_tokenizer.encode.side_effect = ( - lambda text: [1, 2, 3] if isinstance(text, str) else [1, 2, 3] - ) - - # Setup mock sampler to track calls - def mock_sampler_side_effect(*args, **kwargs): - mock_instance = Mock() - mock_instance.__iter__ = Mock(return_value=iter([20, 18, 22, 19, 21] * 2)) - return mock_instance - - mock_sampler.side_effect = mock_sampler_side_effect - - generator = SyntheticTextItemsGenerator( - complex_config, mock_tokenizer, random_seed=42 - ) - - # Initialize the generator to trigger sampler creation - generator_iter = iter(generator) - next(generator_iter) - - # Verify that IntegerRangeSampler is called with correct parameters - assert mock_sampler.call_count == 2 - - # Check prompt tokens sampler call - prompt_call = mock_sampler.call_args_list[0] - assert prompt_call[1]["average"] == complex_config.prompt_tokens - assert prompt_call[1]["variance"] == complex_config.prompt_tokens_stdev - assert prompt_call[1]["min_value"] == complex_config.prompt_tokens_min - assert prompt_call[1]["max_value"] == complex_config.prompt_tokens_max - assert prompt_call[1]["random_seed"] == 42 - - # Check output tokens sampler call - output_call = mock_sampler.call_args_list[1] - assert output_call[1]["average"] == complex_config.output_tokens - assert output_call[1]["variance"] == complex_config.output_tokens_stdev - assert output_call[1]["min_value"] == complex_config.output_tokens_min - assert output_call[1]["max_value"] == complex_config.output_tokens_max - assert output_call[1]["random_seed"] == 43 # 42 + 1 - - @pytest.mark.regression - @patch("guidellm.dataset.synthetic.EndlessTextCreator") - def test_unique_prefix_generation( - self, mock_text_creator, simple_config, mock_tokenizer - ): - """Test that unique prefixes are generated for each request. - - ### WRITTEN BY AI ### - """ - mock_text_creator_instance = Mock() - mock_text_creator_instance.words = ["word"] * 100 - mock_text_creator_instance.create_text.return_value = "sample text" - mock_text_creator.return_value = mock_text_creator_instance - - # Mock the cycle to return predictable values - with patch("guidellm.dataset.synthetic.cycle") as mock_cycle: - mock_cycle.return_value = iter([100, 101, 102, 103, 104]) - - generator = SyntheticTextItemsGenerator( - simple_config, mock_tokenizer, random_seed=42 - ) - - # Access the iterator to trigger the cycle creation - generator_iter = iter(generator) - next(generator_iter) - - # Verify cycle was called with vocab values - mock_cycle.assert_called_once() - - -class TestSyntheticDatasetCreator: - """Test cases for SyntheticDatasetCreator class. - - ### WRITTEN BY AI ### - """ - - @pytest.mark.sanity - def test_is_supported_path_config_file(self): - """Test is_supported with config file paths. - - ### WRITTEN BY AI ### - """ - with tempfile.NamedTemporaryFile(suffix=".config", delete=False) as f: - config_path = Path(f.name) - - try: - assert SyntheticDatasetCreator.is_supported(config_path, None) - finally: - config_path.unlink() - - @pytest.mark.sanity - def test_is_supported_path_yaml_file(self): - """Test is_supported with YAML file paths. - - ### WRITTEN BY AI ### - """ - with tempfile.NamedTemporaryFile(suffix=".yaml", delete=False) as f: - yaml_path = Path(f.name) - - try: - assert SyntheticDatasetCreator.is_supported(yaml_path, None) - finally: - yaml_path.unlink() - - @pytest.mark.smoke - def test_is_supported_json_string(self): - """Test is_supported with JSON string. - - ### WRITTEN BY AI ### - """ - json_str = '{"prompt_tokens": 50, "output_tokens": 25}' - assert SyntheticDatasetCreator.is_supported(json_str, None) - - @pytest.mark.smoke - def test_is_supported_key_value_string(self): - """Test is_supported with key-value string. - - ### WRITTEN BY AI ### - """ - kv_str = "prompt_tokens=50,output_tokens=25" - assert SyntheticDatasetCreator.is_supported(kv_str, None) - - @pytest.mark.sanity - def test_is_supported_config_filename_string(self): - """Test is_supported with config filename string. - - ### WRITTEN BY AI ### - """ - assert SyntheticDatasetCreator.is_supported("config.yaml", None) - assert SyntheticDatasetCreator.is_supported("settings.config", None) - - @pytest.mark.sanity - def test_is_not_supported_regular_string(self): - """Test is_supported returns False for regular strings. - - ### WRITTEN BY AI ### - """ - assert not SyntheticDatasetCreator.is_supported("regular string", None) - assert not SyntheticDatasetCreator.is_supported("single=pair", None) - - @pytest.mark.regression - def test_is_not_supported_non_existent_path(self): - """Test is_supported returns False for non-existent paths. - - ### WRITTEN BY AI ### - """ - non_existent_path = Path("/non/existent/path.config") - assert not SyntheticDatasetCreator.is_supported(non_existent_path, None) - - @pytest.mark.regression - def test_is_not_supported_other_types(self): - """Test is_supported returns False for other data types. - - ### WRITTEN BY AI ### - """ - assert not SyntheticDatasetCreator.is_supported(123, None) - assert not SyntheticDatasetCreator.is_supported(["list"], None) - assert not SyntheticDatasetCreator.is_supported({"dict": "value"}, None) - - @pytest.mark.smoke - @patch("guidellm.dataset.synthetic.check_load_processor") - @patch("guidellm.dataset.synthetic.SyntheticTextItemsGenerator") - @patch("guidellm.dataset.synthetic.Dataset") - def test_handle_create_basic( - self, mock_dataset, mock_generator, mock_check_processor - ): - """Test handle_create basic functionality. - - ### WRITTEN BY AI ### - """ - # Setup mocks - mock_processor = Mock() - mock_check_processor.return_value = mock_processor - - mock_generator_instance = Mock() - mock_generator_instance.__iter__ = Mock( - return_value=iter( - [ - { - "prompt": "test", - "prompt_tokens_count": 10, - "output_tokens_count": 5, - } - ] - ) - ) - mock_generator.return_value = mock_generator_instance - - mock_dataset_instance = Mock() - mock_dataset.from_list.return_value = mock_dataset_instance - - # Test - data = '{"prompt_tokens": 50, "output_tokens": 25}' - result = SyntheticDatasetCreator.handle_create( - data=data, - data_args=None, - processor="gpt2", - processor_args=None, - random_seed=42, - ) - - # Verify - mock_check_processor.assert_called_once_with( - "gpt2", - None, - error_msg="Processor/tokenizer required for synthetic dataset generation.", - ) - mock_generator.assert_called_once() - mock_dataset.from_list.assert_called_once() - assert result == mock_dataset_instance - - @pytest.mark.sanity - @patch("guidellm.dataset.synthetic.check_load_processor") - def test_handle_create_processor_required(self, mock_check_processor): - """Test handle_create requires processor. - - ### WRITTEN BY AI ### - """ - mock_check_processor.side_effect = ValueError("Processor required") - - data = '{"prompt_tokens": 50, "output_tokens": 25}' - - with pytest.raises(ValueError, match="Processor required"): - SyntheticDatasetCreator.handle_create( - data=data, - data_args=None, - processor=None, - processor_args=None, - random_seed=42, - ) - - @pytest.mark.regression - @patch("guidellm.dataset.synthetic.check_load_processor") - @patch("guidellm.dataset.synthetic.SyntheticTextItemsGenerator") - @patch("guidellm.dataset.synthetic.Dataset") - def test_handle_create_with_data_args( - self, mock_dataset, mock_generator, mock_check_processor - ): - """Test handle_create with data_args. - - ### WRITTEN BY AI ### - """ - # Setup mocks - mock_processor = Mock() - mock_check_processor.return_value = mock_processor - - mock_generator_instance = Mock() - mock_generator_instance.__iter__ = Mock(return_value=iter([])) - mock_generator.return_value = mock_generator_instance - - mock_dataset_instance = Mock() - mock_dataset.from_list.return_value = mock_dataset_instance - - # Test with data_args - data = '{"prompt_tokens": 50, "output_tokens": 25}' - data_args = {"features": "custom_features"} - - SyntheticDatasetCreator.handle_create( - data=data, - data_args=data_args, - processor="gpt2", - processor_args=None, - random_seed=42, - ) - - # Verify data_args are passed to Dataset.from_list - mock_dataset.from_list.assert_called_once_with([], **data_args) - - @pytest.mark.sanity - def test_extract_args_column_mappings_empty(self): - """Test extract_args_column_mappings with empty data_args. - - ### WRITTEN BY AI ### - """ - result = SyntheticDatasetCreator.extract_args_column_mappings(None) - - expected = { - "prompt_column": "prompt", - "prompt_tokens_count_column": "prompt_tokens_count", - "output_tokens_count_column": "output_tokens_count", - } - assert result == expected - - @pytest.mark.regression - def test_extract_args_column_mappings_with_parent_mappings(self): - """Test extract_args_column_mappings rejects column mappings. - - ### WRITTEN BY AI ### - """ - with ( - patch.object( - SyntheticDatasetCreator.__bases__[0], - "extract_args_column_mappings", - return_value={"prompt_column": "custom_prompt"}, - ), - pytest.raises(ValueError, match="Column mappings are not supported"), - ): - SyntheticDatasetCreator.extract_args_column_mappings({"some": "args"}) - - @pytest.mark.regression - def test_extract_args_column_mappings_no_parent_mappings(self): - """Test extract_args_column_mappings with no parent mappings. - - ### WRITTEN BY AI ### - """ - with patch.object( - SyntheticDatasetCreator.__bases__[0], - "extract_args_column_mappings", - return_value={}, - ): - result = SyntheticDatasetCreator.extract_args_column_mappings( - {"some": "args"} - ) - - expected = { - "prompt_column": "prompt", - "prompt_tokens_count_column": "prompt_tokens_count", - "output_tokens_count_column": "output_tokens_count", - } - assert result == expected From 000b39ef060063661995fc2cfcd571668dd16dbe Mon Sep 17 00:00:00 2001 From: Samuel Monson Date: Fri, 3 Oct 2025 17:46:04 -0400 Subject: [PATCH 08/57] Fix for container rc tag (#389) ## Summary Fix to parsing rc ref in CI --- - [x] "I certify that all code in this PR is my own, except as noted below." ## Use of AI - [ ] Includes AI-assisted code completion - [ ] Includes code generated by an AI application - [ ] Includes AI-generated tests (NOTE: AI written tests should have a docstring that includes `## WRITTEN BY AI ##`) Signed-off-by: Samuel Monson --- .github/workflows/release.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7f71b633..6cc090cb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -297,7 +297,10 @@ jobs: with: fetch-depth: 0 - name: Get version from branch - run: echo "PACKAGE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV + run: | + GITHUB_REF="${{ github.ref }}" + [[ -z "$GITHUB_REF" ]] && exit 1 # Fail if ref is unset + echo "PACKAGE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - name: Buildah build id: build-image uses: redhat-actions/buildah-build@v2 From bbca65a81c60d0b7336d219d9546793103c4369b Mon Sep 17 00:00:00 2001 From: Mark Kurtz Date: Tue, 7 Oct 2025 10:21:42 -0400 Subject: [PATCH 09/57] Simplifications for new data pathways and reenablement of completions and chat completions pathways --- pyproject.toml | 16 +- src/guidellm/__main__.py | 124 ++++--- src/guidellm/benchmark/aggregator.py | 12 +- src/guidellm/benchmark/entrypoints.py | 118 ++++--- src/guidellm/data/__init__.py | 32 +- src/guidellm/data/collators.py | 16 + src/guidellm/data/datasets.py | 88 ----- .../data/deserializers/deserializer.py | 60 ++-- src/guidellm/data/deserializers/file.py | 16 +- .../data/deserializers/huggingface.py | 2 +- src/guidellm/data/deserializers/memory.py | 12 +- src/guidellm/data/deserializers/synthetic.py | 19 +- src/guidellm/data/formatters/__init__.py | 47 --- src/guidellm/data/formatters/environment.py | 63 ---- src/guidellm/data/formatters/globals.py | 9 - src/guidellm/data/formatters/objects.py | 92 ------ src/guidellm/data/formatters/templates.py | 182 ----------- src/guidellm/data/loaders.py | 136 ++++---- src/guidellm/data/objects.py | 121 ++----- src/guidellm/data/preprocessors/__init__.py | 20 +- src/guidellm/data/preprocessors/formatters.py | 303 ++++++++++++++++++ src/guidellm/data/preprocessors/mappers.py | 257 +++++++++------ src/guidellm/data/preprocessors/objects.py | 20 -- .../data/preprocessors/preprocessor.py | 29 ++ src/guidellm/data/processor.py | 30 ++ src/guidellm/data/utils/__init__.py | 34 ++ .../data/{utils.py => utils/dataset.py} | 78 +---- .../filters.py => utils/functions.py} | 156 +++++---- src/guidellm/scheduler/worker.py | 12 +- src/guidellm/scheduler/worker_group.py | 1 + src/guidellm/settings.py | 2 +- src/guidellm/utils/messaging.py | 4 + 32 files changed, 1000 insertions(+), 1111 deletions(-) create mode 100644 src/guidellm/data/collators.py delete mode 100644 src/guidellm/data/datasets.py delete mode 100644 src/guidellm/data/formatters/__init__.py delete mode 100644 src/guidellm/data/formatters/environment.py delete mode 100644 src/guidellm/data/formatters/globals.py delete mode 100644 src/guidellm/data/formatters/objects.py delete mode 100644 src/guidellm/data/formatters/templates.py create mode 100644 src/guidellm/data/preprocessors/formatters.py delete mode 100644 src/guidellm/data/preprocessors/objects.py create mode 100644 src/guidellm/data/preprocessors/preprocessor.py create mode 100644 src/guidellm/data/processor.py create mode 100644 src/guidellm/data/utils/__init__.py rename src/guidellm/data/{utils.py => utils/dataset.py} (54%) rename src/guidellm/data/{formatters/filters.py => utils/functions.py} (73%) diff --git a/pyproject.toml b/pyproject.toml index 7237e66d..3461530d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,25 +66,21 @@ dependencies = [ "protobuf", "pydantic>=2.11.7", "pydantic-settings>=2.0.0", + "pydub", "pyyaml>=6.0.0", "rich", "sanic", "transformers", "uvloop>=0.18", "librosa>=0.11.0", - "torch>=2.8.0", + "torch", ] [project.optional-dependencies] -perf = [ - "orjson", - "msgpack", - "msgspec", - "uvloop", -] +perf = ["orjson", "msgpack", "msgspec", "uvloop"] recommended = [ - "tiktoken>=0.11.0", # For OpenAI tokenizer - "blobfile>=3.1.0", # For OpenAI tokenizer + "tiktoken>=0.11.0", # For OpenAI tokenizer + "blobfile>=3.1.0", # For OpenAI tokenizer ] dev = [ # build @@ -127,7 +123,7 @@ dev = [ ] [dependency-groups] -dev = [ "guidellm[dev]" ] +dev = ["guidellm[dev]"] [project.urls] homepage = "https://github.com/vllm-project/guidellm" diff --git a/src/guidellm/__main__.py b/src/guidellm/__main__.py index 82632bc8..43939fa7 100644 --- a/src/guidellm/__main__.py +++ b/src/guidellm/__main__.py @@ -56,11 +56,7 @@ from guidellm.benchmark.scenario import ( GenerativeTextScenario, ) -from guidellm.data import ( - GenerativeDatasetArgs, - GenerativeRequestFormatter, - GenerativeRequestType, -) +from guidellm.data import GenerativeRequestType from guidellm.mock_server import MockServer, MockServerConfig from guidellm.preprocess.dataset import ShortPromptStrategy, process_dataset from guidellm.scheduler import StrategyType @@ -177,12 +173,6 @@ def benchmark(): "For rate-type=synchronous,throughput, this must not be set." ), ) -@click.option( - "--random-seed", - default=GenerativeTextScenario.get_default("random_seed"), - type=int, - help="The random seed to use for benchmarking to ensure reproducibility.", -) # Backend configuration @click.option( "--backend", @@ -216,6 +206,24 @@ def benchmark(): ), ) # Data configuration +@click.option( + "--request-type", + default="chat_completions", + type=click.Choice(list(get_literal_vals(GenerativeRequestType))), + help=( + "The type of request to create for each data sample and send to the backend. " + f"Supported types: {list(get_literal_vals(GenerativeRequestType))}." + ), +) +@click.option( + "--request-formatter-kwargs", + default=None, + callback=cli_tools.parse_json, + help=( + "A JSON string containing any arguments to pass to the request formatter " + "as a dict with **kwargs." + ), +) @click.option( "--processor", default=None, @@ -238,16 +246,7 @@ def benchmark(): @click.option( "--data-args", default=None, - callback=( - lambda _ctx, _param, value: [ - GenerativeDatasetArgs.model_validate_json(val) - if val - else GenerativeDatasetArgs() - for val in value - ] - if value - else None - ), + callback=cli_tools.parse_json, help=( "A JSON string containing any arguments to pass to the dataset creation " "as a dict with **kwargs." @@ -259,43 +258,30 @@ def benchmark(): type=int, help=( "The number of samples to use from the dataset. If -1 (default), will use all " - "samples in the dataset." + "samples in the dataset and dynamically generate samples. " + "If >1, will precompile that number of items from the dataset configs." ), ) @click.option( - "--data-sampler", - default=None, - type=click.Choice(["shuffle"]), - help="The data sampler type to use.", -) -@click.option( - "--data-request-type", - default="text_completions", - type=str, - help=( - "The type of request to create for each data sample. " - f"For example, {list(get_literal_vals(GenerativeRequestType))}." - ), -) -@click.option( - "--data-request-template", + "--data-column-mappings", default=None, + callback=cli_tools.parse_json, help=( - "A Jinja2 template string or path to a Jinja2 template file to use for " - "creating requests from the data samples. If not provided, will use a " - "default template based on the request type." + "A JSON string of column mappings to apply to the dataset to map into request " + "column types." ), ) @click.option( - "--data-request-extras", + "--data-sampler", default=None, - callback=cli_tools.parse_json, - help=("A JSON string of extra data to include with each data request."), + type=click.Choice(["shuffle"]), + help="The data sampler type to use.", ) @click.option( - "--data-request-nonstreaming", - is_flag=True, - help="Set this flag to disable streaming for the data requests.", + "--data-num-workers", + default=1, + type=int, + help="The number of worker processes to use for data loading.", ) @click.option( "--dataloader_kwargs", @@ -306,6 +292,12 @@ def benchmark(): "as a dict with **kwargs." ), ) +@click.option( + "--random-seed", + default=GenerativeTextScenario.get_default("random_seed"), + type=int, + help="The random seed to use for benchmarking to ensure reproducibility.", +) # Output configuration @click.option( "--output-path", @@ -435,22 +427,22 @@ def run( data, profile, rate, - random_seed, # Backend Configuration backend, backend_kwargs, model, # Data configuration + request_type, + request_formatter_kwargs, processor, processor_args, data_args, data_samples, + data_column_mappings, data_sampler, - data_request_type, - data_request_template, - data_request_extras, - data_request_nonstreaming, + data_num_workers, dataloader_kwargs, + random_seed, # Output configuration output_path, output_formats, @@ -478,6 +470,12 @@ def run( Supports multiple backends, data sources, output formats, and constraint types for flexible benchmark configuration. """ + data_request_formatter = ( + request_type + if not request_formatter_kwargs + else {"request_type": request_type, **request_formatter_kwargs} + ) + if HAS_UVLOOP: asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) asyncio.run( @@ -487,7 +485,6 @@ def run( # Benchmark configuration profile=profile, rate=rate, - random_seed=random_seed, # Backend configuration backend=backend, backend_kwargs=backend_kwargs, @@ -497,21 +494,12 @@ def run( processor_args=processor_args, data_args=data_args, data_samples=data_samples, - data_column_mapper=None, # use default - data_request_formatter=GenerativeRequestFormatter( - request_type=data_request_type, - request_template=data_request_template, - request_extras=data_request_extras, - request_defaults=( - {} # disable defaults if non-streaming - if data_request_nonstreaming - else None - ), - ), - data_preprocessors=None, # no preprocessors through CLI for now - dataloader_sampler=data_sampler, - dataloader_collate_fn=None, # use default + data_column_mapper=data_column_mappings, + data_request_formatter=data_request_formatter, + data_sampler=data_sampler, + data_num_workers=data_num_workers, dataloader_kwargs=dataloader_kwargs, + random_seed=random_seed, # Output configuration output_path=output_path, output_formats=[ @@ -534,7 +522,7 @@ def run( add_aggregators={"extras": InjectExtrasAggregator(extras=output_extras)}, warmup=warmup, cooldown=cooldown, - request_samples=request_samples, + sample_requests=request_samples, # Constraints configuration max_seconds=max_seconds, max_requests=max_requests, diff --git a/src/guidellm/benchmark/aggregator.py b/src/guidellm/benchmark/aggregator.py index 3040ad36..562fc36c 100644 --- a/src/guidellm/benchmark/aggregator.py +++ b/src/guidellm/benchmark/aggregator.py @@ -678,20 +678,20 @@ class GenerativeRequestsAggregator( @classmethod def validated_kwargs( cls, - request_samples: int | None = 20, + sample_requests: int | None = 20, warmup: int | float | None = None, cooldown: int | float | None = None, **_kwargs, ) -> dict[str, Any]: return { - "request_samples": request_samples, + "sample_requests": sample_requests, "warmup": warmup, "cooldown": cooldown, } type_: Literal["generative_requests"] = Field(default="generative_requests") - request_samples: int | None = Field(default=20, description="") + sample_requests: int | None = Field(default=20, description="") warmup: int | float | None = Field( default=None, description="Number of warmup requests to ignore at benchmark start", @@ -828,9 +828,9 @@ def compile( list[GenerativeRequestStats], list[GenerativeRequestStats], ]( - successful=self._sample_request_stats(successful, self.request_samples), - incomplete=self._sample_request_stats(incomplete, self.request_samples), - errored=self._sample_request_stats(errored, self.request_samples), + successful=self._sample_request_stats(successful, self.sample_requests), + incomplete=self._sample_request_stats(incomplete, self.sample_requests), + errored=self._sample_request_stats(errored, self.sample_requests), ), "metrics": GenerativeMetrics( requests_per_second=self._calculate_requests_per_second( diff --git a/src/guidellm/benchmark/entrypoints.py b/src/guidellm/benchmark/entrypoints.py index 23bc985a..e400907a 100644 --- a/src/guidellm/benchmark/entrypoints.py +++ b/src/guidellm/benchmark/entrypoints.py @@ -1,11 +1,10 @@ from __future__ import annotations from pathlib import Path -from typing import Any, Literal +from typing import Any, Callable, Literal from torch.utils.data import Sampler from transformers import ( # type: ignore[import] - AutoTokenizer, PreTrainedTokenizerBase, ) @@ -35,13 +34,13 @@ BenchmarkerProgressGroup, ) from guidellm.data import ( + DataLoader, DatasetPreprocessor, - GenerativeColumnMapper, - GenerativeDataLoader, GenerativeRequestCollator, - GenerativeRequestFormatter, + PreprocessorRegistry, + ProcessorFactory, ) -from guidellm.data.objects import GenerativeDatasetArgs +from guidellm.data.preprocessors import GenerativeColumnMapper from guidellm.scheduler import ( ConstraintInitializer, NonDistributedEnvironment, @@ -59,14 +58,13 @@ # @validate_call(config={"arbitrary_types_allowed": True}) -async def benchmark_generative_text( # noqa: C901, PLR0915 +async def benchmark_generative_text( # noqa: C901, PLR0915, PLR0912 # Required target: str, data: list[Any], # Benchmark configuration profile: StrategyType | ProfileType | Profile = "sweep", rate: float | list[float] | None = None, - random_seed: int = 42, # Backend configuration backend: BackendType | Backend = "openai_http", backend_kwargs: dict[str, Any] | None = None, @@ -74,14 +72,19 @@ async def benchmark_generative_text( # noqa: C901, PLR0915 # Data configuration processor: str | Path | PreTrainedTokenizerBase | None = None, processor_args: dict[str, Any] | None = None, - data_args: list[GenerativeDatasetArgs] | None = None, + data_args: list[dict[str, Any]] | None = None, data_samples: int = -1, - data_column_mapper: GenerativeColumnMapper | None = None, - data_preprocessors: list[DatasetPreprocessor] | None = None, - data_request_formatter: GenerativeRequestFormatter | None = None, - dataloader_sampler: Sampler[int] | Literal["shuffle"] | None = None, - dataloader_collate_fn: GenerativeRequestCollator | None = None, + data_column_mapper: ( + DatasetPreprocessor | dict[str, str] | Literal["generative_column_mapper"] + ) = "generative_column_mapper", + data_request_formatter: ( + DatasetPreprocessor | dict[str, str] | str + ) = "chat_completions", + data_collator: Callable | Literal["generative"] | None = "generative", + data_sampler: Sampler[int] | Literal["shuffle"] | None = None, + data_num_workers: int | None = 1, dataloader_kwargs: dict[str, Any] | None = None, + random_seed: int = 42, # Output configuration output_path: str | Path | None = _CURRENT_WORKING_DIR, output_formats: ( @@ -99,7 +102,7 @@ async def benchmark_generative_text( # noqa: C901, PLR0915 ) = None, warmup: float | None = None, cooldown: float | None = None, - request_samples: int | None = 20, + sample_requests: int | None = 10, # Constraints configuration max_seconds: int | float | None = None, max_requests: int | None = None, @@ -123,8 +126,17 @@ async def benchmark_generative_text( # noqa: C901, PLR0915 console_step.update(f"{backend.__class__.__name__} backend initialized") await backend.process_startup() await backend.validate() + if model is None: + console_step.update( + title="Resolving default model from backend.default_model", + status_level="info", + ) + model = await backend.default_model() + await backend.process_shutdown() console_step.finish( - title=f"{backend.__class__.__name__} backend initialized", + title=( + f"{backend.__class__.__name__} backend validated with model {model}" + ), details=backend.info, status_level="success", ) @@ -136,54 +148,56 @@ async def benchmark_generative_text( # noqa: C901, PLR0915 details=f"Using processor '{processor}'", status_level="success", ) - elif model is not None: - console_step.finish( - title="Processor resolved", - details=f"Using model '{model}' as processor", - status_level="success", - ) - processor = model else: - console_step.update( - title="Resolving processor from backend.default_model", - status_level="info", - ) - processor = await backend.default_model() + processor = model console_step.finish( title="Processor resolved", - details=( - f"Using model '{processor}' from backend " - f"{backend.__class__.__name__} as processor" - ), + details=f"Using model '{processor}' as processor", status_level="success", ) - await backend.process_shutdown() with console.print_update_step( title=f"Initializing request loader from {data}" ) as console_step: + if not isinstance(data_column_mapper, DatasetPreprocessor): + column_mappings = ( + data_column_mapper if isinstance(data_column_mapper, dict) else None + ) + data_column_mapper = GenerativeColumnMapper( + column_mappings=column_mappings, + ) + if not isinstance(data_request_formatter, DatasetPreprocessor): + request_type = ( + data_request_formatter + if isinstance(data_request_formatter, str) + else data_request_formatter.pop("request_type", "chat_completions") + ) + data_request_formatter = PreprocessorRegistry.get_registered_object( + request_type + )( + model=model, + **( + data_request_formatter + if isinstance(data_request_formatter, dict) + else {} + ), + ) - def processor_factory() -> PreTrainedTokenizerBase: - nonlocal processor - if isinstance(processor, PreTrainedTokenizerBase): - return processor - else: - processor = AutoTokenizer.from_pretrained( - processor, - **(processor_args or {}), - ) - return processor - - request_loader = GenerativeDataLoader( + request_loader = DataLoader( data=data, data_args=data_args, data_samples=data_samples, - processor_factory=processor_factory, - column_mapper=data_column_mapper or GenerativeColumnMapper(), - preprocessors=data_preprocessors or [], - request_formatter=data_request_formatter or GenerativeRequestFormatter(), - sampler=dataloader_sampler, - collate_fn=dataloader_collate_fn, + processor_factory=ProcessorFactory( + processor=processor, processor_args=processor_args + ), + preprocessors=[data_column_mapper, data_request_formatter], + collator=( + data_collator + if callable(data_collator) + else GenerativeRequestCollator() + ), + sampler=data_sampler, + num_workers=data_num_workers, random_seed=random_seed, **(dataloader_kwargs or {}), ) @@ -234,7 +248,7 @@ def processor_factory() -> PreTrainedTokenizerBase: "scheduler_stats": SchedulerStatsAggregator(), "requests_progress": GenerativeStatsProgressAggregator(), "requests": GenerativeRequestsAggregator( - request_samples=request_samples, + request_samples=sample_requests, warmup=warmup, cooldown=cooldown, ), diff --git a/src/guidellm/data/__init__.py b/src/guidellm/data/__init__.py index 282c5b59..d25c719a 100644 --- a/src/guidellm/data/__init__.py +++ b/src/guidellm/data/__init__.py @@ -1,31 +1,29 @@ -from .datasets import GenerativeRequestsDataset +from .collators import GenerativeRequestCollator from .deserializers import ( DataNotSupportedError, DatasetDeserializer, DatasetDeserializerFactory, ) -from .formatters import ( - GenerativeRequestFormatter, - JinjaEnvironmentMixin, - JinjaFiltersRegistry, - JinjaGlobalsRegistry, - JinjaTemplatesRegistry, -) -from .loaders import GenerativeDataLoader, GenerativeRequestCollator +from .loaders import DataLoader from .objects import ( GenerationRequest, GenerationRequestArguments, GenerationRequestTimings, - GenerativeDatasetArgs, GenerativeDatasetColumnType, GenerativeRequestType, ) from .preprocessors import ( + DataDependentPreprocessor, DatasetPreprocessor, - GenerativeColumnMapper, + PreprocessorRegistry, ) +from .processor import ProcessorFactory __all__ = [ + "ColumnMapper", + "ColumnMapperRegistry", + "DataDependentPreprocessor", + "DataLoader", "DataNotSupportedError", "DatasetDeserializer", "DatasetDeserializerFactory", @@ -33,16 +31,12 @@ "GenerationRequest", "GenerationRequestArguments", "GenerationRequestTimings", - "GenerativeColumnMapper", - "GenerativeDataLoader", "GenerativeDatasetArgs", "GenerativeDatasetColumnType", "GenerativeRequestCollator", - "GenerativeRequestFormatter", "GenerativeRequestType", - "GenerativeRequestsDataset", - "JinjaEnvironmentMixin", - "JinjaFiltersRegistry", - "JinjaGlobalsRegistry", - "JinjaTemplatesRegistry", + "PreprocessorRegistry", + "ProcessorFactory", + "RequestFormatter", + "RequestFormatterRegistry", ] diff --git a/src/guidellm/data/collators.py b/src/guidellm/data/collators.py new file mode 100644 index 00000000..4d12f0c0 --- /dev/null +++ b/src/guidellm/data/collators.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from guidellm.data.objects import GenerationRequest + +__all__ = ["GenerativeRequestCollator"] + + +class GenerativeRequestCollator: + def __call__(self, batch: list) -> GenerationRequest: + if len(batch) != 1: + raise NotImplementedError( + f"Batch size greater than 1 is not currently supported. " + f"Got batch size: {len(batch)}" + ) + + return batch[0] diff --git a/src/guidellm/data/datasets.py b/src/guidellm/data/datasets.py deleted file mode 100644 index 8c24683c..00000000 --- a/src/guidellm/data/datasets.py +++ /dev/null @@ -1,88 +0,0 @@ -from __future__ import annotations - -from collections.abc import Callable -from typing import Any - -from datasets import Dataset, IterableDataset -from transformers import PreTrainedTokenizerBase - -from guidellm.data.deserializers import DatasetDeserializerFactory -from guidellm.data.formatters import GenerativeRequestFormatter -from guidellm.data.objects import GenerativeDatasetArgs -from guidellm.data.preprocessors import ( - DatasetPreprocessor, - GenerativeColumnMapper, -) -from guidellm.data.utils import datasets_item_iterator, resolve_dataset_split - -__all__ = ["GenerativeRequestsDataset"] - - -class GenerativeRequestsDataset: - @classmethod - def build( - cls, - data: list[Any], - data_args: list[GenerativeDatasetArgs] | None, - data_samples: int, - processor_factory: Callable[[], PreTrainedTokenizerBase], - column_mapper: GenerativeColumnMapper, - preprocessors: list[DatasetPreprocessor], - request_formatter: GenerativeRequestFormatter, - random_seed: int = 42, - ) -> Dataset | IterableDataset: - if not data or not isinstance(data, list): - raise ValueError(f"Data must be a non-empty list, got {data}.") - - if data_args is None: - data_args = [GenerativeDatasetArgs() for _ in data] - - if len(data) != len(data_args): - raise ValueError( - f"Length of data ({len(data)}) must match length of data_args " - f"({len(data_args)})." - ) - - datasets = [] - for datum, args in zip(data, data_args): - datasets.append( - resolve_dataset_split( - dataset=DatasetDeserializerFactory.deserialize( - data=datum, - data_kwargs=args.to_kwargs(), - processor_factory=processor_factory, - random_seed=random_seed, - type_=args.type_, - ), - split=args.split, - ) - ) - - column_mapper.init_data(datasets=datasets, data_args=data_args) - request_formatter.init_data(datasets=datasets, data_args=data_args) - for preprocessor in preprocessors: - preprocessor.init_data(datasets=datasets, data_args=data_args) - - if data_samples > 0: - dataset = Dataset.from_list( - list( - datasets_item_iterator( - datasets=datasets, - data_samples=data_samples, - ) - ) - ) - else: - dataset = IterableDataset.from_generator( - datasets_item_iterator, - gen_kwargs={ - "datasets": datasets, - "data_samples": data_samples, - }, - ) - - dataset = dataset.map(column_mapper) - for preprocessor in preprocessors: - dataset = dataset.map(preprocessor) - - return dataset.map(request_formatter) diff --git a/src/guidellm/data/deserializers/deserializer.py b/src/guidellm/data/deserializers/deserializer.py index ed9050a1..c7e2f1da 100644 --- a/src/guidellm/data/deserializers/deserializer.py +++ b/src/guidellm/data/deserializers/deserializer.py @@ -4,9 +4,10 @@ from collections.abc import Callable from typing import Any, Protocol, Union, runtime_checkable -from datasets import Dataset, DatasetDict, IterableDataset, IterableDatasetDict +from datasets import Dataset, IterableDataset from transformers import PreTrainedTokenizerBase +from guidellm.data.utils import resolve_dataset_split from guidellm.utils import RegistryMixin __all__ = [ @@ -25,9 +26,9 @@ class DatasetDeserializer(Protocol): def __call__( self, data: Any, - data_kwargs: dict[str, Any], processor_factory: Callable[[], PreTrainedTokenizerBase], random_seed: int, + **data_kwargs: dict[str, Any], ) -> dict[str, list]: ... @@ -38,44 +39,43 @@ class DatasetDeserializerFactory( def deserialize( cls, data: Any, - data_kwargs: dict[str, Any], processor_factory: Callable[[], PreTrainedTokenizerBase], random_seed: int = 42, type_: str | None = None, - ) -> Dataset | IterableDataset | DatasetDict | IterableDatasetDict: - if type_ is not None: - deserializer = cls.get_registered_object(type_) + resolve_split: bool = True, + **data_kwargs: dict[str, Any], + ) -> Dataset | IterableDataset: + dataset = None - if deserializer is None: - raise DataNotSupportedError( - f"Deserializer type '{type_}' is not registered. " - f"Available types: {cls.registry}" + if type_ is None: + for deserializer in cls.registered_objects(): + deserializer_fn: DatasetDeserializer = ( + deserializer() if isinstance(deserializer, type) else deserializer ) - elif isinstance(deserializer, type): - deserializer_fn = deserializer() - else: - deserializer_fn = deserializer - return deserializer_fn( + with contextlib.suppress(DataNotSupportedError): + dataset = deserializer_fn( + data=data, + processor_factory=processor_factory, + random_seed=random_seed, + **data_kwargs, + ) + elif deserializer := cls.get_registered_object(type_) is not None: + deserializer_fn: DatasetDeserializer = ( + deserializer() if isinstance(deserializer, type) else deserializer + ) + + dataset = deserializer_fn( data=data, - data_kwargs=data_kwargs, processor_factory=processor_factory, random_seed=random_seed, + **data_kwargs, ) - for deserializer in cls.registered_objects(): - deserializer_fn: DatasetDeserializer = ( - deserializer() if isinstance(deserializer, type) else deserializer + if dataset is None: + raise DataNotSupportedError( + f"No suitable deserializer found for data {data} " + f"with kwargs {data_kwargs} and type_ {type_}." ) - with contextlib.suppress(DataNotSupportedError): - return deserializer_fn( - data=data, - data_kwargs=data_kwargs, - processor_factory=processor_factory, - random_seed=random_seed, - ) - - raise DataNotSupportedError( - f"No suitable deserializer found for data {data} with kwargs {data_kwargs}." - ) + return resolve_dataset_split(dataset) if resolve_split else dataset diff --git a/src/guidellm/data/deserializers/file.py b/src/guidellm/data/deserializers/file.py index 53688cf0..54b18edb 100644 --- a/src/guidellm/data/deserializers/file.py +++ b/src/guidellm/data/deserializers/file.py @@ -30,9 +30,9 @@ class TextFileDatasetDeserializer(DatasetDeserializer): def __call__( self, data: Any, - data_kwargs: dict[str, Any], processor_factory: Callable[[], PreTrainedTokenizerBase], random_seed: int, + **data_kwargs: dict[str, Any], ) -> dict[str, list]: _ = (processor_factory, random_seed) # Ignore unused args format errors @@ -58,9 +58,9 @@ class CSVFileDatasetDeserializer(DatasetDeserializer): def __call__( self, data: Any, - data_kwargs: dict[str, Any], processor_factory: Callable[[], PreTrainedTokenizerBase], random_seed: int, + **data_kwargs: dict[str, Any], ) -> dict[str, list]: _ = (processor_factory, random_seed) if ( @@ -82,9 +82,9 @@ class JSONFileDatasetDeserializer(DatasetDeserializer): def __call__( self, data: Any, - data_kwargs: dict[str, Any], processor_factory: Callable[[], PreTrainedTokenizerBase], random_seed: int, + **data_kwargs: dict[str, Any], ) -> dict[str, list]: _ = (processor_factory, random_seed) if ( @@ -106,9 +106,9 @@ class ParquetFileDatasetDeserializer(DatasetDeserializer): def __call__( self, data: Any, - data_kwargs: dict[str, Any], processor_factory: Callable[[], PreTrainedTokenizerBase], random_seed: int, + **data_kwargs: dict[str, Any], ) -> dict[str, list]: _ = (processor_factory, random_seed) if ( @@ -130,9 +130,9 @@ class ArrowFileDatasetDeserializer(DatasetDeserializer): def __call__( self, data: Any, - data_kwargs: dict[str, Any], processor_factory: Callable[[], PreTrainedTokenizerBase], random_seed: int, + **data_kwargs: dict[str, Any], ) -> dict[str, list]: _ = (processor_factory, random_seed) if ( @@ -154,9 +154,9 @@ class HDF5FileDatasetDeserializer(DatasetDeserializer): def __call__( self, data: Any, - data_kwargs: dict[str, Any], processor_factory: Callable[[], PreTrainedTokenizerBase], random_seed: int, + **data_kwargs: dict[str, Any], ) -> dict[str, list]: _ = (processor_factory, random_seed) if ( @@ -178,9 +178,9 @@ class DBFileDatasetDeserializer(DatasetDeserializer): def __call__( self, data: Any, - data_kwargs: dict[str, Any], processor_factory: Callable[[], PreTrainedTokenizerBase], random_seed: int, + **data_kwargs: dict[str, Any], ) -> dict[str, list]: _ = (processor_factory, random_seed) if ( @@ -202,9 +202,9 @@ class TarFileDatasetDeserializer(DatasetDeserializer): def __call__( self, data: Any, - data_kwargs: dict[str, Any], processor_factory: Callable[[], PreTrainedTokenizerBase], random_seed: int, + **data_kwargs: dict[str, Any], ) -> dict[str, list]: _ = (processor_factory, random_seed) if ( diff --git a/src/guidellm/data/deserializers/huggingface.py b/src/guidellm/data/deserializers/huggingface.py index 275f7180..3e0cf090 100644 --- a/src/guidellm/data/deserializers/huggingface.py +++ b/src/guidellm/data/deserializers/huggingface.py @@ -27,9 +27,9 @@ class HuggingFaceDatasetDeserializer(DatasetDeserializer): def __call__( self, data: Any, - data_kwargs: dict[str, Any], processor_factory: Callable[[], PreTrainedTokenizerBase], random_seed: int, + **data_kwargs: dict[str, Any], ) -> dict[str, list]: _ = (processor_factory, random_seed) diff --git a/src/guidellm/data/deserializers/memory.py b/src/guidellm/data/deserializers/memory.py index b04ea6bc..ddca64a9 100644 --- a/src/guidellm/data/deserializers/memory.py +++ b/src/guidellm/data/deserializers/memory.py @@ -29,9 +29,9 @@ class InMemoryDictDatasetDeserializer(DatasetDeserializer): def __call__( self, data: Any, - data_kwargs: dict[str, Any], processor_factory: Callable[[], PreTrainedTokenizerBase], random_seed: int, + **data_kwargs: dict[str, Any], ) -> dict[str, list]: _ = (processor_factory, random_seed) # Ignore unused args format errors @@ -63,9 +63,9 @@ class InMemoryDictListDatasetDeserializer(DatasetDeserializer): def __call__( self, data: Any, - data_kwargs: dict[str, Any], processor_factory: Callable[[], PreTrainedTokenizerBase], random_seed: int, + **data_kwargs: dict[str, Any], ) -> dict[str, list]: _ = (processor_factory, random_seed) # Ignore unused args format errors @@ -104,9 +104,9 @@ class InMemoryItemListDatasetDeserializer(DatasetDeserializer): def __call__( self, data: Any, - data_kwargs: dict[str, Any], processor_factory: Callable[[], PreTrainedTokenizerBase], random_seed: int, + **data_kwargs: dict[str, Any], ) -> dict[str, list]: _ = (processor_factory, random_seed) # Ignore unused args format errors @@ -131,9 +131,9 @@ class InMemoryJsonStrDatasetDeserializer(DatasetDeserializer): def __call__( self, data: Any, - data_kwargs: dict[str, Any], processor_factory: Callable[[], PreTrainedTokenizerBase], random_seed: int, + **data_kwargs: dict[str, Any], ) -> dict[str, list]: if ( isinstance(data, str) @@ -167,9 +167,9 @@ class InMemoryCsvDatasetDeserializer(DatasetDeserializer): def __call__( self, data: Any, - data_kwargs: dict[str, Any], processor_factory: Callable[[], PreTrainedTokenizerBase], random_seed: int, + **data_kwargs: dict[str, Any], ) -> dict[str, list]: if ( isinstance(data, str) @@ -182,7 +182,7 @@ def __call__( rows = list(reader) return InMemoryDictListDatasetDeserializer()( - rows, data_kwargs, processor_factory, random_seed + rows, processor_factory, random_seed, **data_kwargs ) raise DataNotSupportedError( diff --git a/src/guidellm/data/deserializers/synthetic.py b/src/guidellm/data/deserializers/synthetic.py index a071eeea..c2078f1a 100644 --- a/src/guidellm/data/deserializers/synthetic.py +++ b/src/guidellm/data/deserializers/synthetic.py @@ -4,7 +4,7 @@ from collections.abc import Iterator from pathlib import Path from random import Random -from typing import Any, Callable, Self +from typing import Any, Callable import yaml from datasets import Features, IterableDataset, Value @@ -98,7 +98,7 @@ class SyntheticTextDatasetConfig(StandardBaseModel): ) @model_validator(mode="after") - def check_prefix_options(self) -> Self: + def check_prefix_options(self) -> SyntheticTextDatasetConfig: prefix_count = self.__pydantic_extra__.get("prefix_count", None) # type: ignore[attr-defined] prefix_tokens = self.__pydantic_extra__.get("prefix_count", None) # type: ignore[attr-defined] if prefix_count is not None or prefix_tokens is not None: @@ -226,17 +226,17 @@ class SyntheticTextDatasetDeserializer(DatasetDeserializer): def __call__( self, data: Any, - data_kwargs: dict[str, Any], processor_factory: Callable[[], PreTrainedTokenizerBase], random_seed: int, + **data_kwargs: dict[str, Any], ) -> IterableDataset: # Config file pathways, deserialize and call self again if (config := self._load_config_file(data)) is not None: - return self(config, data_kwargs, processor_factory, random_seed) + return self(config, processor_factory, random_seed, **data_kwargs) # Config str pathways, deserialize and call self again if (config := self._load_config_str(data)) is not None: - return self(config, data_kwargs, processor_factory, random_seed) + return self(config, processor_factory, random_seed, **data_kwargs) if not isinstance(data, SyntheticTextDatasetConfig): raise DataNotSupportedError( @@ -246,9 +246,12 @@ def __call__( ) return IterableDataset.from_generator( - lambda: SyntheticTextGenerator( - config=data, processor=processor_factory(), random_seed=random_seed - ), + SyntheticTextGenerator, + gen_kwargs={ + "config": data, + "processor": processor_factory(), + "random_seed": random_seed, + }, features=Features( { "prefix": Value("string"), diff --git a/src/guidellm/data/formatters/__init__.py b/src/guidellm/data/formatters/__init__.py deleted file mode 100644 index 0a5ccbc9..00000000 --- a/src/guidellm/data/formatters/__init__.py +++ /dev/null @@ -1,47 +0,0 @@ -from .environment import JinjaEnvironmentMixin -from .filters import ( - JinjaFiltersRegistry, - download_audio, - download_image, - download_video, - encode_audio, - encode_image, - encode_image_base64, - encode_video, - encode_video_base64, - get_file_format, - is_url, - resize_image, -) -from .globals import JinjaGlobalsRegistry -from .objects import GenerativeRequestFormatter -from .templates import ( - DEFAULT_AUDIO_TRANSCRIPTIONS_TEMPLATE, - DEFAULT_AUDIO_TRANSLATIONS_TEMPLATE, - DEFAULT_CHAT_COMPLETIONS_TEMPLATE, - DEFAULT_TEXT_COMPLETIONS_TEMPLATE, - JinjaTemplatesRegistry, -) - -__all__ = [ - "DEFAULT_AUDIO_TRANSCRIPTIONS_TEMPLATE", - "DEFAULT_AUDIO_TRANSLATIONS_TEMPLATE", - "DEFAULT_CHAT_COMPLETIONS_TEMPLATE", - "DEFAULT_TEXT_COMPLETIONS_TEMPLATE", - "GenerativeRequestFormatter", - "JinjaEnvironmentMixin", - "JinjaFiltersRegistry", - "JinjaGlobalsRegistry", - "JinjaTemplatesRegistry", - "download_audio", - "download_image", - "download_video", - "encode_audio", - "encode_image", - "encode_image_base64", - "encode_video", - "encode_video_base64", - "get_file_format", - "is_url", - "resize_image", -] diff --git a/src/guidellm/data/formatters/environment.py b/src/guidellm/data/formatters/environment.py deleted file mode 100644 index bd37e26b..00000000 --- a/src/guidellm/data/formatters/environment.py +++ /dev/null @@ -1,63 +0,0 @@ -from __future__ import annotations - -from typing import Any, ClassVar - -from jinja2 import Template -from jinja2.nativetypes import NativeEnvironment, NativeTemplate - -from guidellm.data.formatters.filters import JinjaFiltersRegistry -from guidellm.data.formatters.globals import JinjaGlobalsRegistry -from guidellm.data.formatters.templates import JinjaTemplatesRegistry - -__all__ = ["JinjaEnvironmentMixin"] - - -class JinjaEnvironmentMixin: - jinja_environment: ClassVar[NativeEnvironment | None] = None - - @classmethod - def create_environment(cls, **env_kwargs: Any) -> NativeEnvironment: - if "autoescape" not in env_kwargs: - env_kwargs["autoescape"] = False - - extensions = env_kwargs.pop("extensions", []) - extensions = set(extensions) | {"jinja2.ext.do"} - - env = NativeEnvironment(extensions=list(extensions), **env_kwargs) # noqa: S701 - - # Attach registered filters - filters_registry = JinjaFiltersRegistry.registry # type: ignore[misc] - if filters_registry: - for name, func in filters_registry.items(): - env.filters[name] = func - - # Attach registered globals - globals_registry = JinjaGlobalsRegistry.registry # type: ignore[misc] - if globals_registry: - for name, value in globals_registry.items(): - env.globals[name] = value - - cls.jinja_environment = env - return env - - @classmethod - def get_environment(cls) -> NativeEnvironment: - if cls.jinja_environment is None: - raise ValueError( - "Jinja environment is not initialized. Call create_environment first." - ) - return cls.jinja_environment - - @classmethod - def template_from_source(cls, source: str | Template) -> NativeTemplate: - if isinstance(source, Template): - return source - env = cls.get_environment() - return env.from_string(source) - - @classmethod - def template_from_registry(cls, name: str) -> NativeTemplate: - template = JinjaTemplatesRegistry.get_registered_object(name) - if template is None: - raise ValueError(f"Template '{name}' not found in registry.") - return cls.template_from_source(template) diff --git a/src/guidellm/data/formatters/globals.py b/src/guidellm/data/formatters/globals.py deleted file mode 100644 index 6c066191..00000000 --- a/src/guidellm/data/formatters/globals.py +++ /dev/null @@ -1,9 +0,0 @@ -from typing import Any - -from guidellm.utils import RegistryMixin - -__all__ = ["JinjaGlobalsRegistry"] - - -class JinjaGlobalsRegistry(RegistryMixin[Any]): - pass diff --git a/src/guidellm/data/formatters/objects.py b/src/guidellm/data/formatters/objects.py deleted file mode 100644 index 3e032089..00000000 --- a/src/guidellm/data/formatters/objects.py +++ /dev/null @@ -1,92 +0,0 @@ -from __future__ import annotations - -from typing import Any, Literal - -from datasets import Dataset, IterableDataset -from jinja2 import Template - -from guidellm.data.formatters import JinjaEnvironmentMixin -from guidellm.data.objects import ( - GenerationRequest, - GenerationRequestArguments, - GenerativeDatasetArgs, - GenerativeRequestType, -) -from guidellm.data.preprocessors.objects import DatasetPreprocessor - -__all__ = ["GenerativeRequestFormatter"] - - -class GenerativeRequestFormatter(DatasetPreprocessor, JinjaEnvironmentMixin): - def __init__( - self, - request_type: GenerativeRequestType | str = "text_completions", - request_template: str | Template | None = None, - request_extras: dict[str, Any] | GenerationRequestArguments | None = None, - request_defaults: dict[str, Any] | GenerationRequestArguments | None = None, - environment_extras: dict[str, Any] | None = None, - ): - self.datasets: list[Dataset | IterableDataset] | None = None - self.data_args: list[GenerativeDatasetArgs] | None = None - - self.request_type = request_type - self.request_template = request_template - self.request_extras = request_extras or {} - self.request_defaults = request_defaults or { - "stream": True, - "json_body": { - "stream": True, - "stream_options": { - "include_usage": True, - }, - }, - } - self.environment_extras = environment_extras or {} - self.jinja_template: Template | None = None - - def init_data( - self, - datasets: list[Dataset | IterableDataset], - data_args: list[GenerativeDatasetArgs], - ): - self.datasets = datasets - self.data_args = data_args - - self.create_environment(**self.environment_extras) - self.jinja_template = ( - self.template_from_source(self.request_template) - if self.request_template - else self.template_from_registry(self.request_type) - ) - - def __call__( - self, item: dict[str, Any] - ) -> dict[Literal["request"], GenerationRequest]: - if self.jinja_template is None: - raise ValueError("GenerativeRequestCreator not initialized with data.") - - stats = {} - if "prompt_tokens_count" in item: - count = item["prompt_tokens_count"][0] - stats["prompt_tokens"] = count - item["prompt_tokens_count"] = count - if "output_tokens_count" in item: - count = item["output_tokens_count"][0] - stats["output_tokens"] = count - item["output_tokens_count"] = count - - return { - "request": { - "request_type": self.request_type, - "arguments": GenerationRequestArguments.model_combine_dict( - self.request_defaults, - self.request_extras, - self.jinja_template.render( - **item, - request_defaults=self.request_defaults, - request_extras=self.request_extras, - ), - ), - "stats": stats, - } - } diff --git a/src/guidellm/data/formatters/templates.py b/src/guidellm/data/formatters/templates.py deleted file mode 100644 index 52db73b1..00000000 --- a/src/guidellm/data/formatters/templates.py +++ /dev/null @@ -1,182 +0,0 @@ -import textwrap -from typing import Union - -from jinja2 import Template - -from guidellm.utils import RegistryMixin - -__all__ = [ - "DEFAULT_AUDIO_TRANSCRIPTIONS_TEMPLATE", - "DEFAULT_AUDIO_TRANSLATIONS_TEMPLATE", - "DEFAULT_CHAT_COMPLETIONS_TEMPLATE", - "DEFAULT_TEXT_COMPLETIONS_TEMPLATE", - "JinjaTemplatesRegistry", -] - - -class JinjaTemplatesRegistry(RegistryMixin[Union[Template, str]]): - pass - - -DEFAULT_TEXT_COMPLETIONS_TEMPLATE = JinjaTemplatesRegistry.register("text_completions")( - textwrap.dedent(""" - {% set obj = { - "json_body": { - "prompt": prefix_column[0]|default("") + text_column[0] - } - } %} - - {% if output_tokens_count is defined and output_tokens_count is not none %} - {% do obj["json_body"].update({ - "max_tokens": output_tokens_count, - "max_completion_tokens": output_tokens_count, - "stop": None, - "ignore_eos": True - }) %} - {% elif max_tokens is defined and max_tokens is not none %} - {% do obj["json_body"].update({"max_tokens": max_tokens}) %} - {% elif max_completion_tokens is defined and max_completion_tokens is not none %} - {% do obj["json_body"].update({"max_completion_tokens": max_completion_tokens}) %} - {% endif %} - - {{ obj }} - """).strip() # noqa: E501 -) - -DEFAULT_CHAT_COMPLETIONS_TEMPLATE = JinjaTemplatesRegistry.register("chat_completions")( - textwrap.dedent(""" - {% set obj = { - "json_body": { - "messages": [ - { - "role": "system", - "content": prefix_column[0]|default("") - }, - { - "role": "user", - "content": [] - } - ] - } - } %} - - {%- for item in text_column or [] %} - {% do obj["json_body"].messages[1].content.append({"type": "text", "text": item}) %} - {%- endfor %} - - {%- for item in image_column or [] %} - {% do obj["json_body"].messages[1].content.append({ - "type": "image_url", - "image_url": encode_image( - item, - max_size=max_size|default(None), - max_width=max_width|default(None), - max_height=max_height|default(None), - encode_type=image_encode_type|default(encode_type|default(None)) - ) - }) %} - {%- endfor %} - - {%- for item in video_column or [] %} - {% do obj["json_body"].messages[1].content.append({ - "type": "video_url", - "video_url": encode_video( - item, - encode_type=video_encode_type|default(encode_type|default(None)) - ) - }) %} - {%- endfor %} - - {%- for item in audio_column or [] %} - {%- set audio_type, audio_val = encode_audio( - item, - sample_rate=sample_rate|default(None), - max_duration=max_duration|default(None), - encode_type=audio_encode_type|default(encode_type|default(None)) - ) -%} - {% do content_list.append({"type": audio_type, audio_type: audio_val}) %} - {%- endfor %} - - {% if output_tokens_count is defined and output_tokens_count is not none %} - {% do obj["json_body"].update({ - "max_completion_tokens": output_tokens_count, - "stop": None, - "ignore_eos": True - }) %} - {% elif max_tokens is defined and max_tokens is not none %} - {% do obj["json_body"].update({"max_completion_tokens": max_tokens}) %} - {% elif max_completion_tokens is defined and max_completion_tokens is not none %} - {% do obj["json_body"].update({"max_completion_tokens": max_completion_tokens}) %} - {% endif %} - - {{ obj }} - """).strip() # noqa: E501 -) - -DEFAULT_AUDIO_TRANSCRIPTIONS_TEMPLATE = JinjaTemplatesRegistry.register( - "audio_transcriptions" -)( - textwrap.dedent(""" - { - {%- if output_tokens_count_column is defined and output_tokens_count_column is not none -%} - "max_tokens": {{ output_tokens_count_column }}, - "max_completion_tokens": {{ output_tokens_count_column }}, - "stop": None, - "ignore_eos": True, - {%- else -%} - {%- if max_tokens is defined and max_tokens is not none -%} - "max_tokens": {{ max_tokens }}, - {%- endif -%} - {%- if max_completion_tokens is defined and max_completion_tokens is not none -%} - "max_completion_tokens": {{ max_completion_tokens }}, - {%- endif -%} - {%- endif -%} - "files": { - "file": {{ encode_audio_file( - audio_column[0], - encode_type=audio_encode_type|default(encode_type|default(None)) - ) }} - } - {%- if text_column and text_column|length > 0 -%} - , - "json": { - "prompt": {{ text_column[0] }} - } - {%- endif -%} - } - """).strip() # noqa: E501 -) - -DEFAULT_AUDIO_TRANSLATIONS_TEMPLATE = JinjaTemplatesRegistry.register( - "audio_translations" -)( - textwrap.dedent(""" - { - {%- if output_tokens_count_column is defined and output_tokens_count_column is not none -%} - "max_tokens": {{ output_tokens_count_column }}, - "max_completion_tokens": {{ output_tokens_count_column }}, - "stop": None, - "ignore_eos": True, - {%- else -%} - {%- if max_tokens is defined and max_tokens is not none -%} - "max_tokens": {{ max_tokens }}, - {%- endif -%} - {%- if max_completion_tokens is defined and max_completion_tokens is not none -%} - "max_completion_tokens": {{ max_completion_tokens }}, - {%- endif -%} - {%- endif -%} - "files": { - "file": {{ encode_audio_file( - audio_column[0], - encode_type=audio_encode_type|default(encode_type|default(None)) - ) }} - } - {%- if text_column and text_column|length > 0 -%} - , - "json": { - "prompt": {{ text_column[0] }} - } - {%- endif -%} - } - """).strip() # noqa: E501 -) diff --git a/src/guidellm/data/loaders.py b/src/guidellm/data/loaders.py index ebecdb6f..303e5a8d 100644 --- a/src/guidellm/data/loaders.py +++ b/src/guidellm/data/loaders.py @@ -1,93 +1,111 @@ from __future__ import annotations -from collections.abc import Callable +import contextlib +import math +from collections.abc import Callable, Iterator from typing import Any, Literal from datasets import Dataset, IterableDataset -from torch.utils.data import DataLoader, Sampler +from torch.utils.data import Sampler +from torch.utils.data.dataloader import DataLoader as PyTorchDataLoader from transformers import PreTrainedTokenizerBase -from guidellm.data.datasets import GenerativeRequestsDataset -from guidellm.data.formatters import GenerativeRequestFormatter -from guidellm.data.objects import GenerationRequest, GenerativeDatasetArgs -from guidellm.data.preprocessors import ( - DatasetPreprocessor, - GenerativeColumnMapper, -) +from guidellm.data.deserializers import DatasetDeserializerFactory +from guidellm.data.objects import GenerationRequest +from guidellm.data.preprocessors import DataDependentPreprocessor, DatasetPreprocessor -__all__ = ["GenerativeDataLoader", "GenerativeRequestCollator"] +__all__ = ["DataLoader", "datasets_item_iterator"] -class GenerativeRequestCollator: - def __call__( - self, batch: list[dict[Literal["request"], dict[str, Any]]] - ) -> GenerationRequest: - if len(batch) != 1: - raise NotImplementedError( - f"Batch size greater than 1 is not currently supported. " - f"Got batch size: {len(batch)}" - ) +def datasets_item_iterator( + datasets: list[Dataset | IterableDataset], + data_samples: int, + preprocessors: tuple[DatasetPreprocessor | DataDependentPreprocessor], +) -> Iterator[Any]: + gen_count = 0 + dataset_iters = [iter(dataset) for dataset in datasets] + + with contextlib.suppress(StopIteration): + while gen_count < data_samples or data_samples == math.inf: + row = {"items": [next(dataset_iter) for dataset_iter in dataset_iters]} + for preprocessor in preprocessors: + row = preprocessor(row) + yield row + gen_count += 1 - return GenerationRequest.model_validate(batch[0]["request"]) + if data_samples != math.inf and gen_count < data_samples: + raise ValueError( + f"Requested {data_samples} samples, but only {gen_count} " + "available from the provided datasets." + ) -class GenerativeDataLoader(DataLoader[GenerationRequest]): +class DataLoader(PyTorchDataLoader[GenerationRequest]): def __init__( self, data: list[Any], - data_args: list[GenerativeDatasetArgs] | None, + data_args: list[dict[str, Any]] | None, data_samples: int, processor_factory: Callable[[], PreTrainedTokenizerBase], - column_mapper: GenerativeColumnMapper, - preprocessors: list[DatasetPreprocessor], - request_formatter: GenerativeRequestFormatter, + preprocessors: list[DatasetPreprocessor | DataDependentPreprocessor], + collator: Callable, sampler: Sampler[int] | Literal["shuffle"] | None = None, - collate_fn: GenerativeRequestCollator | None = None, - num_workers: int | None = None, + num_workers: int | None = 1, random_seed: int = 42, **kwargs: Any, ): - dataset = GenerativeRequestsDataset.build( - data=data, - data_args=data_args, - data_samples=data_samples, - processor_factory=processor_factory, - column_mapper=column_mapper, - request_formatter=request_formatter, - preprocessors=preprocessors, - random_seed=random_seed, - ) + if not data or not isinstance(data, list): + raise ValueError(f"Data must be a non-empty list, got {data}.") - if collate_fn is None: - collate_fn = GenerativeRequestCollator() + if data_args is None: + data_args = [{} for _ in data] - # Handle sampler/shuffle logic based on dataset type - if sampler == "shuffle": - shuffle = True - sampler = None - elif isinstance(sampler, str) and sampler != "shuffle": + if len(data) != len(data_args): raise ValueError( - f"Invalid string sampler: {sampler}. " - f"Only 'shuffle' is supported as a string value." + f"Length of data ({len(data)}) must match length of data_args " + f"({len(data_args)})." ) - else: - shuffle = False - if isinstance(dataset, IterableDataset) and sampler is not None: - raise ValueError( - "Samplers are not supported with IterableDataset. " - "Use shuffle=True or apply shuffling to the dataset directly." + datasets = [] + for datum, data_kwargs in zip(data, data_args): + type_ = data_kwargs.pop("type_") if "type_" in data_kwargs else None + datasets.append( + DatasetDeserializerFactory.deserialize( + data=datum, + data_kwargs=data_args, + processor_factory=processor_factory, + random_seed=random_seed, + type_=type_, + **data_kwargs, + ) + ) + for preprocessor in preprocessors: + if isinstance(preprocessor, DataDependentPreprocessor): + preprocessor.setup_data( + datasets=datasets, + data_args=data_args, + ) + if data_samples != math.inf and data_samples > 0: + cached_samples = list( + datasets_item_iterator(datasets, data_samples, tuple(preprocessors)) + ) + dataset = IterableDataset.from_generator(lambda: cached_samples) + else: + dataset = IterableDataset.from_generator( + datasets_item_iterator, + gen_kwargs={ + "datasets": datasets, + "data_samples": math.inf, + "preprocessors": tuple(preprocessors), + }, ) - elif isinstance(dataset, Dataset) and shuffle: - dataset = dataset.shuffle(seed=random_seed) - shuffle = False super().__init__( dataset=dataset, batch_size=1, - shuffle=shuffle, - sampler=sampler, - collate_fn=collate_fn, - num_workers=num_workers or 0, + shuffle=sampler == "shuffle", + sampler=sampler if sampler != "shuffle" else None, + collate_fn=collator, + num_workers=num_workers, **kwargs, ) diff --git a/src/guidellm/data/objects.py b/src/guidellm/data/objects.py index b4a38719..2a4b3857 100644 --- a/src/guidellm/data/objects.py +++ b/src/guidellm/data/objects.py @@ -1,7 +1,7 @@ from __future__ import annotations import uuid -from typing import Any, Literal, get_args +from typing import Any, Literal from pydantic import Field @@ -15,7 +15,6 @@ "GenerationRequest", "GenerationRequestArguments", "GenerationRequestTimings", - "GenerativeDatasetArgs", "GenerativeDatasetColumnType", "GenerativeRequestType", ] @@ -47,66 +46,23 @@ def model_combine_dict( # noqa: C901, PLR0912 combined = {} for args in arguments: - if ( - url := args.get("url") if isinstance(args, dict) else args.url - ) is not None: - combined["url"] = url - - if ( - path := args.get("path") if isinstance(args, dict) else args.path - ) is not None: - combined["path"] = path - - if ( - method := args.get("method") if isinstance(args, dict) else args.method - ) is not None: - combined["method"] = method - - if ( - stream := args.get("stream") if isinstance(args, dict) else args.stream - ) is not None: - combined["stream"] = stream - - if ( - content_body := ( - args.get("content_body") - if isinstance(args, dict) - else args.content_body - ) - ) is not None: - combined["content_body"] = content_body - - if ( - json_body := ( - args.get("json_body") if isinstance(args, dict) else args.json_body - ) - ) is not None: - if "json_body" not in combined: - combined["json_body"] = {} - combined["json_body"].update(json_body) - - if ( - files := args.get("files") if isinstance(args, dict) else args.files - ) is not None: - if "files" not in combined: - combined["files"] = {} - combined["files"].update(files) - - if ( - params := args.get("params") if isinstance(args, dict) else args.params - ) is not None: - if "params" not in combined: - combined["params"] = {} - combined["params"].update(params) - - if ( - headers := ( - args.get("headers") if isinstance(args, dict) else args.headers - ) - ) is not None: - if "headers" not in combined: - combined["headers"] = {} - combined["headers"].update(headers) + args_dict = args if isinstance(args, dict) else args.model_dump() + combined["url"] = args_dict.get("url", combined.get("url")) + combined["path"] = args_dict.get("path", combined.get("path")) + combined["method"] = args_dict.get("method", combined.get("method")) + combined["stream"] = args_dict.get("stream", combined.get("stream")) + combined["content_body"] = args_dict.get( + "content_body", combined.get("content_body") + ) + + if (json_body := args_dict.get("json_body")) is not None: + combined["json_body"] = combined.get("json_body", {}) + json_body + if (files := args_dict.get("files")) is not None: + combined["files"] = combined.get("files", {}) + files + if (params := args_dict.get("params")) is not None: + combined["params"] = combined.get("params", {}) + params + if (headers := args_dict.get("headers")) is not None: + combined["headers"] = combined.get("headers", {}) + headers return combined @@ -189,44 +145,3 @@ class GenerationRequestTimings(MeasuredRequestTimings): default=None, description="Unix timestamp when the last generation iteration completed.", ) - - -class GenerativeDatasetArgs(StandardBaseDict): - type_: str | None = None - split: str | None = None - prompt_tokens_count_column: str | None = None - output_tokens_count_column: str | None = None - prefix_column: str | None = None - text_column: str | list[str] | None = None - image_column: str | list[str] | None = None - video_column: str | list[str] | None = None - audio_column: str | list[str] | None = None - - def to_kwargs(self) -> dict[str, Any]: - return { - key: value - for key, value in self.model_extra.items() - if not key.endswith("_column") - } - - def get_mapped_columns( - self, - ) -> dict[GenerativeDatasetColumnType | str, str | list[str]]: - column_mapping: dict[GenerativeDatasetColumnType | str, list[str] | None] = {} - - # Add in any non None columns from the fields - for column in get_args(GenerativeDatasetColumnType): - value = getattr(self, column) - if value is not None: - column_mapping[column] = value - - # Enable flexibility for extra columns to be passed through and referenced later - for extra in self.model_extra: - if ( - extra.endswith("_column") - and extra not in column_mapping - and self.model_extra[extra] is not None - ): - column_mapping[extra] = self.model_extra[extra] - - return column_mapping diff --git a/src/guidellm/data/preprocessors/__init__.py b/src/guidellm/data/preprocessors/__init__.py index 039f74a5..664e196b 100644 --- a/src/guidellm/data/preprocessors/__init__.py +++ b/src/guidellm/data/preprocessors/__init__.py @@ -1,7 +1,25 @@ +from .formatters import ( + GenerativeAudioTranscriptionRequestFormatter, + GenerativeAudioTranslationRequestFormatter, + GenerativeChatCompletionsRequestFormatter, + GenerativeTextCompletionsRequestFormatter, +) from .mappers import GenerativeColumnMapper -from .objects import DatasetPreprocessor +from .preprocessor import ( + DataDependentPreprocessor, + DatasetPreprocessor, + PreprocessorRegistry, +) __all__ = [ + "ColumnMapper", + "ColumnMapperRegistry", + "DataDependentPreprocessor", "DatasetPreprocessor", + "GenerativeAudioTranscriptionRequestFormatter", + "GenerativeAudioTranslationRequestFormatter", + "GenerativeChatCompletionsRequestFormatter", "GenerativeColumnMapper", + "GenerativeTextCompletionsRequestFormatter", + "PreprocessorRegistry", ] diff --git a/src/guidellm/data/preprocessors/formatters.py b/src/guidellm/data/preprocessors/formatters.py new file mode 100644 index 00000000..c41ce936 --- /dev/null +++ b/src/guidellm/data/preprocessors/formatters.py @@ -0,0 +1,303 @@ +from __future__ import annotations + +from typing import Any, Literal + +from guidellm.data.objects import ( + GenerationRequest, + GenerationRequestArguments, + GenerativeDatasetColumnType, +) +from guidellm.data.preprocessors.preprocessor import ( + DatasetPreprocessor, + PreprocessorRegistry, +) +from guidellm.data.utils import ( + encode_audio_as_dict, + encode_audio_as_file, + encode_image, + encode_video, +) + +__all__ = [ + "GenerativeAudioTranscriptionRequestFormatter", + "GenerativeAudioTranslationRequestFormatter", + "GenerativeChatCompletionsRequestFormatter", + "GenerativeTextCompletionsRequestFormatter", +] + + +@PreprocessorRegistry.register("text_completions") +class GenerativeTextCompletionsRequestFormatter(DatasetPreprocessor): + def __init__( + self, + model: str, + extras: dict[str, Any] | GenerationRequestArguments | None = None, + stream: bool = True, + max_tokens: int | None = None, + max_completion_tokens: int | None = None, + ): + self.model: str | None = model + self.extras = ( + GenerationRequestArguments(**extras) + if extras and isinstance(extras, dict) + else extras + ) + self.stream: bool = stream + self.max_tokens: int | None = max_tokens or max_completion_tokens + + def __call__( + self, columns: dict[GenerativeDatasetColumnType, list[Any]] + ) -> GenerationRequest: + arguments = {"json_body": {}} + stats = {} + + # Add model + if self.model is not None: + arguments["json_body"]["model"] = self.model + + # Configure streaming + if self.stream: + arguments["json_body"].update( + {"stream": True, "stream_options": {"include_usage": True}} + ) + arguments["stream"] = True + + # Handle output tokens + if output_tokens := columns.get("output_tokens_count_column", []): + output_count = output_tokens[0] + stats["output_tokens"] = output_count + arguments["json_body"].update( + {"max_tokens": output_count, "stop": None, "ignore_eos": True} + ) + elif self.max_tokens is not None: + arguments["json_body"]["max_tokens"] = self.max_tokens + + # Handle prompt tokens + if prompt_tokens := columns.get("prompt_tokens_count_column", []): + stats["prompt_tokens"] = prompt_tokens[0] + + # Apply extra arguments + if self.extras: + arguments = GenerationRequestArguments.model_combine_dict( + arguments, self.extras + ) + + # Build prompt + arguments["json_body"]["prompt"] = "".join( + columns.get("prefix_column", []) + columns.get("text_column", []) + ) + + return GenerationRequest( + request_type="text_completions", + arguments=GenerationRequestArguments(**arguments), + stats=stats, + ) + + +@PreprocessorRegistry.register("chat_completions") +class GenerativeChatCompletionsRequestFormatter(DatasetPreprocessor): + def __init__( + self, + model: str, + extras: dict[str, Any] | GenerationRequestArguments | None = None, + stream: bool = True, + max_tokens: int | None = None, + max_completion_tokens: int | None = None, + encode_kwargs: dict[str, Any] | None = None, + ): + self.model = model + self.extras = ( + GenerationRequestArguments(**extras) + if extras and isinstance(extras, dict) + else extras + ) + self.stream = stream + self.max_completion_tokens = max_tokens or max_completion_tokens + self.encode_image_kwargs = ( + encode_kwargs.get("image", {}) if encode_kwargs else {} + ) + self.encode_video_kwargs = ( + encode_kwargs.get("video", {}) if encode_kwargs else {} + ) + self.encode_audio_kwargs = ( + encode_kwargs.get("audio", {}) if encode_kwargs else {} + ) + + def __call__( + self, columns: dict[GenerativeDatasetColumnType, list[Any]] + ) -> GenerationRequest: + arguments = {"json_body": {}} + stats = {} + + # Add model + if self.model is not None: + arguments["json_body"]["model"] = self.model + + # Configure streaming + if self.stream: + arguments["json_body"].update( + {"stream": True, "stream_options": {"include_usage": True}} + ) + arguments["stream"] = True + + # Handle output tokens + if output_tokens := columns.pop("output_tokens_count_column", []): + output_count = output_tokens[0] + stats["output_tokens"] = output_count + arguments["json_body"].update( + { + "max_completion_tokens": output_count, + "stop": None, + "ignore_eos": True, + } + ) + elif self.max_completion_tokens is not None: + arguments["json_body"]["max_completion_tokens"] = self.max_completion_tokens + + # Handle prompt tokens + if prompt_tokens := columns.pop("prompt_tokens_count_column", []): + stats["prompt_tokens"] = prompt_tokens[0] + + # Apply extra arguments + if self.extras: + arguments = GenerationRequestArguments.model_combine_dict( + arguments, self.extras + ) + + # Build messages + arguments["json_body"]["messages"] = ( + [ + {"role": "system", "content": prefix} + for prefix in columns.pop("prefix_column", []) + ] + + [ + {"role": "user", "content": [{"type": "text", "text": text}]} + for text in columns.pop("text_column", []) + ] + + [ + { + "role": "user", + "content": [ + { + "type": "image_url", + "image_url": encode_image( + image, **self.encode_image_kwargs + ), + } + ], + } + for image in columns.pop("image_column", []) + ] + + [ + { + "role": "user", + "content": [ + { + "type": "video_url", + "video_url": encode_video( + video, **self.encode_video_kwargs + ), + } + ], + } + for video in columns.pop("video_column", []) + ] + + [ + { + "role": "user", + "content": [ + { + "type": "input_audio", + "input_audio": encode_audio_as_dict( + audio, **self.encode_audio_kwargs + ), + } + ], + } + for audio in columns.pop("audio_column", []) + ] + ) + + return GenerationRequest( + request_type="chat_completions", + arguments=GenerationRequestArguments(**arguments), + stats=stats, + ) + + +@PreprocessorRegistry.register("audio_transcriptions") +class GenerativeAudioTranscriptionRequestFormatter(DatasetPreprocessor): + def __init__( + self, + model: str, + extra_args: dict[str, Any] | GenerationRequestArguments | None = None, + stream: bool = True, + encode_kwargs: dict[str, Any] | None = None, + ): + self.model = model + self.extra_args = extra_args + self.stream = stream + self.encode_audio_kwargs = encode_kwargs or {} + + def __call__( + self, columns: dict[GenerativeDatasetColumnType, list[Any]] + ) -> GenerationRequest: + arguments = {"json_body": {}} + stats = {} + + # Add model + if self.model is not None: + arguments["json_body"]["model"] = self.model + + # Configure streaming + if self.stream: + arguments["json_body"].update( + {"stream": True, "stream_options": {"include_usage": True}} + ) + + # Apply extra arguments + if self.extra_args: + arguments = GenerationRequestArguments.model_combine_dict( + arguments, self.extra_args + ) + + # Handle stats tokens + if output_tokens := columns.get("output_tokens_count_column", []): + output_count = output_tokens[0] + stats["output_tokens"] = output_count + if prompt_tokens := columns.get("prompt_tokens_count_column", []): + stats["prompt_tokens"] = prompt_tokens[0] + + # Build audio input + if audio := columns.get("audio_column", []): + arguments["files"]["file"] = encode_audio_as_file( + audio[0], **self.encode_audio_kwargs + ) + else: + raise ValueError("No audio column found for audio transcription request.") + + # Build prompt + if (prefix := columns.get("prefix_column", [])) or ( + text := columns.get("text_column", []) + ): + arguments["json_body"]["prompt"] = "".join(prefix) + "".join(text) + + return { + "request": { + "request_type": "audio_transcriptions", + "arguments": arguments, + "stats": stats, + } + } + + +@PreprocessorRegistry.register("audio_translations") +class GenerativeAudioTranslationRequestFormatter( + GenerativeAudioTranscriptionRequestFormatter +): + def __call__( + self, columns: dict[GenerativeDatasetColumnType, list[Any]] + ) -> dict[Literal["request"], dict[Literal["request_type"], Any]]: + result = super().__call__(columns) + result["request"]["request_type"] = "audio_translations" + return result diff --git a/src/guidellm/data/preprocessors/mappers.py b/src/guidellm/data/preprocessors/mappers.py index 1792cb7e..56ca0342 100644 --- a/src/guidellm/data/preprocessors/mappers.py +++ b/src/guidellm/data/preprocessors/mappers.py @@ -1,115 +1,182 @@ from __future__ import annotations -from dataclasses import dataclass -from typing import Any, Literal +from collections import defaultdict +from typing import Any, ClassVar from datasets import Dataset, IterableDataset -from guidellm.data.objects import ( - GenerativeDatasetArgs, - GenerativeDatasetColumnType, +from guidellm.data.objects import GenerativeDatasetColumnType +from guidellm.data.preprocessors.preprocessor import ( + DataDependentPreprocessor, + PreprocessorRegistry, ) -from guidellm.data.preprocessors.objects import DatasetPreprocessor -from guidellm.data.utils import DEFAULT_COLUMN_NAMES -__all__ = ["ColumnMapping", "GenerativeColumnMapper"] +__all__ = ["GenerativeColumnMapper"] + + +@PreprocessorRegistry.register("generative_column_mapper") +class GenerativeColumnMapper(DataDependentPreprocessor): + defaults: ClassVar[dict[str, list[str]]] = { + "prompt_tokens_count_column": ["prompt_tokens_count", "input_tokens_count"], + "output_tokens_count_column": [ + "output_tokens_count", + "completion_tokens_count", + ], + "prefix_column": [ + "system_prompt", + "system", + "prefix", + ], + "text_column": [ + "prompt", + "instruction", + "question", + "input", + "context", + "content", + "conversation", + "turn", + "text", + ], + "image_column": [ + "image", + "picture", + "photo", + "img", + ], + "video_column": [ + "video", + "clip", + "movie", + "footage", + "mp4", + "mov", + "avi", + ], + "audio_column": [ + "audio", + "sound", + "voice", + "speech", + "wav", + "mp3", + ], + } + + @classmethod + def datasets_default_mappings( + cls, datasets: list[Dataset | IterableDataset] + ) -> dict[str, list[tuple[int, str]]]: + mappings: dict[GenerativeDatasetColumnType, list[tuple[int, str]]] = ( + defaultdict(list) + ) + + for index, dataset in enumerate(datasets): + dataset_columns = dataset.column_names or list(next(iter(dataset)).keys()) + + for column_type in cls.defaults: + if column_type in mappings: + continue + + type_names = [ + variant + for name in cls.defaults.get(column_type, []) + for plural in [name, f"{name}s", f"{name}es"] + for variant in [ + plural, + plural.lower(), + plural.upper(), + plural.capitalize(), + ] + ] + + for name in type_names: + if name in dataset_columns: + mappings[column_type].append((index, name)) + break + return mappings -@dataclass -class ColumnMapping: - indices: list[int] - names: list[str] + @classmethod + def datasets_mappings( + cls, + datasets: list[Dataset | IterableDataset], + input_mappings: dict[GenerativeDatasetColumnType, str | list[str]], + ) -> dict[GenerativeDatasetColumnType, list[tuple[int, str]]]: + mappings: dict[GenerativeDatasetColumnType, list[tuple[int, str]]] = ( + defaultdict(list) + ) + datasets_named_indices = { + ( + dataset.info.dataset_name + if dataset.info and dataset.info.dataset_name + else index + ): index + for index, dataset in enumerate(datasets) + } + datasets_columns = { + index: dataset.column_names or list(next(iter(dataset)).keys()) + for index, dataset in enumerate(datasets) + } + + for column_type, names in input_mappings.items(): + mappings[column_type] = [] + + for name in names if isinstance(names, list) else [names]: + dataset, column_name = name.split(".", 1) + dataset_index = ( + int(dataset) + if dataset.isdigit() + else datasets_named_indices.get(dataset) + ) + if dataset_index is None or dataset_index >= len(datasets): + raise ValueError( + f"Dataset '{dataset}' not found in datasets: " + f"{datasets_named_indices}." + ) + if column_name not in datasets_columns[dataset_index]: + raise ValueError( + f"Column '{column_name}' not found in dataset '{dataset}' " + f"columns: {datasets_columns[dataset_index]}." + ) + mappings[column_type].append((dataset_index, column_name)) + return mappings -class GenerativeColumnMapper(DatasetPreprocessor): - def __init__(self): - self.datasets: list[Dataset | IterableDataset] | None = None - self.data_args: list[GenerativeDatasetArgs] | None = None - self.column_mappings: ( - dict[GenerativeDatasetColumnType, ColumnMapping | None] | None - ) = None + def __init__( + self, + column_mappings: dict[GenerativeDatasetColumnType, str | list[str]] + | None = None, + ): + self.input_mappings = column_mappings + self.datasets_column_mappings: ( + dict[GenerativeDatasetColumnType, list[tuple[int, str]]] | None + ) - def __call__( - self, row: dict[Literal["items"], tuple[dict[str, Any]]] - ) -> dict[str, Any]: - if ( - self.datasets is None - or self.data_args is None - or self.column_mapping is None - ): - raise ValueError("GenerativeColumnMapper not initialized with data.") + def __call__(self, row: dict[int, list[dict[str, Any]]]) -> dict[str, list[Any]]: + if self.datasets_column_mappings is None: + raise ValueError("DefaultGenerativeColumnMapper not setup with data.") - mapped: dict[GenerativeDatasetColumnType, list[Any]] = {} items = row.pop("items") + mapped: dict[GenerativeDatasetColumnType, list[Any]] = defaultdict(list) - for column_type, column_mapping in self.column_mapping.items(): - mapped[column_type] = [ - items[index].get(name) - for index, name in zip(column_mapping.indices, column_mapping.names) - ] + for column_type, column_mappings in self.datasets_column_mappings.items(): + for ( + dataset_index, + dataset_column, + ) in column_mappings: + mapped[column_type].append(items[dataset_index][dataset_column]) - return mapped + return dict(mapped) - def init_data( + def setup_data( self, datasets: list[Dataset | IterableDataset], - data_args: list[GenerativeDatasetArgs], + data_args: list[dict[str, Any]], ): - self.datasets = datasets - self.data_args = data_args - self.column_mapping = self.generate_column_mapping() - - def generate_column_mapping( - self, - ) -> dict[GenerativeDatasetColumnType, ColumnMapping]: - mappings: dict[GenerativeDatasetColumnType, ColumnMapping] = {} - # Map any columns specified in the GenerativeDatasetArgs first - self._fill_mappings_from_data_args(mappings) - # For standard column types not mapped, fill in first one found from defaults - self._fill_mappings_from_defaults(mappings) - - return mappings - - def _fill_mappings_from_data_args( - self, mappings: dict[GenerativeDatasetColumnType, ColumnMapping] - ): - for index, args in enumerate(self.data_args): - args_column_mappings = args.get_mapped_columns() - for column_type, column_name in args_column_mappings.items(): - if column_type not in mappings: - mappings[column_type] = ColumnMapping(indices=[], names=[]) - column_mapping = mappings[column_type] - - for name in ( - column_name if isinstance(column_name, list) else [column_name] - ): - if name not in self.datasets[index].column_names: - raise ValueError( - f"Column '{name}' not found in dataset columns: " - f"{self.datasets[index].column_names}" - ) - column_mapping.indices.append(index) - column_mapping.names.append(name) - - def _fill_mappings_from_defaults( - self, mappings: dict[GenerativeDatasetColumnType, ColumnMapping] - ): - for column_type, default_names in DEFAULT_COLUMN_NAMES.items(): - if column_type in mappings: - continue - - for index, dataset in enumerate(self.datasets): - for name in default_names: - if name in dataset.column_names: - mappings[column_type] = ColumnMapping( - indices=[index], names=[name] - ) - break - # Check for plural form of the name - if f"{name}s" in dataset.column_names: - mappings[column_type] = ColumnMapping( - indices=[index], names=[f"{name}s"] - ) - break - if column_type in mappings: - break + _ = data_args # Unused for this mapper + self.datasets_column_mappings = ( + self.datasets_default_mappings(datasets) + if self.input_mappings is None + else self.datasets_mappings(datasets, self.input_mappings) + ) diff --git a/src/guidellm/data/preprocessors/objects.py b/src/guidellm/data/preprocessors/objects.py deleted file mode 100644 index 831f944d..00000000 --- a/src/guidellm/data/preprocessors/objects.py +++ /dev/null @@ -1,20 +0,0 @@ -from __future__ import annotations - -from typing import Any, Protocol, runtime_checkable - -from datasets import Dataset, IterableDataset - -from guidellm.data.objects import GenerativeDatasetArgs - -__all__ = ["DatasetPreprocessor"] - - -@runtime_checkable -class DatasetPreprocessor(Protocol): - def init_data( - self, - datasets: list[Dataset | IterableDataset], - data_args: list[GenerativeDatasetArgs], - ): ... - - def __call__(self, item: dict[str, Any]) -> dict[str, Any]: ... diff --git a/src/guidellm/data/preprocessors/preprocessor.py b/src/guidellm/data/preprocessors/preprocessor.py new file mode 100644 index 00000000..eefb53d3 --- /dev/null +++ b/src/guidellm/data/preprocessors/preprocessor.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from typing import Any, Protocol, Union, runtime_checkable + +from datasets import Dataset, IterableDataset + +from guidellm.utils import RegistryMixin + +__all__ = ["DataDependentPreprocessor", "DatasetPreprocessor", "PreprocessorRegistry"] + + +@runtime_checkable +class DatasetPreprocessor(Protocol): + def __call__(self, item: dict[str, Any]) -> dict[str, Any]: ... + + +@runtime_checkable +class DataDependentPreprocessor(DatasetPreprocessor, Protocol): + def setup_data( + self, + datasets: list[Dataset | IterableDataset], + data_args: list[dict[str, Any]], + ): ... + + +class PreprocessorRegistry( + RegistryMixin[Union[DataDependentPreprocessor, type[DataDependentPreprocessor]]] +): + pass diff --git a/src/guidellm/data/processor.py b/src/guidellm/data/processor.py new file mode 100644 index 00000000..645683c4 --- /dev/null +++ b/src/guidellm/data/processor.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +from typing import Any + +from transformers import ( # type: ignore[import] + AutoTokenizer, + PreTrainedTokenizerBase, +) + +__all__ = ["ProcessorFactory"] + + +class ProcessorFactory: + def __init__( + self, + processor: str | PreTrainedTokenizerBase, + processor_args: dict[str, Any] | None = None, + ) -> None: + self.processor = processor + self.processor_args = processor_args or {} + + def __call__(self) -> PreTrainedTokenizerBase: + if isinstance(self.processor, PreTrainedTokenizerBase): + return self.processor + else: + self.processor = AutoTokenizer.from_pretrained( + self.processor, + **(self.processor_args or {}), + ) + return self.processor diff --git a/src/guidellm/data/utils/__init__.py b/src/guidellm/data/utils/__init__.py new file mode 100644 index 00000000..aac657f8 --- /dev/null +++ b/src/guidellm/data/utils/__init__.py @@ -0,0 +1,34 @@ +from .dataset import DEFAULT_SPLITS, resolve_dataset_split +from .functions import ( + download_audio, + download_image, + download_video, + encode_audio, + encode_audio_as_dict, + encode_audio_as_file, + encode_image, + encode_image_base64, + encode_video, + encode_video_base64, + get_file_format, + is_url, + resize_image, +) + +__all__ = [ + "DEFAULT_SPLITS", + "download_audio", + "download_image", + "download_video", + "encode_audio", + "encode_audio_as_dict", + "encode_audio_as_file", + "encode_image", + "encode_image_base64", + "encode_video", + "encode_video_base64", + "get_file_format", + "is_url", + "resize_image", + "resolve_dataset_split", +] diff --git a/src/guidellm/data/utils.py b/src/guidellm/data/utils/dataset.py similarity index 54% rename from src/guidellm/data/utils.py rename to src/guidellm/data/utils/dataset.py index d2fa1f9c..9656c1a7 100644 --- a/src/guidellm/data/utils.py +++ b/src/guidellm/data/utils/dataset.py @@ -1,18 +1,10 @@ from __future__ import annotations -import contextlib -import math -from collections.abc import Iterator -from typing import Any, Literal +from typing import Literal from datasets import Dataset, DatasetDict, IterableDataset, IterableDatasetDict -__all__ = [ - "DEFAULT_COLUMN_NAMES", - "DEFAULT_SPLITS", - "datasets_item_iterator", - "resolve_dataset_split", -] +__all__ = ["DEFAULT_SPLITS", "resolve_dataset_split"] DEFAULT_SPLITS: dict[Literal["train", "calib", "val", "test"], list[str]] = { @@ -77,54 +69,9 @@ } -DEFAULT_COLUMN_NAMES: dict[str, list[str]] = { - "prompt_tokens_count": ["prompt_tokens_count", "input_tokens_count"], - "output_tokens_count": ["output_tokens_count", "completion_tokens_count"], - "prefix_column": [ - "system_prompt", - "system", - "prefix", - ], - "text_column": [ - "prompt", - "instruction", - "question", - "input", - "context", - "content", - "conversation", - "turn", - "text", - ], - "image_column": [ - "image", - "picture", - "photo", - "img", - ], - "video_column": [ - "video", - "clip", - "movie", - "footage", - "mp4", - "mov", - "avi", - ], - "audio_column": [ - "audio", - "sound", - "voice", - "speech", - "wav", - "mp3", - ], -} - - def resolve_dataset_split( dataset: Dataset | IterableDataset | DatasetDict | IterableDatasetDict, - split: str | None, + split: str | None = None, ) -> Dataset | IterableDataset: if split is not None and isinstance(dataset, (DatasetDict, IterableDatasetDict)): if split in dataset: @@ -145,22 +92,3 @@ def resolve_dataset_split( return dataset[default_split] return dataset[list(dataset.keys())[0]] - - -def datasets_item_iterator( - datasets: list[Dataset | IterableDataset], - data_samples: int, -) -> Iterator[dict[Literal["items"], tuple[dict[str, Any]]]]: - dataset_iters = [iter(dataset) for dataset in datasets] - gen_count = 0 - - with contextlib.suppress(StopIteration): - while gen_count < data_samples or data_samples <= 0 or data_samples == math.inf: - yield {"items": tuple(next(dataset_iter) for dataset_iter in dataset_iters)} - gen_count += 1 - - if gen_count < data_samples and data_samples > 0 and data_samples != math.inf: - raise ValueError( - f"Requested {data_samples} samples, but only {gen_count} available " - "from the provided datasets." - ) diff --git a/src/guidellm/data/formatters/filters.py b/src/guidellm/data/utils/functions.py similarity index 73% rename from src/guidellm/data/formatters/filters.py rename to src/guidellm/data/utils/functions.py index 8dd4e445..c9ca20ed 100644 --- a/src/guidellm/data/formatters/filters.py +++ b/src/guidellm/data/utils/functions.py @@ -3,7 +3,7 @@ import base64 import io from pathlib import Path -from typing import Any, Callable, Literal +from typing import Any, Literal import datasets import httpx @@ -11,15 +11,15 @@ import numpy as np import soundfile from PIL import Image as PILImage - -from guidellm.utils import RegistryMixin +from pydub import AudioSegment __all__ = [ - "JinjaFiltersRegistry", "download_audio", "download_image", "download_video", "encode_audio", + "encode_audio_as_dict", + "encode_audio_as_file", "encode_image", "encode_image_base64", "encode_video", @@ -30,16 +30,10 @@ ] -class JinjaFiltersRegistry(RegistryMixin[Callable[..., Any]]): - pass - - -@JinjaFiltersRegistry.register("is_url") def is_url(text: Any) -> bool: return isinstance(text, str) and text.startswith(("http://", "https://")) -@JinjaFiltersRegistry.register("encode_image") def encode_image( image: bytes | str | Path | np.ndarray | PILImage.Image | datasets.Image, max_size: int | None = None, @@ -90,7 +84,6 @@ def encode_image( ) -@JinjaFiltersRegistry.register("encode_image_base64") def encode_image_base64( image: bytes | str | Path | np.ndarray | PILImage.Image, width: int | None = None, @@ -137,7 +130,6 @@ def encode_image_base64( return f"data:image/jpeg;base64,{image_base64}" -@JinjaFiltersRegistry.register("resize_image") def resize_image( image: PILImage.Image, width: int | None = None, @@ -183,14 +175,12 @@ def resize_image( return image -@JinjaFiltersRegistry.register("download_image") def download_image(url: str) -> bytes: response = httpx.get(url) response.raise_for_status() return response.content -@JinjaFiltersRegistry.register("encode_video") def encode_video( video: bytes | str | Path | datasets.Video, encode_type: Literal["base64", "url"] | None = None, @@ -221,7 +211,6 @@ def encode_video( return encode_video_base64(video=video) -@JinjaFiltersRegistry.register("encode_video_base64") def encode_video_base64(video: bytes | str | Path) -> str: if ( isinstance(video, str) @@ -246,78 +235,121 @@ def encode_video_base64(video: bytes | str | Path) -> str: return f"data:video/{video_format};base64,{video_base64}" -@JinjaFiltersRegistry.register("download_video") def download_video(url: str) -> tuple[bytes, str]: response = httpx.get(url) response.raise_for_status() return response.content, get_file_format(url) -@JinjaFiltersRegistry.register("encode_audio") -def encode_audio( +def encode_audio_as_dict( audio: bytes | str | Path | dict | np.ndarray, - sample_rate: int | None = None, + sample_rate: int | None = 16000, max_duration: float | None = None, -) -> dict[str, str]: - """ - Input audio types: - - bytes: raw audio bytes - - str: file path on disk or URL - - pathlib.Path: file path on disk - - dict: {"data": base64_string, "format": "wav"} format - - numpy.ndarray: audio array, assumed to be at sample_rate if provided - - sample_rate: sample rate of the input audio if input is np.ndarray - target_sample_rate: resample to this rate if provided - duration: limit audio to this duration in seconds if provided + mono: bool = True, + audio_format: str = "mp3", + bitrate: str = "64k", +) -> dict[Literal["data", "format"], Any]: + content, file_name, file_format = encode_audio( + audio=audio, + sample_rate=sample_rate or 16000, + max_duration=max_duration, + mono=mono, + audio_format=audio_format, + bitrate=bitrate, + ) - Returns dict with format: - { - "data": base64_encoded_audio_bytes, - "format": "wav" + return { + "data": base64.b64encode(content).decode("utf-8"), + "format": file_format, } - """ - if is_url(audio): - audio, _ = download_audio(audio) - if isinstance(audio, dict): - if "data" not in audio: - raise ValueError("Audio dict must contain 'data' key") - audio = base64.b64decode(audio["data"]) - if isinstance(audio, bytes): - audio_data, sample_rate = librosa.load(io.BytesIO(audio), sr=sample_rate) +def encode_audio_as_file( + audio: bytes | str | Path | dict | np.ndarray, + sample_rate: int | None = 16000, + max_duration: float | None = None, + mono: bool = True, + audio_format: str = "mp3", + bitrate: str = "64k", +) -> tuple[str, bytes, str]: + content, file_name, file_format = encode_audio( + audio=audio, + sample_rate=sample_rate or 16000, + max_duration=max_duration, + mono=mono, + audio_format=audio_format, + bitrate=bitrate, + ) + + return file_name, content, f"audio/{file_format}" + + +def encode_audio( + audio: bytes | str | Path | dict, + sample_rate: int = 16000, + max_duration: float | None = None, + mono: bool = True, + audio_format: str = "mp3", + bitrate: str = "64k", +) -> tuple[bytes, str, str]: + file_name = "audio.wav" + + if is_url(audio): + audio, file_name, _ = download_audio(audio) + elif isinstance(audio, dict): + file_name = audio.get("name", "audio") + audio = base64.b64decode(audio["data"]) elif isinstance(audio, (str, Path)): - audio_data, sample_rate = librosa.load(str(audio), sr=sample_rate) - elif isinstance(audio, np.ndarray): - if sample_rate is None: - raise ValueError("sample_rate must be provided for numpy arrays") - audio_data = audio - else: + path = Path(audio) + file_name = get_file_name(path) + audio = path.read_bytes() + elif not isinstance(audio, bytes): raise ValueError(f"Unsupported audio type: {type(audio)}") - if max_duration is not None: - max_samples = int(max_duration * sample_rate) - if len(audio_data) > max_samples: - audio_data = audio_data[:max_samples] + processed_audio, sample_rate = librosa.load( + io.BytesIO(audio), + sr=sample_rate, + mono=mono, + duration=max_duration, + ) + # Encode to target format buffer = io.BytesIO() - soundfile.write(buffer, audio_data, sample_rate, format="WAV", subtype="PCM_16") + if audio_format.lower() == "mp3": + temp_wav = io.BytesIO() + soundfile.write( + temp_wav, + processed_audio, + sample_rate, + format="WAV", + subtype="PCM_16", + ) + temp_wav.seek(0) + AudioSegment.from_wav(temp_wav).export(buffer, format="mp3", bitrate=bitrate) + else: + soundfile.write( + buffer, + processed_audio, + sample_rate, + format=audio_format.upper(), + ) - return {"data": buffer.getvalue(), "format": "wav"} + return buffer.getvalue(), file_name, audio_format.lower() -@JinjaFiltersRegistry.register("download_audio") -def download_audio(url: str) -> tuple[bytes, str]: - """Download audio from URL and return bytes with format.""" +def download_audio(url: str) -> tuple[bytes, str, str]: response = httpx.get(url) response.raise_for_status() content = response.content - audio_format = get_file_format(url) - return content, audio_format + + return content, get_file_name(url), get_file_format(url) + + +def get_file_name(path: Path | str) -> str: + """Get file name from path.""" + return Path(path).name -@JinjaFiltersRegistry.register("get_file_format") def get_file_format(path: Path | str) -> str: """Get file format from path extension.""" suffix = Path(path).suffix.lower() diff --git a/src/guidellm/scheduler/worker.py b/src/guidellm/scheduler/worker.py index 5f2fb74b..1832d25f 100644 --- a/src/guidellm/scheduler/worker.py +++ b/src/guidellm/scheduler/worker.py @@ -233,6 +233,12 @@ async def _processing_startup(self): self.backend_started = True await self.backend.validate() + # Wait for all processes to be ready + await wait_for_sync_barrier( + self.startup_barrier, + poll_interval=self.messaging.poll_interval, + ) + # Get messaging system ready await self.messaging.start( receive_stop_criteria=[self.requests_generated_event], @@ -240,12 +246,6 @@ async def _processing_startup(self): ) self.messaging_started = True - # Wait for all processes to be ready - await wait_for_sync_barrier( - self.startup_barrier, - poll_interval=self.messaging.poll_interval, - ) - self.startup_completed = True async def _processing_shutdown(self): diff --git a/src/guidellm/scheduler/worker_group.py b/src/guidellm/scheduler/worker_group.py index e64d64fc..9baccd1b 100644 --- a/src/guidellm/scheduler/worker_group.py +++ b/src/guidellm/scheduler/worker_group.py @@ -495,6 +495,7 @@ def _iter(): count = 0 request_info: ScheduledRequestInfo = None + for request in _iter(): count += 1 diff --git a/src/guidellm/settings.py b/src/guidellm/settings.py index 20d9ff96..5c360eff 100644 --- a/src/guidellm/settings.py +++ b/src/guidellm/settings.py @@ -145,7 +145,7 @@ class Settings(BaseSettings): mp_max_pending_buffer_percent: float = 0.5 mp_max_worker_buffer_percent: float = 0.2 max_concurrency: int = 512 - max_worker_processes: int = 10 + max_worker_processes: int = 2 scheduler_start_delay_non_distributed: float = 1.0 constraint_error_window_size: float = 30 constraint_error_min_processed: float = 30 diff --git a/src/guidellm/utils/messaging.py b/src/guidellm/utils/messaging.py index c56ec29a..2f631a87 100644 --- a/src/guidellm/utils/messaging.py +++ b/src/guidellm/utils/messaging.py @@ -610,6 +610,8 @@ def _send_messages_task_thread( # noqa: C901, PLR0912 except (culsans.QueueFull, queue.Full): pass + time.sleep(0) # Yield to other threads + def _receive_messages_task_thread( # noqa: C901 self, receive_callback: Callable[[Any], Any] | None, @@ -649,6 +651,8 @@ def _receive_messages_task_thread( # noqa: C901 except (culsans.QueueFull, queue.Full): pass + time.sleep(0) # Yield to other threads + class InterProcessMessagingManagerQueue( InterProcessMessagingQueue[SendMessageT, ReceiveMessageT] From 616ef92e5cd456ca7db971deb822e88119f3c57b Mon Sep 17 00:00:00 2001 From: Mark Kurtz Date: Wed, 8 Oct 2025 15:44:55 -0400 Subject: [PATCH 10/57] Fix audio pathways so requests work --- pyproject.toml | 2 +- src/guidellm/__init__.py | 4 +- src/guidellm/__main__.py | 1 + src/guidellm/backends/openai.py | 4 +- src/guidellm/benchmark/aggregator.py | 2 +- src/guidellm/benchmark/objects.py | 6 +- .../data/deserializers/deserializer.py | 16 +- .../data/deserializers/huggingface.py | 6 + src/guidellm/data/loaders.py | 88 ++++---- src/guidellm/data/objects.py | 10 + src/guidellm/data/preprocessors/formatters.py | 22 +- src/guidellm/data/preprocessors/mappers.py | 35 ++-- src/guidellm/data/utils/functions.py | 190 +++++++++++++----- src/guidellm/scheduler/worker_group.py | 4 +- src/guidellm/settings.py | 2 +- src/guidellm/utils/cli.py | 4 +- 16 files changed, 266 insertions(+), 130 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3461530d..6ccbf06a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,7 @@ dependencies = [ "httpx[http2]<1.0.0", "loguru", "msgpack", - "numpy", + "numpy<2.0.0", "pillow", "protobuf", "pydantic>=2.11.7", diff --git a/src/guidellm/__init__.py b/src/guidellm/__init__.py index dde6e937..f466073e 100644 --- a/src/guidellm/__init__.py +++ b/src/guidellm/__init__.py @@ -7,7 +7,7 @@ import logging import os -from datasets.utils.logging import disable_progress_bar +from datasets import config with ( open(os.devnull, "w") as devnull, # noqa: PTH123 @@ -21,7 +21,7 @@ os.environ["TOKENIZERS_PARALLELISM"] = "false" # Silence warnings for tokenizers hf_logging.set_verbosity_error() logging.getLogger("transformers").setLevel(logging.ERROR) - disable_progress_bar() + config.USE_AUDIO_DECODE = False from .logger import configure_logger, logger from .settings import ( diff --git a/src/guidellm/__main__.py b/src/guidellm/__main__.py index 43939fa7..4bb43d0f 100644 --- a/src/guidellm/__main__.py +++ b/src/guidellm/__main__.py @@ -245,6 +245,7 @@ def benchmark(): ) @click.option( "--data-args", + multiple=True, default=None, callback=cli_tools.parse_json, help=( diff --git a/src/guidellm/backends/openai.py b/src/guidellm/backends/openai.py index 22394afe..f8ccaafb 100644 --- a/src/guidellm/backends/openai.py +++ b/src/guidellm/backends/openai.py @@ -248,7 +248,7 @@ async def resolve( # noqa: C901 request.arguments.method or "POST", request.arguments.url, content=request.arguments.content_body, - files=request.arguments.files, + files=request.arguments.request_files, json=request.arguments.json_body, params=request.arguments.params, headers=request.arguments.headers, @@ -281,7 +281,7 @@ async def resolve( # noqa: C901 request.arguments.method or "POST", request.arguments.url, content=request.arguments.content_body, - files=request.arguments.files, + files=request.arguments.request_files, json=request.arguments.json_body, params=request.arguments.params, headers=request.arguments.headers, diff --git a/src/guidellm/benchmark/aggregator.py b/src/guidellm/benchmark/aggregator.py index 562fc36c..2dc3c56f 100644 --- a/src/guidellm/benchmark/aggregator.py +++ b/src/guidellm/benchmark/aggregator.py @@ -937,7 +937,7 @@ def _create_generative_request_stats( return GenerativeRequestStats( request_id=request.request_id, request_type=request.request_type, - request_args=request.arguments, + request_args=str(request.arguments), output=response.text if response else None, iterations=response.iterations if response else 0, prompt_tokens=( diff --git a/src/guidellm/benchmark/objects.py b/src/guidellm/benchmark/objects.py index a9b5ff79..c3481303 100644 --- a/src/guidellm/benchmark/objects.py +++ b/src/guidellm/benchmark/objects.py @@ -35,7 +35,7 @@ Profile, ) from guidellm.data import ( - GenerationRequestArguments, + GenerativeRequestType, ) from guidellm.scheduler import ( ScheduledRequestInfo, @@ -214,10 +214,10 @@ class GenerativeRequestStats(BenchmarkRequestStats): type_: Literal["generative_request_stats"] = "generative_request_stats" request_id: str = Field(description="Unique identifier for the request") - request_type: Literal["text_completions", "chat_completions"] = Field( + request_type: GenerativeRequestType | str = Field( description="Type of generative request: text or chat completion" ) - request_args: GenerationRequestArguments | None = Field( + request_args: str | None = Field( default=None, description="Arguments passed to the backend for this request" ) output: str | None = Field( diff --git a/src/guidellm/data/deserializers/deserializer.py b/src/guidellm/data/deserializers/deserializer.py index c7e2f1da..cb362710 100644 --- a/src/guidellm/data/deserializers/deserializer.py +++ b/src/guidellm/data/deserializers/deserializer.py @@ -43,6 +43,8 @@ def deserialize( random_seed: int = 42, type_: str | None = None, resolve_split: bool = True, + select_columns: list[str] | None = None, + remove_columns: list[str] | None = None, **data_kwargs: dict[str, Any], ) -> Dataset | IterableDataset: dataset = None @@ -78,4 +80,16 @@ def deserialize( f"with kwargs {data_kwargs} and type_ {type_}." ) - return resolve_dataset_split(dataset) if resolve_split else dataset + if resolve_split: + dataset = resolve_dataset_split(dataset) + + if select_columns is not None or remove_columns is not None: + column_names = dataset.column_names or list(next(iter(dataset)).keys()) + if select_columns is not None: + remove_columns = [ + col for col in column_names if col not in select_columns + ] + + dataset = dataset.remove_columns(remove_columns) + + return dataset diff --git a/src/guidellm/data/deserializers/huggingface.py b/src/guidellm/data/deserializers/huggingface.py index 3e0cf090..69f7d506 100644 --- a/src/guidellm/data/deserializers/huggingface.py +++ b/src/guidellm/data/deserializers/huggingface.py @@ -62,6 +62,12 @@ def __call__( except Exception as err: # noqa: BLE001 load_error = err + try: + # Handle dataset identifier from the Hugging Face Hub + return load_dataset(str(data), **data_kwargs) + except Exception as err: # noqa: BLE001 + load_error = err + not_supported = DataNotSupportedError( "Unsupported data for HuggingFaceDatasetDeserializer, " "expected Dataset, IterableDataset, DatasetDict, IterableDatasetDict, " diff --git a/src/guidellm/data/loaders.py b/src/guidellm/data/loaders.py index 303e5a8d..89098964 100644 --- a/src/guidellm/data/loaders.py +++ b/src/guidellm/data/loaders.py @@ -14,31 +14,47 @@ from guidellm.data.objects import GenerationRequest from guidellm.data.preprocessors import DataDependentPreprocessor, DatasetPreprocessor -__all__ = ["DataLoader", "datasets_item_iterator"] - - -def datasets_item_iterator( - datasets: list[Dataset | IterableDataset], - data_samples: int, - preprocessors: tuple[DatasetPreprocessor | DataDependentPreprocessor], -) -> Iterator[Any]: - gen_count = 0 - dataset_iters = [iter(dataset) for dataset in datasets] - - with contextlib.suppress(StopIteration): - while gen_count < data_samples or data_samples == math.inf: - row = {"items": [next(dataset_iter) for dataset_iter in dataset_iters]} - for preprocessor in preprocessors: - row = preprocessor(row) - yield row - gen_count += 1 - - if data_samples != math.inf and gen_count < data_samples: - raise ValueError( - f"Requested {data_samples} samples, but only {gen_count} " - "available from the provided datasets." +__all__ = ["DataIterator", "DataLoader"] + + +class DataIterator: + def __init__( + self, + datasets: list[Dataset | IterableDataset], + preprocessors: list[DatasetPreprocessor | DataDependentPreprocessor], + precache_size: int | None = None, + ): + self.datasets = datasets + self.preprocessors = preprocessors + self.precache = ( + None if not precache_size else list(self.generator(precache_size)) ) + def __iter__(self): + if self.precache is not None: + yield from self.precache + else: + yield from self.generator() + + def generator(self, max_items: int | None = None) -> Iterator[Any]: + gen_count = 0 + + with contextlib.suppress(StopIteration): + dataset_iters = [iter(dataset) for dataset in self.datasets] + + while max_items is None or gen_count < max_items: + row = {"items": [next(dataset_iter) for dataset_iter in dataset_iters]} + for preprocessor in self.preprocessors: + row = preprocessor(row) + yield row + gen_count += 1 + + if max_items is not None and gen_count < max_items: + raise ValueError( + f"Requested {max_items} samples, but only {gen_count} " + "available from the provided datasets." + ) + class DataLoader(PyTorchDataLoader[GenerationRequest]): def __init__( @@ -68,14 +84,11 @@ def __init__( datasets = [] for datum, data_kwargs in zip(data, data_args): - type_ = data_kwargs.pop("type_") if "type_" in data_kwargs else None datasets.append( DatasetDeserializerFactory.deserialize( data=datum, - data_kwargs=data_args, processor_factory=processor_factory, random_seed=random_seed, - type_=type_, **data_kwargs, ) ) @@ -85,20 +98,15 @@ def __init__( datasets=datasets, data_args=data_args, ) - if data_samples != math.inf and data_samples > 0: - cached_samples = list( - datasets_item_iterator(datasets, data_samples, tuple(preprocessors)) - ) - dataset = IterableDataset.from_generator(lambda: cached_samples) - else: - dataset = IterableDataset.from_generator( - datasets_item_iterator, - gen_kwargs={ - "datasets": datasets, - "data_samples": math.inf, - "preprocessors": tuple(preprocessors), - }, - ) + + data_iterator = DataIterator( + datasets=datasets, + preprocessors=preprocessors, + precache_size=data_samples + if data_samples != math.inf and data_samples > 0 + else None, + ) + dataset = IterableDataset.from_generator(data_iterator.__iter__) super().__init__( dataset=dataset, diff --git a/src/guidellm/data/objects.py b/src/guidellm/data/objects.py index 2a4b3857..095014d3 100644 --- a/src/guidellm/data/objects.py +++ b/src/guidellm/data/objects.py @@ -103,6 +103,16 @@ def model_combine_dict( # noqa: C901, PLR0912 description="HTTP headers to include in the request, if applicable.", ) + @property + def request_files(self) -> dict[str, Any] | None: + if not self.files: + return None + + return { + key: value if not isinstance(value, list) else tuple(value) + for key, value in self.files.items() + } + @SchedulerMessagingPydanticRegistry.register() class GenerationRequest(StandardBaseModel): diff --git a/src/guidellm/data/preprocessors/formatters.py b/src/guidellm/data/preprocessors/formatters.py index c41ce936..02bb7398 100644 --- a/src/guidellm/data/preprocessors/formatters.py +++ b/src/guidellm/data/preprocessors/formatters.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any, Literal +from typing import Any from guidellm.data.objects import ( GenerationRequest, @@ -242,7 +242,7 @@ def __init__( def __call__( self, columns: dict[GenerativeDatasetColumnType, list[Any]] ) -> GenerationRequest: - arguments = {"json_body": {}} + arguments = {"json_body": {}, "files": {}} stats = {} # Add model @@ -251,6 +251,7 @@ def __call__( # Configure streaming if self.stream: + arguments["stream"] = True arguments["json_body"].update( {"stream": True, "stream_options": {"include_usage": True}} ) @@ -282,13 +283,11 @@ def __call__( ): arguments["json_body"]["prompt"] = "".join(prefix) + "".join(text) - return { - "request": { - "request_type": "audio_transcriptions", - "arguments": arguments, - "stats": stats, - } - } + return GenerationRequest( + request_type="audio_transcriptions", + arguments=GenerationRequestArguments(**arguments), + stats=stats, + ) @PreprocessorRegistry.register("audio_translations") @@ -297,7 +296,8 @@ class GenerativeAudioTranslationRequestFormatter( ): def __call__( self, columns: dict[GenerativeDatasetColumnType, list[Any]] - ) -> dict[Literal["request"], dict[Literal["request_type"], Any]]: + ) -> GenerationRequest: result = super().__call__(columns) - result["request"]["request_type"] = "audio_translations" + result.request_type = "audio_translations" + return result diff --git a/src/guidellm/data/preprocessors/mappers.py b/src/guidellm/data/preprocessors/mappers.py index 56ca0342..5e64b51c 100644 --- a/src/guidellm/data/preprocessors/mappers.py +++ b/src/guidellm/data/preprocessors/mappers.py @@ -1,7 +1,7 @@ from __future__ import annotations from collections import defaultdict -from typing import Any, ClassVar +from typing import Any, ClassVar, cast from datasets import Dataset, IterableDataset @@ -66,7 +66,7 @@ class GenerativeColumnMapper(DataDependentPreprocessor): @classmethod def datasets_default_mappings( cls, datasets: list[Dataset | IterableDataset] - ) -> dict[str, list[tuple[int, str]]]: + ) -> dict[GenerativeDatasetColumnType, list[tuple[int, str]]]: mappings: dict[GenerativeDatasetColumnType, list[tuple[int, str]]] = ( defaultdict(list) ) @@ -92,7 +92,8 @@ def datasets_default_mappings( for name in type_names: if name in dataset_columns: - mappings[column_type].append((index, name)) + key = cast("GenerativeDatasetColumnType", column_type) + mappings[key].append((index, name)) break return mappings @@ -123,20 +124,26 @@ def datasets_mappings( mappings[column_type] = [] for name in names if isinstance(names, list) else [names]: - dataset, column_name = name.split(".", 1) - dataset_index = ( - int(dataset) - if dataset.isdigit() - else datasets_named_indices.get(dataset) - ) + if "." in name: + dataset, column_name = name.split(".", 1) + dataset_index = ( + int(dataset) + if dataset.isdigit() + else datasets_named_indices.get(dataset) + ) + else: + dataset_index = 0 + column_name = name + if dataset_index is None or dataset_index >= len(datasets): raise ValueError( - f"Dataset '{dataset}' not found in datasets: " + f"Dataset '{name}' not found in datasets: " f"{datasets_named_indices}." ) if column_name not in datasets_columns[dataset_index]: raise ValueError( - f"Column '{column_name}' not found in dataset '{dataset}' " + f"Column '{column_name}' not found in dataset " + f"'{datasets[dataset_index]}' " f"columns: {datasets_columns[dataset_index]}." ) mappings[column_type].append((dataset_index, column_name)) @@ -153,11 +160,13 @@ def __init__( dict[GenerativeDatasetColumnType, list[tuple[int, str]]] | None ) - def __call__(self, row: dict[int, list[dict[str, Any]]]) -> dict[str, list[Any]]: + def __call__( + self, row: dict[str, Any] + ) -> dict[GenerativeDatasetColumnType, list[Any]]: if self.datasets_column_mappings is None: raise ValueError("DefaultGenerativeColumnMapper not setup with data.") - items = row.pop("items") + items = cast("dict[int, dict[str, Any]]", row.pop("items")) mapped: dict[GenerativeDatasetColumnType, list[Any]] = defaultdict(list) for column_type, column_mappings in self.datasets_column_mappings.items(): diff --git a/src/guidellm/data/utils/functions.py b/src/guidellm/data/utils/functions.py index c9ca20ed..413b5a92 100644 --- a/src/guidellm/data/utils/functions.py +++ b/src/guidellm/data/utils/functions.py @@ -12,6 +12,7 @@ import soundfile from PIL import Image as PILImage from pydub import AudioSegment +from torch import Tensor __all__ = [ "download_audio", @@ -182,7 +183,7 @@ def download_image(url: str) -> bytes: def encode_video( - video: bytes | str | Path | datasets.Video, + video: bytes | str | Path, encode_type: Literal["base64", "url"] | None = None, ) -> str: """ @@ -201,11 +202,13 @@ def encode_video( - video url - "data:video/{type};base64, {data}" string """ - url = is_url(video) - - if url and (encode_type is None or encode_type == "url"): + if ( + isinstance(video, str) + and is_url(video) + and (encode_type is None or encode_type == "url") + ): return video - elif url and encode_type == "base64": + elif isinstance(video, str) and is_url(video) and encode_type == "base64": raise ValueError(f"Cannot encode URL video {video}") return encode_video_base64(video=video) @@ -221,7 +224,7 @@ def encode_video_base64(video: bytes | str | Path) -> str: video_format = "unknown" - if is_url(video): + if isinstance(video, str) and is_url(video): video, video_format = download_video(video) if isinstance(video, (str, Path)): @@ -242,16 +245,18 @@ def download_video(url: str) -> tuple[bytes, str]: def encode_audio_as_dict( - audio: bytes | str | Path | dict | np.ndarray, - sample_rate: int | None = 16000, + audio: Any, + sample_rate: int = 16000, + encode_sample_rate: int = 16000, max_duration: float | None = None, mono: bool = True, audio_format: str = "mp3", bitrate: str = "64k", ) -> dict[Literal["data", "format"], Any]: - content, file_name, file_format = encode_audio( + content, _, file_format = encode_audio( audio=audio, - sample_rate=sample_rate or 16000, + sample_rate=sample_rate, + encode_sample_rate=encode_sample_rate, max_duration=max_duration, mono=mono, audio_format=audio_format, @@ -265,8 +270,9 @@ def encode_audio_as_dict( def encode_audio_as_file( - audio: bytes | str | Path | dict | np.ndarray, - sample_rate: int | None = 16000, + audio: Any, + sample_rate: int = 16000, + encode_sample_rate: int = 16000, max_duration: float | None = None, mono: bool = True, audio_format: str = "mp3", @@ -274,7 +280,8 @@ def encode_audio_as_file( ) -> tuple[str, bytes, str]: content, file_name, file_format = encode_audio( audio=audio, - sample_rate=sample_rate or 16000, + sample_rate=sample_rate, + encode_sample_rate=encode_sample_rate, max_duration=max_duration, mono=mono, audio_format=audio_format, @@ -284,57 +291,136 @@ def encode_audio_as_file( return file_name, content, f"audio/{file_format}" -def encode_audio( - audio: bytes | str | Path | dict, +def encode_audio( # noqa: PLR0912, PLR0911, C901 + audio: Any, sample_rate: int = 16000, + file_name: str = "audio.wav", + encode_sample_rate: int = 16000, max_duration: float | None = None, mono: bool = True, audio_format: str = "mp3", bitrate: str = "64k", ) -> tuple[bytes, str, str]: - file_name = "audio.wav" - - if is_url(audio): - audio, file_name, _ = download_audio(audio) - elif isinstance(audio, dict): - file_name = audio.get("name", "audio") - audio = base64.b64decode(audio["data"]) - elif isinstance(audio, (str, Path)): - path = Path(audio) - file_name = get_file_name(path) - audio = path.read_bytes() - elif not isinstance(audio, bytes): + audio_buffer: io.BytesIO = io.BytesIO() + + if hasattr(audio, "get_samples_played_in_range"): + # HF datasets Audio object + audio_samples = audio.get_samples_played_in_range( + start_seconds=0.0, + stop_seconds=None + if max_duration is None + else min(max_duration, audio.metadata.duration_seconds_from_header), + ) + return encode_audio( + audio=audio_samples.data.numpy(), + sample_rate=audio_samples.sample_rate, + encode_sample_rate=encode_sample_rate, + max_duration=max_duration, + mono=mono, + audio_format=audio_format, + bitrate=bitrate, + ) + + if isinstance(audio, Tensor): + return encode_audio( + audio=audio.numpy(), + sample_rate=sample_rate, + encode_sample_rate=encode_sample_rate, + max_duration=max_duration, + mono=mono, + audio_format=audio_format, + bitrate=bitrate, + ) + + if isinstance(audio, dict): + sample_rate = audio.get("sample_rate", audio.get("sampling_rate", sample_rate)) + if "data" not in audio and "url" not in audio: + raise ValueError( + f"Audio dict must contain either 'data' or 'url' keys, got {audio}" + ) + return encode_audio( + audio=audio.get("data") or audio.get("url"), + sample_rate=sample_rate, + encode_sample_rate=encode_sample_rate, + max_duration=max_duration, + mono=mono, + audio_format=audio_format, + bitrate=bitrate, + ) + + if isinstance(audio, str) and is_url(audio): + audio_bytes, file_name, _ = download_audio(audio) + return encode_audio( + audio=audio_bytes, + sample_rate=sample_rate, + encode_sample_rate=encode_sample_rate, + max_duration=max_duration, + mono=mono, + audio_format=audio_format, + bitrate=bitrate, + ) + + if isinstance(audio, (str, Path)): + if not Path(audio).exists(): + raise ValueError(f"Audio file does not exist: {audio}") + file_name = get_file_name(audio) + data, sample_rate = soundfile.read(str(audio), dtype="float32") + + return encode_audio( + audio=data, + sample_rate=sample_rate, + encode_sample_rate=encode_sample_rate, + max_duration=max_duration, + mono=mono, + audio_format=audio_format, + bitrate=bitrate, + ) + + if isinstance(audio, bytes): + data, sample_rate = soundfile.read(io.BytesIO(audio), dtype="float32") + + return encode_audio( + audio=data, + sample_rate=sample_rate, + encode_sample_rate=encode_sample_rate, + max_duration=max_duration, + mono=mono, + audio_format=audio_format, + bitrate=bitrate, + ) + + if not isinstance(audio, np.ndarray): raise ValueError(f"Unsupported audio type: {type(audio)}") - processed_audio, sample_rate = librosa.load( - io.BytesIO(audio), - sr=sample_rate, - mono=mono, - duration=max_duration, - ) + if sample_rate != encode_sample_rate: + audio = librosa.resample( + audio.astype(np.float32), orig_sr=sample_rate, target_sr=encode_sample_rate + ) + sample_rate = encode_sample_rate + + audio = librosa.to_mono(audio) + + if ( + max_duration is not None + and max_duration > 0 + and (max_samples := int(max_duration * sample_rate)) < len(audio) + ): + audio = audio[:max_samples] + + audio_buffer = io.BytesIO() - # Encode to target format - buffer = io.BytesIO() if audio_format.lower() == "mp3": - temp_wav = io.BytesIO() - soundfile.write( - temp_wav, - processed_audio, - sample_rate, - format="WAV", - subtype="PCM_16", - ) - temp_wav.seek(0) - AudioSegment.from_wav(temp_wav).export(buffer, format="mp3", bitrate=bitrate) + wav = io.BytesIO() + soundfile.write(wav, audio, sample_rate, format="WAV", subtype="PCM_16") + wav.seek(0) + + sound = AudioSegment.from_wav(wav) + sound.export(audio_buffer, format="mp3", bitrate=bitrate) else: - soundfile.write( - buffer, - processed_audio, - sample_rate, - format=audio_format.upper(), - ) + soundfile.write(audio_buffer, audio, sample_rate, format=audio_format.upper()) - return buffer.getvalue(), file_name, audio_format.lower() + audio_buffer.seek(0) + return audio_buffer.read(), file_name, audio_format.lower() def download_audio(url: str) -> tuple[bytes, str, str]: diff --git a/src/guidellm/scheduler/worker_group.py b/src/guidellm/scheduler/worker_group.py index 9baccd1b..278fb44d 100644 --- a/src/guidellm/scheduler/worker_group.py +++ b/src/guidellm/scheduler/worker_group.py @@ -557,7 +557,7 @@ def received_callback( # based on no more requests sent and all requests removed from queue if ( state_update.state.queued_requests == 0 - and self.send_requests_stopped_event.is_set() + and self.stop_send_requests_event.is_set() and not self.requests_generated_event.is_set() ): self.requests_generated_event.set() @@ -569,7 +569,7 @@ def received_callback( # Check if all requests have been processed and can shutdown if ( state_update.state.processed_requests == state_update.state.created_requests - and self.send_requests_stopped_event.is_set() + and self.stop_send_requests_event.is_set() and self.requests_generated_event.is_set() and self.constraint_reached_event.is_set() and not self.shutdown_event.is_set() diff --git a/src/guidellm/settings.py b/src/guidellm/settings.py index 5c360eff..222d85f9 100644 --- a/src/guidellm/settings.py +++ b/src/guidellm/settings.py @@ -46,7 +46,7 @@ class LoggingSettings(BaseModel): disabled: bool = False clear_loggers: bool = True - console_log_level: str = "WARNING" + console_log_level: str = "DEBUG" log_file: str | None = None log_file_level: str | None = None diff --git a/src/guidellm/utils/cli.py b/src/guidellm/utils/cli.py index 4d83526a..f049e94e 100644 --- a/src/guidellm/utils/cli.py +++ b/src/guidellm/utils/cli.py @@ -5,8 +5,10 @@ def parse_json(ctx, param, value): # noqa: ARG001 - if value is None: + if value is None or value == [None]: return None + if isinstance(value, (list, tuple)): + return [parse_json(ctx, param, val) for val in value] try: return json.loads(value) except json.JSONDecodeError as err: From 90a05ab363024df2805d2b11304de715b1a54ebd Mon Sep 17 00:00:00 2001 From: Samuel Monson Date: Thu, 9 Oct 2025 14:26:50 -0400 Subject: [PATCH 11/57] Fix for container rc tag (Attempt 2) (#398) ## Summary This is the same fix as #389 but applied to the RC workflow rather than the release workflow as was the original intent with #389. Both workflows need this change so not reverting the other one. --- - [x] "I certify that all code in this PR is my own, except as noted below." ## Use of AI - [ ] Includes AI-assisted code completion - [ ] Includes code generated by an AI application - [ ] Includes AI-generated tests (NOTE: AI written tests should have a docstring that includes `## WRITTEN BY AI ##`) Signed-off-by: Samuel Monson --- .github/workflows/release-candidate.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index 52425491..429f7a98 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -298,7 +298,10 @@ jobs: with: fetch-depth: 0 - name: Get version from branch - run: echo "PACKAGE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV + run: | + GITHUB_REF="${{ github.ref }}" + [[ -z "$GITHUB_REF" ]] && exit 1 # Fail if ref is unset + echo "PACKAGE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - name: Buildah build id: build-image uses: redhat-actions/buildah-build@v2 From 81af01bad4f0a9878c733b8553b328c7c5320e3b Mon Sep 17 00:00:00 2001 From: Samuel Monson Date: Thu, 9 Oct 2025 15:53:45 -0400 Subject: [PATCH 12/57] Fix the failing CI again (#400) ## Summary ## Details - [ ] ## Test Plan - ## Related Issues - Resolves # --- - [ ] "I certify that all code in this PR is my own, except as noted below." ## Use of AI - [ ] Includes AI-assisted code completion - [ ] Includes code generated by an AI application - [ ] Includes AI-generated tests (NOTE: AI written tests should have a docstring that includes `## WRITTEN BY AI ##`) Signed-off-by: Samuel Monson --- .github/workflows/release-candidate.yml | 10 ++++++---- .github/workflows/release.yml | 8 +++++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index 429f7a98..d63e12ad 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -299,9 +299,11 @@ jobs: fetch-depth: 0 - name: Get version from branch run: | - GITHUB_REF="${{ github.ref }}" - [[ -z "$GITHUB_REF" ]] && exit 1 # Fail if ref is unset - echo "PACKAGE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV + echo "package_version=${GITHUB_REF#refs/heads/release/}" >> $GITHUB_ENV + - name: Fail if version is unset + if: ${{ env.package_version == '' }} + run: | + exit 1 - name: Buildah build id: build-image uses: redhat-actions/buildah-build@v2 @@ -309,7 +311,7 @@ jobs: image: ${{ github.event.repository.name }} build-args: | GUIDELLM_BUILD_TYPE=candidate - tags: ${{ env.package_version }}~rc + tags: ${{ env.package_version= }}~rc containerfiles: | ./Containerfile - name: Push To ghcr.io diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6cc090cb..8938381b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -298,9 +298,11 @@ jobs: fetch-depth: 0 - name: Get version from branch run: | - GITHUB_REF="${{ github.ref }}" - [[ -z "$GITHUB_REF" ]] && exit 1 # Fail if ref is unset - echo "PACKAGE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV + echo "package_version=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV + - name: Fail if version is unset + if: ${{ env.package_version == '' }} + run: | + exit 1 - name: Buildah build id: build-image uses: redhat-actions/buildah-build@v2 From a24a22d010c7dd9dc47abc5ffa7b37579ff2ed74 Mon Sep 17 00:00:00 2001 From: Samuel Monson Date: Thu, 9 Oct 2025 15:57:19 -0400 Subject: [PATCH 13/57] Fix typo in CI (#401) ## Summary ## Details - [ ] ## Test Plan - ## Related Issues - Resolves # --- - [ ] "I certify that all code in this PR is my own, except as noted below." ## Use of AI - [ ] Includes AI-assisted code completion - [ ] Includes code generated by an AI application - [ ] Includes AI-generated tests (NOTE: AI written tests should have a docstring that includes `## WRITTEN BY AI ##`) Signed-off-by: Samuel Monson --- .github/workflows/release-candidate.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index d63e12ad..2b5e92cc 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -311,7 +311,7 @@ jobs: image: ${{ github.event.repository.name }} build-args: | GUIDELLM_BUILD_TYPE=candidate - tags: ${{ env.package_version= }}~rc + tags: ${{ env.package_version }}~rc containerfiles: | ./Containerfile - name: Push To ghcr.io From 87ba006c78d237012ebc253dceb8e4c7e20fc284 Mon Sep 17 00:00:00 2001 From: Jared O'Connell Date: Thu, 9 Oct 2025 18:11:19 -0400 Subject: [PATCH 14/57] Fixed quality errors Many of the quality errors are due to using the older union style, and have appeared due to the upgrade of the minimum Python version from 3.9 to 3.10 Signed-off-by: Jared O'Connell --- src/guidellm/backends/openai.py | 2 +- src/guidellm/benchmark/aggregator.py | 2 +- src/guidellm/benchmark/benchmarker.py | 4 +- src/guidellm/benchmark/output.py | 2 +- src/guidellm/benchmark/scenario.py | 2 +- src/guidellm/dataset/creator.py | 10 ++-- src/guidellm/dataset/file.py | 4 +- src/guidellm/dataset/hf_datasets.py | 6 +-- src/guidellm/scheduler/constraints.py | 20 ++++---- src/guidellm/scheduler/worker.py | 2 +- src/guidellm/utils/encoding.py | 72 +++++++++++++-------------- src/guidellm/utils/hf_datasets.py | 3 +- src/guidellm/utils/hf_transformers.py | 8 +-- src/guidellm/utils/messaging.py | 6 +-- src/guidellm/utils/mixins.py | 2 +- src/guidellm/utils/pydantic_utils.py | 3 +- src/guidellm/utils/random.py | 7 ++- src/guidellm/utils/registry.py | 5 +- src/guidellm/utils/statistics.py | 44 ++++++++-------- src/guidellm/utils/synchronous.py | 13 ++--- src/guidellm/utils/typing.py | 7 +-- 21 files changed, 108 insertions(+), 116 deletions(-) diff --git a/src/guidellm/backends/openai.py b/src/guidellm/backends/openai.py index ce83076f..fd14ee65 100644 --- a/src/guidellm/backends/openai.py +++ b/src/guidellm/backends/openai.py @@ -559,7 +559,7 @@ def _get_chat_messages( resolved_content.append(item) elif isinstance(item, str): resolved_content.append({"type": "text", "text": item}) - elif isinstance(item, (Image.Image, Path)): + elif isinstance(item, Image.Image | Path): resolved_content.append(self._get_chat_message_media_item(item)) else: raise ValueError(f"Unsupported content item type: {type(item)}") diff --git a/src/guidellm/benchmark/aggregator.py b/src/guidellm/benchmark/aggregator.py index e965c482..be70276b 100644 --- a/src/guidellm/benchmark/aggregator.py +++ b/src/guidellm/benchmark/aggregator.py @@ -267,7 +267,7 @@ def resolve( resolved = {} for key, val in aggregators.items(): - if isinstance(val, (Aggregator, CompilableAggregator)): + if isinstance(val, Aggregator | CompilableAggregator): resolved[key] = val else: aggregator_class = cls.get_registered_object(key) diff --git a/src/guidellm/benchmark/benchmarker.py b/src/guidellm/benchmark/benchmarker.py index 5f05065a..99410e4c 100644 --- a/src/guidellm/benchmark/benchmarker.py +++ b/src/guidellm/benchmark/benchmarker.py @@ -228,12 +228,12 @@ def _combine( existing: dict[str, Any] | StandardBaseDict, addition: dict[str, Any] | StandardBaseDict, ) -> dict[str, Any] | StandardBaseDict: - if not isinstance(existing, (dict, StandardBaseDict)): + if not isinstance(existing, dict | StandardBaseDict): raise ValueError( f"Existing value {existing} (type: {type(existing).__name__}) " f"is not a valid type for merging." ) - if not isinstance(addition, (dict, StandardBaseDict)): + if not isinstance(addition, dict | StandardBaseDict): raise ValueError( f"Addition value {addition} (type: {type(addition).__name__}) " f"is not a valid type for merging." diff --git a/src/guidellm/benchmark/output.py b/src/guidellm/benchmark/output.py index 56775dac..c4e8fb0f 100644 --- a/src/guidellm/benchmark/output.py +++ b/src/guidellm/benchmark/output.py @@ -90,7 +90,7 @@ def resolve( if not output_formats: return {} - if isinstance(output_formats, (list, tuple)): + if isinstance(output_formats, list | tuple): # support list of output keys: ["csv", "json"] # support list of files: ["path/to/file.json", "path/to/file.csv"] formats_list = output_formats diff --git a/src/guidellm/benchmark/scenario.py b/src/guidellm/benchmark/scenario.py index b53ef424..5299616f 100644 --- a/src/guidellm/benchmark/scenario.py +++ b/src/guidellm/benchmark/scenario.py @@ -38,7 +38,7 @@ def parse_float_list(value: str | float | list[float]) -> list[float]: or convert single float list of one or pass float list through. """ - if isinstance(value, (int, float)): + if isinstance(value, int | float): return [value] elif isinstance(value, list): return value diff --git a/src/guidellm/dataset/creator.py b/src/guidellm/dataset/creator.py index a74ec8c0..b95f4c50 100644 --- a/src/guidellm/dataset/creator.py +++ b/src/guidellm/dataset/creator.py @@ -95,10 +95,10 @@ def create( data, data_args, processor, processor_args, random_seed ) - if isinstance(dataset, (DatasetDict, IterableDatasetDict)): + if isinstance(dataset, DatasetDict | IterableDatasetDict): dataset = cls.extract_dataset_split(dataset, split, split_pref_order) - if not isinstance(dataset, (Dataset, IterableDataset)): + if not isinstance(dataset, Dataset | IterableDataset): raise ValueError( f"Unsupported data type: {type(dataset)} given for {dataset}." ) @@ -145,10 +145,10 @@ def extract_args_column_mappings( def extract_dataset_name( cls, dataset: Union[Dataset, IterableDataset, DatasetDict, IterableDatasetDict] ) -> Optional[str]: - if isinstance(dataset, (DatasetDict, IterableDatasetDict)): + if isinstance(dataset, DatasetDict | IterableDatasetDict): dataset = dataset[list(dataset.keys())[0]] - if isinstance(dataset, (Dataset, IterableDataset)): + if isinstance(dataset, Dataset | IterableDataset): if not hasattr(dataset, "info") or not hasattr( dataset.info, "dataset_name" ): @@ -165,7 +165,7 @@ def extract_dataset_split( specified_split: Union[Literal["auto"], str] = "auto", split_pref_order: Optional[Union[Literal["auto"], list[str]]] = "auto", ) -> Union[Dataset, IterableDataset]: - if not isinstance(dataset, (DatasetDict, IterableDatasetDict)): + if not isinstance(dataset, DatasetDict | IterableDatasetDict): raise ValueError( f"Unsupported data type: {type(dataset)} given for {dataset}." ) diff --git a/src/guidellm/dataset/file.py b/src/guidellm/dataset/file.py index 5d6df1d9..455ef580 100644 --- a/src/guidellm/dataset/file.py +++ b/src/guidellm/dataset/file.py @@ -31,7 +31,7 @@ class FileDatasetCreator(DatasetCreator): @classmethod def is_supported(cls, data: Any, data_args: Optional[dict[str, Any]]) -> bool: # noqa: ARG003 - if isinstance(data, (str, Path)) and (path := Path(data)).exists(): + if isinstance(data, str | Path) and (path := Path(data)).exists(): # local folder or py file, assume supported return path.suffix.lower() in cls.SUPPORTED_TYPES @@ -46,7 +46,7 @@ def handle_create( processor_args: Optional[dict[str, Any]], # noqa: ARG003 random_seed: int, # noqa: ARG003 ) -> Union[Dataset, DatasetDict, IterableDataset, IterableDatasetDict]: - if not isinstance(data, (str, Path)): + if not isinstance(data, str | Path): raise ValueError(f"Unsupported data type: {type(data)} given for {data}. ") path = Path(data) diff --git a/src/guidellm/dataset/hf_datasets.py b/src/guidellm/dataset/hf_datasets.py index 7f91facd..56c79936 100644 --- a/src/guidellm/dataset/hf_datasets.py +++ b/src/guidellm/dataset/hf_datasets.py @@ -25,11 +25,11 @@ def is_supported(cls, data: Any, data_args: Optional[dict[str, Any]]) -> bool: # base type is supported return True - if isinstance(data, (str, Path)) and (path := Path(data)).exists(): + if isinstance(data, str | Path) and (path := Path(data)).exists(): # local folder or py file, assume supported return path.is_dir() or path.suffix == ".py" - if isinstance(data, (str, Path)): + if isinstance(data, str | Path): try: # try to load dataset return get_dataset_config_info(data) is not None @@ -47,7 +47,7 @@ def handle_create( processor_args: Optional[dict[str, Any]], # noqa: ARG003 random_seed: int, # noqa: ARG003 ) -> Union[Dataset, DatasetDict, IterableDataset, IterableDatasetDict]: - if isinstance(data, (str, Path)): + if isinstance(data, str | Path): data = load_dataset(data, **(data_args or {})) elif data_args: raise ValueError( diff --git a/src/guidellm/scheduler/constraints.py b/src/guidellm/scheduler/constraints.py index c724a74a..c974225a 100644 --- a/src/guidellm/scheduler/constraints.py +++ b/src/guidellm/scheduler/constraints.py @@ -450,7 +450,7 @@ def __call__( current_index = max(0, self.current_index) max_num = ( self.max_num - if isinstance(self.max_num, (int, float)) + if isinstance(self.max_num, int | float) else self.max_num[min(current_index, len(self.max_num) - 1)] ) @@ -489,7 +489,7 @@ def _validate_max_num( raise ValueError( f"max_num must be set and truthful, received {value} ({val} failed)" ) - if not isinstance(val, (int, float)) or val <= 0: + if not isinstance(val, int | float) or val <= 0: raise ValueError( f"max_num must be a positive num, received {value} ({val} failed)" ) @@ -568,7 +568,7 @@ def __call__( current_index = max(0, self.current_index) max_duration = ( self.max_duration - if isinstance(self.max_duration, (int, float)) + if isinstance(self.max_duration, int | float) else self.max_duration[min(current_index, len(self.max_duration) - 1)] ) @@ -607,7 +607,7 @@ def _validate_max_duration( "max_duration must be set and truthful, " f"received {value} ({val} failed)" ) - if not isinstance(val, (int, float)) or val <= 0: + if not isinstance(val, int | float) or val <= 0: raise ValueError( "max_duration must be a positive num," f"received {value} ({val} failed)" @@ -682,7 +682,7 @@ def __call__( current_index = max(0, self.current_index) max_errors = ( self.max_errors - if isinstance(self.max_errors, (int, float)) + if isinstance(self.max_errors, int | float) else self.max_errors[min(current_index, len(self.max_errors) - 1)] ) errors_exceeded = state.errored_requests >= max_errors @@ -710,7 +710,7 @@ def _validate_max_errors( "max_errors must be set and truthful, " f"received {value} ({val} failed)" ) - if not isinstance(val, (int, float)) or val <= 0: + if not isinstance(val, int | float) or val <= 0: raise ValueError( f"max_errors must be a positive num,received {value} ({val} failed)" ) @@ -799,7 +799,7 @@ def __call__( current_index = max(0, self.current_index) max_error_rate = ( self.max_error_rate - if isinstance(self.max_error_rate, (int, float)) + if isinstance(self.max_error_rate, int | float) else self.max_error_rate[min(current_index, len(self.max_error_rate) - 1)] ) @@ -850,7 +850,7 @@ def _validate_max_error_rate( "max_error_rate must be set and truthful, " f"received {value} ({val} failed)" ) - if not isinstance(val, (int, float)) or val <= 0 or val >= 1: + if not isinstance(val, int | float) or val <= 0 or val >= 1: raise ValueError( "max_error_rate must be a number between 0 and 1," f"received {value} ({val} failed)" @@ -940,7 +940,7 @@ def __call__( current_index = max(0, self.current_index) max_error_rate = ( self.max_error_rate - if isinstance(self.max_error_rate, (int, float)) + if isinstance(self.max_error_rate, int | float) else self.max_error_rate[min(current_index, len(self.max_error_rate) - 1)] ) @@ -982,7 +982,7 @@ def _validate_max_error_rate( "max_error_rate must be set and truthful, " f"received {value} ({val} failed)" ) - if not isinstance(val, (int, float)) or val <= 0 or val >= 1: + if not isinstance(val, int | float) or val <= 0 or val >= 1: raise ValueError( "max_error_rate must be a number between 0 and 1," f"received {value} ({val} failed)" diff --git a/src/guidellm/scheduler/worker.py b/src/guidellm/scheduler/worker.py index 5f2fb74b..104ab418 100644 --- a/src/guidellm/scheduler/worker.py +++ b/src/guidellm/scheduler/worker.py @@ -310,7 +310,7 @@ async def _process_next_request(self): # Pull request from the queue request, request_info = await self.messaging.get() - if isinstance(request, (list, tuple)): + if isinstance(request, list | tuple): raise NotImplementedError("Multi-turn requests are not yet supported") # Calculate targeted start and set pending state for request diff --git a/src/guidellm/utils/encoding.py b/src/guidellm/utils/encoding.py index 6823fb77..916d6633 100644 --- a/src/guidellm/utils/encoding.py +++ b/src/guidellm/utils/encoding.py @@ -12,7 +12,7 @@ import json from collections.abc import Mapping -from typing import Annotated, Any, ClassVar, Generic, Literal, Optional, TypeVar, cast +from typing import Any, ClassVar, Generic, Literal, TypeVar, cast try: import msgpack # type: ignore[import-untyped] # Optional dependency @@ -45,7 +45,6 @@ HAS_ORJSON = False from pydantic import BaseModel -from typing_extensions import TypeAlias __all__ = [ "Encoder", @@ -60,14 +59,10 @@ ObjT = TypeVar("ObjT") MsgT = TypeVar("MsgT") -SerializationTypesAlias: TypeAlias = Annotated[ - Optional[Literal["dict", "sequence"]], - "Type alias for available serialization strategies", -] -EncodingTypesAlias: TypeAlias = Annotated[ - Optional[Literal["msgpack", "msgspec"]], - "Type alias for available binary encoding formats", -] +# Type alias for available serialization strategies +SerializationTypesAlias = Literal["dict", "sequence"] | None +# "Type alias for available binary encoding formats" +EncodingTypesAlias = Literal["msgpack", "msgspec"] class MessageEncoding(Generic[ObjT, MsgT]): @@ -405,7 +400,7 @@ def to_dict(self, obj: Any) -> Any: if isinstance(obj, BaseModel): return self.to_dict_pydantic(obj) - if isinstance(obj, (list, tuple)) and any( + if isinstance(obj, list | tuple) and any( isinstance(item, BaseModel) for item in obj ): return [ @@ -432,7 +427,7 @@ def from_dict(self, data: Any) -> Any: :param data: Dictionary representation possibly containing type metadata :return: Reconstructed object with proper types restored """ - if isinstance(data, (list, tuple)): + if isinstance(data, list | tuple): return [ self.from_dict_pydantic(item) if isinstance(item, dict) and "*PYD*" in item @@ -493,7 +488,7 @@ def to_sequence(self, obj: Any) -> str | Any: if isinstance(obj, BaseModel): payload_type = "pydantic" payload = self.to_sequence_pydantic(obj) - elif isinstance(obj, (list, tuple)) and any( + elif isinstance(obj, list | tuple) and any( isinstance(item, BaseModel) for item in obj ): payload_type = "collection_sequence" @@ -694,33 +689,36 @@ def pack_next_sequence( # noqa: C901, PLR0912 length=(payload_len.bit_length() + 7) // 8 if payload_len > 0 else 1, byteorder="big", ) - if type_ == "pydantic": - payload_type = b"P" - elif type_ == "python": - payload_type = b"p" - elif type_ == "collection_tuple": - payload_type = b"T" - elif type_ == "collection_sequence": - payload_type = b"S" - elif type_ == "collection_mapping": - payload_type = b"M" - else: - raise ValueError(f"Unknown type for packing: {type_}") + match type_: + case "pydantic": + payload_type = b"P" + case "python": + payload_type = b"p" + case "collection_tuple": + payload_type = b"T" + case "collection_sequence": + payload_type = b"S" + case "collection_mapping": + payload_type = b"M" + case _: + raise ValueError(f"Unknown type for packing: {type_}") delimiter = b"|" else: payload_len_output = str(payload_len) - if type_ == "pydantic": - payload_type = "P" - elif type_ == "python": - payload_type = "p" - elif type_ == "collection_tuple": - payload_type = "T" - elif type_ == "collection_sequence": - payload_type = "S" - elif type_ == "collection_mapping": - payload_type = "M" - else: - raise ValueError(f"Unknown type for packing: {type_}") + + match type_: + case "pydantic": + payload_type = "P" + case "python": + payload_type = "p" + case "collection_tuple": + payload_type = "T" + case "collection_sequence": + payload_type = "S" + case "collection_mapping": + payload_type = "M" + case _: + raise ValueError(f"Unknown type for packing: {type_}") delimiter = "|" # Type ignores because types are enforced at runtime diff --git a/src/guidellm/utils/hf_datasets.py b/src/guidellm/utils/hf_datasets.py index 73e55ebc..86f04485 100644 --- a/src/guidellm/utils/hf_datasets.py +++ b/src/guidellm/utils/hf_datasets.py @@ -1,5 +1,4 @@ from pathlib import Path -from typing import Union from datasets import Dataset @@ -11,7 +10,7 @@ } -def save_dataset_to_file(dataset: Dataset, output_path: Union[str, Path]) -> None: +def save_dataset_to_file(dataset: Dataset, output_path: str | Path) -> None: """ Saves a HuggingFace Dataset to file in a supported format. diff --git a/src/guidellm/utils/hf_transformers.py b/src/guidellm/utils/hf_transformers.py index 1f2aa1b5..636988c3 100644 --- a/src/guidellm/utils/hf_transformers.py +++ b/src/guidellm/utils/hf_transformers.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import Any, Optional, Union +from typing import Any from transformers import AutoTokenizer, PreTrainedTokenizerBase # type: ignore[import] @@ -9,15 +9,15 @@ def check_load_processor( - processor: Optional[Union[str, Path, PreTrainedTokenizerBase]], - processor_args: Optional[dict[str, Any]], + processor: str | Path | PreTrainedTokenizerBase | None, + processor_args: dict[str, Any] | None, error_msg: str, ) -> PreTrainedTokenizerBase: if processor is None: raise ValueError(f"Processor/Tokenizer is required for {error_msg}.") try: - if isinstance(processor, (str, Path)): + if isinstance(processor, str | Path): loaded = AutoTokenizer.from_pretrained( processor, **(processor_args or {}), diff --git a/src/guidellm/utils/messaging.py b/src/guidellm/utils/messaging.py index 9311259d..4dce576d 100644 --- a/src/guidellm/utils/messaging.py +++ b/src/guidellm/utils/messaging.py @@ -16,13 +16,13 @@ import threading import time from abc import ABC, abstractmethod -from collections.abc import Iterable +from collections.abc import Callable, Iterable from multiprocessing.connection import Connection from multiprocessing.context import BaseContext from multiprocessing.managers import SyncManager from multiprocessing.synchronize import Event as ProcessingEvent from threading import Event as ThreadingEvent -from typing import Any, Callable, Generic, Protocol, TypeVar, cast +from typing import Any, Generic, Protocol, TypeVar, cast import culsans from pydantic import BaseModel @@ -420,7 +420,7 @@ def _create_check_stop_callable( stop_events = tuple( item for item in stop_criteria or [] - if isinstance(item, (ThreadingEvent, ProcessingEvent)) + if isinstance(item, ThreadingEvent | ProcessingEvent) ) stop_callbacks = tuple(item for item in stop_criteria or [] if callable(item)) diff --git a/src/guidellm/utils/mixins.py b/src/guidellm/utils/mixins.py index b001ff2d..7cf28d00 100644 --- a/src/guidellm/utils/mixins.py +++ b/src/guidellm/utils/mixins.py @@ -91,7 +91,7 @@ def create_info_dict(cls, obj: Any) -> dict[str, Any]: "attributes": ( { key: val - if isinstance(val, (str, int, float, bool, list, dict)) + if isinstance(val, str | int | float | bool | list | dict) else repr(val) for key, val in obj.__dict__.items() if not key.startswith("_") diff --git a/src/guidellm/utils/pydantic_utils.py b/src/guidellm/utils/pydantic_utils.py index 55816ef1..05f5ad81 100644 --- a/src/guidellm/utils/pydantic_utils.py +++ b/src/guidellm/utils/pydantic_utils.py @@ -11,11 +11,10 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import Any, ClassVar, Generic, TypeVar, cast +from typing import Any, ClassVar, Generic, TypeVar, cast, get_args, get_origin from pydantic import BaseModel, ConfigDict, Field, GetCoreSchemaHandler from pydantic_core import CoreSchema, core_schema -from typing_extensions import get_args, get_origin from guidellm.utils.registry import RegistryMixin diff --git a/src/guidellm/utils/random.py b/src/guidellm/utils/random.py index ceef20b9..6c8f396d 100644 --- a/src/guidellm/utils/random.py +++ b/src/guidellm/utils/random.py @@ -1,6 +1,5 @@ import random from collections.abc import Iterator -from typing import Optional __all__ = ["IntegerRangeSampler"] @@ -9,9 +8,9 @@ class IntegerRangeSampler: def __init__( self, average: int, - variance: Optional[int], - min_value: Optional[int], - max_value: Optional[int], + variance: int | None, + min_value: int | None, + max_value: int | None, random_seed: int, ): self.average = average diff --git a/src/guidellm/utils/registry.py b/src/guidellm/utils/registry.py index e6f1b657..e4727cbd 100644 --- a/src/guidellm/utils/registry.py +++ b/src/guidellm/utils/registry.py @@ -10,7 +10,8 @@ from __future__ import annotations -from typing import Any, Callable, ClassVar, Generic, TypeVar, cast +from collections.abc import Callable +from typing import Any, ClassVar, Generic, TypeVar, cast from guidellm.utils.auto_importer import AutoImporterMixin @@ -103,7 +104,7 @@ def register_decorator( if name is None: name = obj.__name__ - elif not isinstance(name, (str, list)): + elif not isinstance(name, str | list): raise ValueError( "RegistryMixin.register_decorator name must be a string or " f"an iterable of strings. Got {name}." diff --git a/src/guidellm/utils/statistics.py b/src/guidellm/utils/statistics.py index acd9d4f1..04484c2c 100644 --- a/src/guidellm/utils/statistics.py +++ b/src/guidellm/utils/statistics.py @@ -149,7 +149,7 @@ def from_distribution_function( in the output :return: DistributionSummary instance with calculated statistical metrics """ - values, weights = zip(*distribution) if distribution else ([], []) + values, weights = zip(*distribution, strict=True) if distribution else ([], []) values = np.array(values) # type: ignore[assignment] weights = np.array(weights) # type: ignore[assignment] @@ -247,7 +247,7 @@ def from_values( ) return DistributionSummary.from_distribution_function( - distribution=list(zip(values, weights)), + distribution=list(zip(values, weights, strict=True)), include_cdf=include_cdf, ) @@ -389,7 +389,8 @@ def from_iterable_request_times( events[global_end] = 0 for (_, end), first_iter, first_iter_count, total_count in zip( - requests, first_iter_times, first_iter_counts, iter_counts + requests, first_iter_times, first_iter_counts, iter_counts, + strict=True ): events[first_iter] += first_iter_count @@ -499,36 +500,36 @@ def from_values( ) _, successful_values, successful_weights = ( - zip(*successful) + zip(*successful, strict=True) if ( successful := list( filter( lambda val: val[0] == "successful", - zip(value_types, values, weights), + zip(value_types, values, weights, strict=True), ) ) ) else ([], [], []) ) _, incomplete_values, incomplete_weights = ( - zip(*incomplete) + zip(*incomplete, strict=True) if ( incomplete := list( filter( lambda val: val[0] == "incomplete", - zip(value_types, values, weights), + zip(value_types, values, weights, strict=True), ) ) ) else ([], [], []) ) _, errored_values, errored_weights = ( - zip(*errored) + zip(*errored, strict=True) if ( errored := list( filter( lambda val: val[0] == "error", - zip(value_types, values, weights), + zip(value_types, values, weights, strict=True), ) ) ) @@ -604,36 +605,36 @@ def from_request_times( ) _, successful_requests = ( - zip(*successful) + zip(*successful, strict=True) if ( successful := list( filter( lambda val: val[0] == "successful", - zip(request_types, requests), + zip(request_types, requests, strict=True), ) ) ) else ([], []) ) _, incomplete_requests = ( - zip(*incomplete) + zip(*incomplete, strict=True) if ( incomplete := list( filter( lambda val: val[0] == "incomplete", - zip(request_types, requests), + zip(request_types, requests, strict=True), ) ) ) else ([], []) ) _, errored_requests = ( - zip(*errored) + zip(*errored, strict=True) if ( errored := list( filter( lambda val: val[0] == "error", - zip(request_types, requests), + zip(request_types, requests, strict=True), ) ) ) @@ -734,7 +735,7 @@ def from_iterable_request_times( successful_iter_counts, successful_first_iter_counts, ) = ( - zip(*successful) + zip(*successful, strict=True) if ( successful := list( filter( @@ -745,6 +746,7 @@ def from_iterable_request_times( first_iter_times, iter_counts, first_iter_counts, + strict=True, ), ) ) @@ -758,7 +760,7 @@ def from_iterable_request_times( incomplete_iter_counts, incomplete_first_iter_counts, ) = ( - zip(*incomplete) + zip(*incomplete, strict=True) if ( incomplete := list( filter( @@ -769,6 +771,7 @@ def from_iterable_request_times( first_iter_times, iter_counts, first_iter_counts, + strict=True, ), ) ) @@ -782,7 +785,7 @@ def from_iterable_request_times( errored_iter_counts, errored_first_iter_counts, ) = ( - zip(*errored) + zip(*errored, strict=True) if ( errored := list( filter( @@ -793,6 +796,7 @@ def from_iterable_request_times( first_iter_times, iter_counts, first_iter_counts, + strict=True, ), ) ) @@ -904,7 +908,7 @@ def __add__(self, value: Any) -> float: :return: Updated mean after adding the value :raises ValueError: If value is not numeric (int or float) """ - if not isinstance(value, (int, float)): + if not isinstance(value, int | float): raise ValueError( f"Value must be an int or float, got {type(value)} instead.", ) @@ -921,7 +925,7 @@ def __iadd__(self, value: Any) -> RunningStats: :return: Self reference for method chaining :raises ValueError: If value is not numeric (int or float) """ - if not isinstance(value, (int, float)): + if not isinstance(value, int | float): raise ValueError( f"Value must be an int or float, got {type(value)} instead.", ) diff --git a/src/guidellm/utils/synchronous.py b/src/guidellm/utils/synchronous.py index 64c14e94..d37daec2 100644 --- a/src/guidellm/utils/synchronous.py +++ b/src/guidellm/utils/synchronous.py @@ -16,9 +16,6 @@ from multiprocessing.synchronize import Event as ProcessingEvent from threading import Barrier as ThreadingBarrier from threading import Event as ThreadingEvent -from typing import Annotated, Union - -from typing_extensions import TypeAlias __all__ = [ "SyncObjectTypesAlias", @@ -28,10 +25,10 @@ ] -SyncObjectTypesAlias: TypeAlias = Annotated[ - Union[ThreadingEvent, ProcessingEvent, ThreadingBarrier, ProcessingBarrier], - "Type alias for threading and multiprocessing synchronization object types", -] +# Type alias for threading and multiprocessing synchronization object types +SyncObjectTypesAlias = ( + ThreadingEvent | ProcessingEvent | ThreadingBarrier | ProcessingBarrier +) async def wait_for_sync_event( @@ -146,7 +143,7 @@ async def wait_for_sync_objects( tasks = [ asyncio.create_task( wait_for_sync_barrier(obj, poll_interval) - if isinstance(obj, (ThreadingBarrier, ProcessingBarrier)) + if isinstance(obj, ThreadingBarrier | ProcessingBarrier) else wait_for_sync_event(obj, poll_interval) ) for obj in objects diff --git a/src/guidellm/utils/typing.py b/src/guidellm/utils/typing.py index 8146ea1e..8d3580ef 100644 --- a/src/guidellm/utils/typing.py +++ b/src/guidellm/utils/typing.py @@ -1,14 +1,9 @@ from __future__ import annotations from collections.abc import Iterator +from types import UnionType from typing import Annotated, Literal, Union, get_args, get_origin -# Backwards compatibility for Python <3.10 -try: - from types import UnionType # type: ignore[attr-defined] -except ImportError: - UnionType = Union - # Backwards compatibility for Python <3.12 try: from typing import TypeAliasType # type: ignore[attr-defined] From 1bd8846a10b58b1b3fdce55925335621e32d0c00 Mon Sep 17 00:00:00 2001 From: Jared O'Connell Date: Thu, 9 Oct 2025 18:14:33 -0400 Subject: [PATCH 15/57] Run auto-formatter Signed-off-by: Jared O'Connell --- setup.py | 7 +- src/guidellm/backends/objects.py | 28 +++---- src/guidellm/backends/openai.py | 74 +++++++++---------- src/guidellm/benchmark/aggregator.py | 22 +++--- src/guidellm/benchmark/output.py | 16 ++-- src/guidellm/benchmark/profile.py | 4 +- src/guidellm/benchmark/scenario.py | 3 +- src/guidellm/dataset/creator.py | 38 +++++----- src/guidellm/dataset/entrypoints.py | 12 +-- src/guidellm/dataset/file.py | 16 ++-- src/guidellm/dataset/hf_datasets.py | 12 +-- src/guidellm/dataset/in_memory.py | 12 +-- src/guidellm/dataset/synthetic.py | 38 +++++----- src/guidellm/logger.py | 2 +- src/guidellm/preprocess/dataset.py | 44 +++++------ src/guidellm/presentation/data_models.py | 16 ++-- src/guidellm/presentation/injector.py | 3 +- src/guidellm/request/loader.py | 30 +++----- src/guidellm/request/request.py | 4 +- src/guidellm/scheduler/objects.py | 6 +- src/guidellm/utils/statistics.py | 3 +- tests/integration/scheduler/test_scheduler.py | 2 +- tests/unit/benchmark/test_output.py | 9 ++- tests/unit/dataset/test_synthetic.py | 2 +- tests/unit/mock_backend.py | 12 +-- tests/unit/mock_benchmark.py | 1 + tests/unit/utils/test_encoding.py | 2 +- tests/unit/utils/test_typing.py | 5 +- 28 files changed, 203 insertions(+), 220 deletions(-) diff --git a/setup.py b/setup.py index 623bad28..d3b92889 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,6 @@ import os import re from pathlib import Path -from typing import Optional, Union from packaging.version import Version from setuptools import setup @@ -11,7 +10,7 @@ TAG_VERSION_PATTERN = re.compile(r"^v(\d+\.\d+\.\d+)$") -def get_last_version_diff() -> tuple[Version, Optional[str], Optional[int]]: +def get_last_version_diff() -> tuple[Version, str | None, int | None]: """ Get the last version, last tag, and the number of commits since the last tag. If no tags are found, return the last release version and None for the tag/commits. @@ -38,8 +37,8 @@ def get_last_version_diff() -> tuple[Version, Optional[str], Optional[int]]: def get_next_version( - build_type: str, build_iteration: Optional[Union[str, int]] -) -> tuple[Version, Optional[str], int]: + build_type: str, build_iteration: str | int | None +) -> tuple[Version, str | None, int]: """ Get the next version based on the build type and iteration. - build_type == release: take the last version and add a post if build iteration diff --git a/src/guidellm/backends/objects.py b/src/guidellm/backends/objects.py index 05280940..001aeb70 100644 --- a/src/guidellm/backends/objects.py +++ b/src/guidellm/backends/objects.py @@ -7,7 +7,7 @@ """ import uuid -from typing import Any, Literal, Optional +from typing import Any, Literal from pydantic import Field @@ -73,32 +73,32 @@ class GenerationResponse(StandardBaseModel): request_args: dict[str, Any] = Field( description="Arguments passed to the backend for this request." ) - value: Optional[str] = Field( + value: str | None = Field( default=None, description="Complete generated text content. None for streaming responses.", ) - delta: Optional[str] = Field( + delta: str | None = Field( default=None, description="Incremental text content for streaming responses." ) iterations: int = Field( default=0, description="Number of generation iterations completed." ) - request_prompt_tokens: Optional[int] = Field( + request_prompt_tokens: int | None = Field( default=None, description="Token count from the original request prompt." ) - request_output_tokens: Optional[int] = Field( + request_output_tokens: int | None = Field( default=None, description="Expected output token count from the original request.", ) - response_prompt_tokens: Optional[int] = Field( + response_prompt_tokens: int | None = Field( default=None, description="Actual prompt token count reported by the backend." ) - response_output_tokens: Optional[int] = Field( + response_output_tokens: int | None = Field( default=None, description="Actual output token count reported by the backend." ) @property - def prompt_tokens(self) -> Optional[int]: + def prompt_tokens(self) -> int | None: """ :return: The number of prompt tokens used in the request (response_prompt_tokens if available, otherwise request_prompt_tokens). @@ -106,7 +106,7 @@ def prompt_tokens(self) -> Optional[int]: return self.response_prompt_tokens or self.request_prompt_tokens @property - def output_tokens(self) -> Optional[int]: + def output_tokens(self) -> int | None: """ :return: The number of output tokens generated in the response (response_output_tokens if available, otherwise request_output_tokens). @@ -114,7 +114,7 @@ def output_tokens(self) -> Optional[int]: return self.response_output_tokens or self.request_output_tokens @property - def total_tokens(self) -> Optional[int]: + def total_tokens(self) -> int | None: """ :return: The total number of tokens used in the request and response. Sum of prompt_tokens and output_tokens. @@ -125,7 +125,7 @@ def total_tokens(self) -> Optional[int]: def preferred_prompt_tokens( self, preferred_source: Literal["request", "response"] - ) -> Optional[int]: + ) -> int | None: if preferred_source == "request": return self.request_prompt_tokens or self.response_prompt_tokens else: @@ -133,7 +133,7 @@ def preferred_prompt_tokens( def preferred_output_tokens( self, preferred_source: Literal["request", "response"] - ) -> Optional[int]: + ) -> int | None: if preferred_source == "request": return self.request_output_tokens or self.response_output_tokens else: @@ -146,11 +146,11 @@ class GenerationRequestTimings(MeasuredRequestTimings): """Timing model for tracking generation request lifecycle events.""" timings_type: Literal["generation_request_timings"] = "generation_request_timings" - first_iteration: Optional[float] = Field( + first_iteration: float | None = Field( default=None, description="Unix timestamp when the first generation iteration began.", ) - last_iteration: Optional[float] = Field( + last_iteration: float | None = Field( default=None, description="Unix timestamp when the last generation iteration completed.", ) diff --git a/src/guidellm/backends/openai.py b/src/guidellm/backends/openai.py index fd14ee65..fd539063 100644 --- a/src/guidellm/backends/openai.py +++ b/src/guidellm/backends/openai.py @@ -17,7 +17,7 @@ import time from collections.abc import AsyncIterator from pathlib import Path -from typing import Any, ClassVar, Optional, Union +from typing import Any, ClassVar import httpx from PIL import Image @@ -38,8 +38,8 @@ class UsageStats: """Token usage statistics for generation requests.""" - prompt_tokens: Optional[int] = None - output_tokens: Optional[int] = None + prompt_tokens: int | None = None + output_tokens: int | None = None @Backend.register("openai_http") @@ -78,19 +78,19 @@ class OpenAIHTTPBackend(Backend): def __init__( self, target: str, - model: Optional[str] = None, - api_key: Optional[str] = None, - organization: Optional[str] = None, - project: Optional[str] = None, + model: str | None = None, + api_key: str | None = None, + organization: str | None = None, + project: str | None = None, timeout: float = 60.0, http2: bool = True, follow_redirects: bool = True, - max_output_tokens: Optional[int] = None, + max_output_tokens: int | None = None, stream_response: bool = True, - extra_query: Optional[dict] = None, - extra_body: Optional[dict] = None, - remove_from_body: Optional[list[str]] = None, - headers: Optional[dict] = None, + extra_query: dict | None = None, + extra_body: dict | None = None, + remove_from_body: list[str] | None = None, + headers: dict | None = None, verify: bool = False, ): """ @@ -137,7 +137,7 @@ def __init__( # Runtime state self._in_process = False - self._async_client: Optional[httpx.AsyncClient] = None + self._async_client: httpx.AsyncClient | None = None @property def info(self) -> dict[str, Any]: @@ -264,7 +264,7 @@ async def available_models(self) -> list[str]: return [item["id"] for item in response.json()["data"]] - async def default_model(self) -> Optional[str]: + async def default_model(self) -> str | None: """ Get the default model for this backend. @@ -280,7 +280,7 @@ async def resolve( self, request: GenerationRequest, request_info: ScheduledRequestInfo, - history: Optional[list[tuple[GenerationRequest, GenerationResponse]]] = None, + history: list[tuple[GenerationRequest, GenerationResponse]] | None = None, ) -> AsyncIterator[tuple[GenerationResponse, ScheduledRequestInfo]]: """ Process a generation request and yield progressive responses. @@ -363,12 +363,12 @@ async def resolve( async def text_completions( self, - prompt: Union[str, list[str]], - request_id: Optional[str], # noqa: ARG002 - output_token_count: Optional[int] = None, + prompt: str | list[str], + request_id: str | None, # noqa: ARG002 + output_token_count: int | None = None, stream_response: bool = True, **kwargs, - ) -> AsyncIterator[tuple[Optional[str], Optional[UsageStats]]]: + ) -> AsyncIterator[tuple[str | None, UsageStats | None]]: """ Generate text completions using the /v1/completions endpoint. @@ -431,17 +431,13 @@ async def text_completions( async def chat_completions( self, - content: Union[ - str, - list[Union[str, dict[str, Union[str, dict[str, str]]], Path, Image.Image]], - Any, - ], - request_id: Optional[str] = None, # noqa: ARG002 - output_token_count: Optional[int] = None, + content: str | list[str | dict[str, str | dict[str, str]] | Path | Image.Image] | Any, + request_id: str | None = None, # noqa: ARG002 + output_token_count: int | None = None, raw_content: bool = False, stream_response: bool = True, **kwargs, - ) -> AsyncIterator[tuple[Optional[str], Optional[UsageStats]]]: + ) -> AsyncIterator[tuple[str | None, UsageStats | None]]: """ Generate chat completions using the /v1/chat/completions endpoint. @@ -502,10 +498,10 @@ async def chat_completions( def _build_headers( self, - api_key: Optional[str], - organization: Optional[str], - project: Optional[str], - user_headers: Optional[dict], + api_key: str | None, + organization: str | None, + project: str | None, + user_headers: dict | None, ) -> dict[str, str]: headers = {} @@ -541,11 +537,7 @@ def _get_params(self, endpoint_type: str) -> dict[str, str]: def _get_chat_messages( self, - content: Union[ - str, - list[Union[str, dict[str, Union[str, dict[str, str]]], Path, Image.Image]], - Any, - ], + content: str | list[str | dict[str, str | dict[str, str]] | Path | Image.Image] | Any, ) -> list[dict[str, Any]]: if isinstance(content, str): return [{"role": "user", "content": content}] @@ -567,7 +559,7 @@ def _get_chat_messages( return [{"role": "user", "content": resolved_content}] def _get_chat_message_media_item( - self, item: Union[Path, Image.Image] + self, item: Path | Image.Image ) -> dict[str, Any]: if isinstance(item, Image.Image): encoded = base64.b64encode(item.tobytes()).decode("utf-8") @@ -597,8 +589,8 @@ def _get_chat_message_media_item( def _get_body( self, endpoint_type: str, - request_kwargs: Optional[dict[str, Any]], - max_output_tokens: Optional[int] = None, + request_kwargs: dict[str, Any] | None, + max_output_tokens: int | None = None, **kwargs, ) -> dict[str, Any]: # Start with endpoint-specific extra body parameters @@ -628,7 +620,7 @@ def _get_body( return {key: val for key, val in body.items() if val is not None} - def _get_completions_text_content(self, data: dict) -> Optional[str]: + def _get_completions_text_content(self, data: dict) -> str | None: if not data.get("choices"): return None @@ -639,7 +631,7 @@ def _get_completions_text_content(self, data: dict) -> Optional[str]: or choice.get("message", {}).get("content") ) - def _get_completions_usage_stats(self, data: dict) -> Optional[UsageStats]: + def _get_completions_usage_stats(self, data: dict) -> UsageStats | None: if not data.get("usage"): return None diff --git a/src/guidellm/benchmark/aggregator.py b/src/guidellm/benchmark/aggregator.py index be70276b..b33a7b14 100644 --- a/src/guidellm/benchmark/aggregator.py +++ b/src/guidellm/benchmark/aggregator.py @@ -975,7 +975,7 @@ def _calculate_requests_per_second( filtered_statuses = [] filtered_times = [] - for status, request in zip(statuses, requests): + for status, request in zip(statuses, requests, strict=False): if not all_defined( safe_getattr(request.scheduler_info.request_timings, "request_start"), safe_getattr(request.scheduler_info.request_timings, "request_end"), @@ -1005,7 +1005,7 @@ def _calculate_request_concurrency( filtered_statuses = [] filtered_times = [] - for status, request in zip(statuses, requests): + for status, request in zip(statuses, requests, strict=False): if not all_defined( safe_getattr(request.scheduler_info.request_timings, "request_start"), safe_getattr(request.scheduler_info.request_timings, "request_end"), @@ -1035,7 +1035,7 @@ def _calculate_request_latency( filtered_statuses = [] filtered_values = [] - for status, request in zip(statuses, requests): + for status, request in zip(statuses, requests, strict=False): if not all_defined(request.request_latency): continue @@ -1056,7 +1056,7 @@ def _calculate_prompt_token_count( filtered_statuses = [] filtered_values = [] - for status, request in zip(statuses, requests): + for status, request in zip(statuses, requests, strict=False): if not all_defined(request.prompt_tokens): continue @@ -1077,7 +1077,7 @@ def _calculate_output_token_count( filtered_statuses = [] filtered_values = [] - for status, request in zip(statuses, requests): + for status, request in zip(statuses, requests, strict=False): if not all_defined(request.output_tokens): continue @@ -1098,7 +1098,7 @@ def _calculate_total_token_count( filtered_statuses = [] filtered_values = [] - for status, request in zip(statuses, requests): + for status, request in zip(statuses, requests, strict=False): if not all_defined(request.total_tokens): continue @@ -1119,7 +1119,7 @@ def _calculate_time_to_first_token_ms( filtered_statuses = [] filtered_values = [] - for status, request in zip(statuses, requests): + for status, request in zip(statuses, requests, strict=False): if not all_defined(request.time_to_first_token_ms): continue @@ -1141,7 +1141,7 @@ def _calculate_time_per_output_token_ms( filtered_values = [] filtered_weights = [] - for status, request in zip(statuses, requests): + for status, request in zip(statuses, requests, strict=False): if not all_defined(request.time_to_first_token_ms): continue @@ -1174,7 +1174,7 @@ def _calculate_inter_token_latency_ms( filtered_values = [] filtered_weights = [] - for status, request in zip(statuses, requests): + for status, request in zip(statuses, requests, strict=False): if not all_defined(request.inter_token_latency_ms): continue @@ -1199,7 +1199,7 @@ def _calculate_output_tokens_per_second( filtered_first_iter_times = [] filtered_iter_counts = [] - for status, request in zip(statuses, requests): + for status, request in zip(statuses, requests, strict=False): if not all_defined(request.output_tokens_per_second): continue @@ -1234,7 +1234,7 @@ def _calculate_tokens_per_second( filtered_iter_counts = [] filtered_first_iter_counts = [] - for status, request in zip(statuses, requests): + for status, request in zip(statuses, requests, strict=False): if not all_defined(request.tokens_per_second): continue diff --git a/src/guidellm/benchmark/output.py b/src/guidellm/benchmark/output.py index c4e8fb0f..cacadc94 100644 --- a/src/guidellm/benchmark/output.py +++ b/src/guidellm/benchmark/output.py @@ -34,10 +34,11 @@ DistributionSummary, RegistryMixin, StatusDistributionSummary, + camelize_str, + recursive_key_update, safe_format_timestamp, split_text_list_by_length, ) -from guidellm.utils import recursive_key_update, camelize_str __all__ = [ "GenerativeBenchmarkerCSV", @@ -369,7 +370,7 @@ def _print_line( f"Value and style length mismatch: {len(value)} vs {len(style)}" ) - for val, sty in zip(value, style): + for val, sty in zip(value, style, strict=False): text.append(val, style=sty) self.console.print(Padding.indent(text, indent)) @@ -568,8 +569,8 @@ async def finalize(self, report: GenerativeBenchmarksReport) -> Path: benchmark_values: list[str | float | list[float]] = [] # Add basic run description info - desc_headers, desc_values = ( - self._get_benchmark_desc_headers_and_values(benchmark) + desc_headers, desc_values = self._get_benchmark_desc_headers_and_values( + benchmark ) benchmark_headers.extend(desc_headers) benchmark_values.extend(desc_values) @@ -680,7 +681,8 @@ def _get_benchmark_status_metrics_stats( return headers, values def _get_benchmark_extras_headers_and_values( - self, benchmark: GenerativeBenchmark, + self, + benchmark: GenerativeBenchmark, ) -> tuple[list[str], list[str]]: headers = ["Profile", "Backend", "Generator Data"] values: list[str] = [ @@ -733,9 +735,7 @@ async def finalize(self, report: GenerativeBenchmarksReport) -> Path: ui_api_data = {} for k, v in camel_data.items(): placeholder_key = f"window.{k} = {{}};" - replacement_value = ( - f"window.{k} = {json.dumps(v, indent=2)};\n" - ) + replacement_value = f"window.{k} = {json.dumps(v, indent=2)};\n" ui_api_data[placeholder_key] = replacement_value create_report(ui_api_data, output_path) diff --git a/src/guidellm/benchmark/profile.py b/src/guidellm/benchmark/profile.py index 3ff8d0e0..ec4fa839 100644 --- a/src/guidellm/benchmark/profile.py +++ b/src/guidellm/benchmark/profile.py @@ -679,7 +679,9 @@ def next_strategy( prev_benchmark.metrics.requests_per_second.successful.mean ) if self.synchronous_rate <= 0 and self.throughput_rate <= 0: - raise RuntimeError("Invalid rates in sweep; aborting. Were there any successful requests?") + raise RuntimeError( + "Invalid rates in sweep; aborting. Were there any successful requests?" + ) self.measured_rates = list( np.linspace( self.synchronous_rate, diff --git a/src/guidellm/benchmark/scenario.py b/src/guidellm/benchmark/scenario.py index 5299616f..73a9a050 100644 --- a/src/guidellm/benchmark/scenario.py +++ b/src/guidellm/benchmark/scenario.py @@ -1,10 +1,11 @@ from __future__ import annotations import json +from collections.abc import Callable from functools import cache, wraps from inspect import Parameter, signature from pathlib import Path -from typing import Annotated, Any, Callable, Literal, TypeVar +from typing import Annotated, Any, Literal, TypeVar import yaml from loguru import logger diff --git a/src/guidellm/dataset/creator.py b/src/guidellm/dataset/creator.py index b95f4c50..fe712c23 100644 --- a/src/guidellm/dataset/creator.py +++ b/src/guidellm/dataset/creator.py @@ -1,6 +1,6 @@ from abc import ABC, abstractmethod from pathlib import Path -from typing import Any, Literal, Optional, Union +from typing import Any, Literal from datasets import Dataset, DatasetDict, IterableDataset, IterableDatasetDict from transformers import PreTrainedTokenizerBase # type: ignore[import] @@ -80,12 +80,12 @@ class DatasetCreator(ABC): def create( cls, data: Any, - data_args: Optional[dict[str, Any]], - processor: Optional[Union[str, Path, PreTrainedTokenizerBase]], - processor_args: Optional[dict[str, Any]], + data_args: dict[str, Any] | None, + processor: str | Path | PreTrainedTokenizerBase | None, + processor_args: dict[str, Any] | None, random_seed: int = 42, - split_pref_order: Optional[list[str]] = None, - ) -> tuple[Union[Dataset, IterableDataset], dict[ColumnInputTypes, str]]: + split_pref_order: list[str] | None = None, + ) -> tuple[Dataset | IterableDataset, dict[ColumnInputTypes, str]]: if not cls.is_supported(data, data_args): raise ValueError(f"Unsupported data type: {type(data)} given for {data}. ") @@ -106,7 +106,7 @@ def create( return dataset, column_mappings @classmethod - def extract_args_split(cls, data_args: Optional[dict[str, Any]]) -> str: + def extract_args_split(cls, data_args: dict[str, Any] | None) -> str: split = "auto" if data_args and "split" in data_args: @@ -118,7 +118,7 @@ def extract_args_split(cls, data_args: Optional[dict[str, Any]]) -> str: @classmethod def extract_args_column_mappings( cls, - data_args: Optional[dict[str, Any]], + data_args: dict[str, Any] | None, ) -> dict[ColumnInputTypes, str]: columns: dict[ColumnInputTypes, str] = {} @@ -143,8 +143,8 @@ def extract_args_column_mappings( @classmethod def extract_dataset_name( - cls, dataset: Union[Dataset, IterableDataset, DatasetDict, IterableDatasetDict] - ) -> Optional[str]: + cls, dataset: Dataset | IterableDataset | DatasetDict | IterableDatasetDict + ) -> str | None: if isinstance(dataset, DatasetDict | IterableDatasetDict): dataset = dataset[list(dataset.keys())[0]] @@ -161,10 +161,10 @@ def extract_dataset_name( @classmethod def extract_dataset_split( cls, - dataset: Union[DatasetDict, IterableDatasetDict], - specified_split: Union[Literal["auto"], str] = "auto", - split_pref_order: Optional[Union[Literal["auto"], list[str]]] = "auto", - ) -> Union[Dataset, IterableDataset]: + dataset: DatasetDict | IterableDatasetDict, + specified_split: Literal["auto"] | str = "auto", + split_pref_order: Literal["auto"] | list[str] | None = "auto", + ) -> Dataset | IterableDataset: if not isinstance(dataset, DatasetDict | IterableDatasetDict): raise ValueError( f"Unsupported data type: {type(dataset)} given for {dataset}." @@ -199,15 +199,15 @@ def extract_dataset_split( @classmethod @abstractmethod - def is_supported(cls, data: Any, data_args: Optional[dict[str, Any]]) -> bool: ... + def is_supported(cls, data: Any, data_args: dict[str, Any] | None) -> bool: ... @classmethod @abstractmethod def handle_create( cls, data: Any, - data_args: Optional[dict[str, Any]], - processor: Optional[Union[str, Path, PreTrainedTokenizerBase]], - processor_args: Optional[dict[str, Any]], + data_args: dict[str, Any] | None, + processor: str | Path | PreTrainedTokenizerBase | None, + processor_args: dict[str, Any] | None, random_seed: int, - ) -> Union[Dataset, DatasetDict, IterableDataset, IterableDatasetDict]: ... + ) -> Dataset | DatasetDict | IterableDataset | IterableDatasetDict: ... diff --git a/src/guidellm/dataset/entrypoints.py b/src/guidellm/dataset/entrypoints.py index cf689956..1da2222a 100644 --- a/src/guidellm/dataset/entrypoints.py +++ b/src/guidellm/dataset/entrypoints.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import Any, Optional, Union +from typing import Any from datasets import Dataset, IterableDataset from transformers import PreTrainedTokenizerBase # type: ignore[import] @@ -15,12 +15,12 @@ def load_dataset( data: Any, - data_args: Optional[dict[str, Any]], - processor: Optional[Union[str, Path, PreTrainedTokenizerBase]], - processor_args: Optional[dict[str, Any]], + data_args: dict[str, Any] | None, + processor: str | Path | PreTrainedTokenizerBase | None, + processor_args: dict[str, Any] | None, random_seed: int = 42, - split_pref_order: Optional[list[str]] = None, -) -> tuple[Union[Dataset, IterableDataset], dict[ColumnInputTypes, str]]: + split_pref_order: list[str] | None = None, +) -> tuple[Dataset | IterableDataset, dict[ColumnInputTypes, str]]: creators = [ InMemoryDatasetCreator, SyntheticDatasetCreator, diff --git a/src/guidellm/dataset/file.py b/src/guidellm/dataset/file.py index 455ef580..718cb46f 100644 --- a/src/guidellm/dataset/file.py +++ b/src/guidellm/dataset/file.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import Any, Optional, Union +from typing import Any import pandas as pd # type: ignore[import] from datasets import ( @@ -30,7 +30,7 @@ class FileDatasetCreator(DatasetCreator): } @classmethod - def is_supported(cls, data: Any, data_args: Optional[dict[str, Any]]) -> bool: # noqa: ARG003 + def is_supported(cls, data: Any, data_args: dict[str, Any] | None) -> bool: # noqa: ARG003 if isinstance(data, str | Path) and (path := Path(data)).exists(): # local folder or py file, assume supported return path.suffix.lower() in cls.SUPPORTED_TYPES @@ -41,11 +41,11 @@ def is_supported(cls, data: Any, data_args: Optional[dict[str, Any]]) -> bool: def handle_create( cls, data: Any, - data_args: Optional[dict[str, Any]], - processor: Optional[Union[str, Path, PreTrainedTokenizerBase]], # noqa: ARG003 - processor_args: Optional[dict[str, Any]], # noqa: ARG003 + data_args: dict[str, Any] | None, + processor: str | Path | PreTrainedTokenizerBase | None, # noqa: ARG003 + processor_args: dict[str, Any] | None, # noqa: ARG003 random_seed: int, # noqa: ARG003 - ) -> Union[Dataset, DatasetDict, IterableDataset, IterableDatasetDict]: + ) -> Dataset | DatasetDict | IterableDataset | IterableDatasetDict: if not isinstance(data, str | Path): raise ValueError(f"Unsupported data type: {type(data)} given for {data}. ") @@ -63,8 +63,8 @@ def handle_create( @classmethod def load_dataset( - cls, path: Path, data_args: Optional[dict[str, Any]] - ) -> Union[Dataset, IterableDataset]: + cls, path: Path, data_args: dict[str, Any] | None + ) -> Dataset | IterableDataset: if path.suffix.lower() in {".txt", ".text"}: with path.open("r") as file: items = file.readlines() diff --git a/src/guidellm/dataset/hf_datasets.py b/src/guidellm/dataset/hf_datasets.py index 56c79936..bd8d8c23 100644 --- a/src/guidellm/dataset/hf_datasets.py +++ b/src/guidellm/dataset/hf_datasets.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import Any, Optional, Union +from typing import Any from datasets import ( Dataset, @@ -18,7 +18,7 @@ class HFDatasetsCreator(DatasetCreator): @classmethod - def is_supported(cls, data: Any, data_args: Optional[dict[str, Any]]) -> bool: # noqa: ARG003 + def is_supported(cls, data: Any, data_args: dict[str, Any] | None) -> bool: # noqa: ARG003 if isinstance( data, (Dataset, DatasetDict, IterableDataset, IterableDatasetDict) ): @@ -42,11 +42,11 @@ def is_supported(cls, data: Any, data_args: Optional[dict[str, Any]]) -> bool: def handle_create( cls, data: Any, - data_args: Optional[dict[str, Any]], - processor: Optional[Union[str, Path, PreTrainedTokenizerBase]], # noqa: ARG003 - processor_args: Optional[dict[str, Any]], # noqa: ARG003 + data_args: dict[str, Any] | None, + processor: str | Path | PreTrainedTokenizerBase | None, # noqa: ARG003 + processor_args: dict[str, Any] | None, # noqa: ARG003 random_seed: int, # noqa: ARG003 - ) -> Union[Dataset, DatasetDict, IterableDataset, IterableDatasetDict]: + ) -> Dataset | DatasetDict | IterableDataset | IterableDatasetDict: if isinstance(data, str | Path): data = load_dataset(data, **(data_args or {})) elif data_args: diff --git a/src/guidellm/dataset/in_memory.py b/src/guidellm/dataset/in_memory.py index af84f658..0461948c 100644 --- a/src/guidellm/dataset/in_memory.py +++ b/src/guidellm/dataset/in_memory.py @@ -1,6 +1,6 @@ from collections.abc import Iterable from pathlib import Path -from typing import Any, Optional, Union +from typing import Any from datasets import ( Dataset, @@ -17,18 +17,18 @@ class InMemoryDatasetCreator(DatasetCreator): @classmethod - def is_supported(cls, data: Any, data_args: Optional[dict[str, Any]]) -> bool: # noqa: ARG003 + def is_supported(cls, data: Any, data_args: dict[str, Any] | None) -> bool: # noqa: ARG003 return isinstance(data, Iterable) and not isinstance(data, str) @classmethod def handle_create( cls, data: Any, - data_args: Optional[dict[str, Any]], - processor: Optional[Union[str, Path, PreTrainedTokenizerBase]], # noqa: ARG003 - processor_args: Optional[dict[str, Any]], # noqa: ARG003 + data_args: dict[str, Any] | None, + processor: str | Path | PreTrainedTokenizerBase | None, # noqa: ARG003 + processor_args: dict[str, Any] | None, # noqa: ARG003 random_seed: int, # noqa: ARG003 - ) -> Union[Dataset, DatasetDict, IterableDataset, IterableDatasetDict]: + ) -> Dataset | DatasetDict | IterableDataset | IterableDatasetDict: if not isinstance(data, Iterable): raise TypeError( f"Unsupported data format. Expected Iterable[Any], got {type(data)}" diff --git a/src/guidellm/dataset/synthetic.py b/src/guidellm/dataset/synthetic.py index 8c30f0f7..8a1626fe 100644 --- a/src/guidellm/dataset/synthetic.py +++ b/src/guidellm/dataset/synthetic.py @@ -3,7 +3,7 @@ from collections.abc import Iterable, Iterator from itertools import cycle from pathlib import Path -from typing import Any, Literal, Optional, Union +from typing import Any, Literal import yaml from datasets import ( @@ -35,17 +35,17 @@ class SyntheticDatasetConfig(BaseModel): description="The average number of text tokens generated for prompts.", gt=0, ) - prompt_tokens_stdev: Optional[int] = Field( + prompt_tokens_stdev: int | None = Field( description="The standard deviation of the tokens generated for prompts.", gt=0, default=None, ) - prompt_tokens_min: Optional[int] = Field( + prompt_tokens_min: int | None = Field( description="The minimum number of text tokens generated for prompts.", gt=0, default=None, ) - prompt_tokens_max: Optional[int] = Field( + prompt_tokens_max: int | None = Field( description="The maximum number of text tokens generated for prompts.", gt=0, default=None, @@ -54,17 +54,17 @@ class SyntheticDatasetConfig(BaseModel): description="The average number of text tokens generated for outputs.", gt=0, ) - output_tokens_stdev: Optional[int] = Field( + output_tokens_stdev: int | None = Field( description="The standard deviation of the tokens generated for outputs.", gt=0, default=None, ) - output_tokens_min: Optional[int] = Field( + output_tokens_min: int | None = Field( description="The minimum number of text tokens generated for outputs.", gt=0, default=None, ) - output_tokens_max: Optional[int] = Field( + output_tokens_max: int | None = Field( description="The maximum number of text tokens generated for outputs.", gt=0, default=None, @@ -80,7 +80,7 @@ class SyntheticDatasetConfig(BaseModel): ) @staticmethod - def parse_str(data: Union[str, Path]) -> "SyntheticDatasetConfig": + def parse_str(data: str | Path) -> "SyntheticDatasetConfig": if ( isinstance(data, Path) or data.strip().endswith(".config") @@ -117,7 +117,7 @@ def parse_key_value_pairs(data: str) -> "SyntheticDatasetConfig": return SyntheticDatasetConfig(**config_dict) # type: ignore[arg-type] @staticmethod - def parse_config_file(data: Union[str, Path]) -> "SyntheticDatasetConfig": + def parse_config_file(data: str | Path) -> "SyntheticDatasetConfig": with Path(data).open("r") as file: config_dict = yaml.safe_load(file) @@ -128,7 +128,7 @@ class SyntheticTextItemsGenerator( Iterable[ dict[ Literal["prompt", "prompt_tokens_count", "output_tokens_count"], - Union[str, int], + str | int, ] ] ): @@ -150,7 +150,7 @@ def __iter__( ) -> Iterator[ dict[ Literal["prompt", "prompt_tokens_count", "output_tokens_count"], - Union[str, int], + str | int, ] ]: prompt_tokens_sampler = IntegerRangeSampler( @@ -177,7 +177,7 @@ def __iter__( for _, prompt_tokens, output_tokens in zip( range(self.config.samples), prompt_tokens_sampler, - output_tokens_sampler, + output_tokens_sampler, strict=False, ): start_index = rand.randint(0, len(self.text_creator.words)) prompt_text = self.processor.decode( @@ -194,7 +194,7 @@ def __iter__( } def _create_prompt( - self, prompt_tokens: int, start_index: int, unique_prefix: Optional[int] = None + self, prompt_tokens: int, start_index: int, unique_prefix: int | None = None ) -> list[int]: if prompt_tokens <= 0: return [] @@ -224,7 +224,7 @@ class SyntheticDatasetCreator(DatasetCreator): def is_supported( cls, data: Any, - data_args: Optional[dict[str, Any]], # noqa: ARG003 + data_args: dict[str, Any] | None, # noqa: ARG003 ) -> bool: if ( isinstance(data, Path) @@ -248,11 +248,11 @@ def is_supported( def handle_create( cls, data: Any, - data_args: Optional[dict[str, Any]], - processor: Optional[Union[str, Path, PreTrainedTokenizerBase]], - processor_args: Optional[dict[str, Any]], + data_args: dict[str, Any] | None, + processor: str | Path | PreTrainedTokenizerBase | None, + processor_args: dict[str, Any] | None, random_seed: int, - ) -> Union[Dataset, DatasetDict, IterableDataset, IterableDatasetDict]: + ) -> Dataset | DatasetDict | IterableDataset | IterableDatasetDict: processor = check_load_processor( processor, processor_args, @@ -270,7 +270,7 @@ def handle_create( @classmethod def extract_args_column_mappings( cls, - data_args: Optional[dict[str, Any]], + data_args: dict[str, Any] | None, ) -> dict[ColumnInputTypes, str]: data_args_columns = super().extract_args_column_mappings(data_args) diff --git a/src/guidellm/logger.py b/src/guidellm/logger.py index 70259bad..da3464f9 100644 --- a/src/guidellm/logger.py +++ b/src/guidellm/logger.py @@ -72,7 +72,7 @@ def configure_logger(config: LoggingSettings = settings.logging): sys.stdout, level=config.console_log_level.upper(), format="{time:YY-MM-DD HH:mm:ss}|{level: <8} \ - |{name}:{function}:{line} - {message}" + |{name}:{function}:{line} - {message}", ) if config.log_file or config.log_file_level: diff --git a/src/guidellm/preprocess/dataset.py b/src/guidellm/preprocess/dataset.py index a94b8a14..b02efec5 100644 --- a/src/guidellm/preprocess/dataset.py +++ b/src/guidellm/preprocess/dataset.py @@ -1,9 +1,9 @@ import json import os -from collections.abc import Iterator +from collections.abc import Callable, Iterator from enum import Enum from pathlib import Path -from typing import Any, Callable, Optional, Union +from typing import Any import yaml from datasets import Dataset @@ -32,7 +32,7 @@ def handle_ignore_strategy( min_prompt_tokens: int, tokenizer: PreTrainedTokenizerBase, **_kwargs, -) -> Optional[str]: +) -> str | None: """ Ignores prompts that are shorter than the required minimum token length. @@ -56,7 +56,7 @@ def handle_concatenate_strategy( tokenizer: PreTrainedTokenizerBase, concat_delimiter: str, **_kwargs, -) -> Optional[str]: +) -> str | None: """ Concatenates prompts until the minimum token requirement is met. @@ -117,7 +117,7 @@ def handle_error_strategy( min_prompt_tokens: int, tokenizer: PreTrainedTokenizerBase, **_kwargs, -) -> Optional[str]: +) -> str | None: """ Raises an error if the prompt is too short. @@ -150,24 +150,24 @@ class TokensConfig(BaseModel): description="The average number of tokens.", gt=0, ) - stdev: Optional[int] = Field( + stdev: int | None = Field( description="The standard deviation of the tokens.", gt=0, default=None, ) - min: Optional[int] = Field( + min: int | None = Field( description="The minimum number of tokens.", gt=0, default=None, ) - max: Optional[int] = Field( + max: int | None = Field( description="The maximum number of tokens.", gt=0, default=None, ) @staticmethod - def parse_str(data: Union[str, Path]) -> "TokensConfig": + def parse_str(data: str | Path) -> "TokensConfig": """ Parses a string or path into a TokensConfig object. Supports: - JSON string @@ -215,14 +215,14 @@ def parse_key_value_pairs(data: str) -> "TokensConfig": return TokensConfig(**config_dict) # type: ignore[arg-type] @staticmethod - def parse_config_file(data: Union[str, Path]) -> "TokensConfig": + def parse_config_file(data: str | Path) -> "TokensConfig": with Path(data).open("r") as file: config_dict = yaml.safe_load(file) return TokensConfig(**config_dict) -def _validate_output_suffix(output_path: Union[str, Path]) -> None: +def _validate_output_suffix(output_path: str | Path) -> None: output_path = Path(output_path) suffix = output_path.suffix.lower() if suffix not in SUPPORTED_TYPES: @@ -233,18 +233,18 @@ def _validate_output_suffix(output_path: Union[str, Path]) -> None: def process_dataset( - data: Union[str, Path], - output_path: Union[str, Path], - processor: Union[str, Path, PreTrainedTokenizerBase], - prompt_tokens: Union[str, Path], - output_tokens: Union[str, Path], - processor_args: Optional[dict[str, Any]] = None, - data_args: Optional[dict[str, Any]] = None, + data: str | Path, + output_path: str | Path, + processor: str | Path | PreTrainedTokenizerBase, + prompt_tokens: str | Path, + output_tokens: str | Path, + processor_args: dict[str, Any] | None = None, + data_args: dict[str, Any] | None = None, short_prompt_strategy: ShortPromptStrategy = ShortPromptStrategy.IGNORE, - pad_char: Optional[str] = None, - concat_delimiter: Optional[str] = None, + pad_char: str | None = None, + concat_delimiter: str | None = None, push_to_hub: bool = False, - hub_dataset_id: Optional[str] = None, + hub_dataset_id: str | None = None, random_seed: int = 42, ) -> None: """ @@ -354,7 +354,7 @@ def process_dataset( def push_dataset_to_hub( - hub_dataset_id: Optional[str], + hub_dataset_id: str | None, processed_dataset: Dataset, ) -> None: """ diff --git a/src/guidellm/presentation/data_models.py b/src/guidellm/presentation/data_models.py index c1e8f13f..ff2863b4 100644 --- a/src/guidellm/presentation/data_models.py +++ b/src/guidellm/presentation/data_models.py @@ -1,7 +1,7 @@ import random from collections import defaultdict from math import ceil -from typing import TYPE_CHECKING, Optional, Union +from typing import TYPE_CHECKING from pydantic import BaseModel, computed_field @@ -12,14 +12,14 @@ class Bucket(BaseModel): - value: Union[float, int] + value: float | int count: int @staticmethod def from_data( - data: Union[list[float], list[int]], - bucket_width: Optional[float] = None, - n_buckets: Optional[int] = None, + data: list[float] | list[int], + bucket_width: float | None = None, + n_buckets: int | None = None, ) -> tuple[list["Bucket"], float]: if not data: return [], 1.0 @@ -35,7 +35,7 @@ def from_data( else: n_buckets = ceil(range_v / bucket_width) - bucket_counts: defaultdict[Union[float, int], int] = defaultdict(int) + bucket_counts: defaultdict[float | int, int] = defaultdict(int) for val in data: idx = int((val - min_v) // bucket_width) if idx >= n_buckets: @@ -80,7 +80,7 @@ def from_benchmarks(cls, benchmarks: list["GenerativeBenchmark"]): class Distribution(BaseModel): - statistics: Optional[DistributionSummary] = None + statistics: DistributionSummary | None = None buckets: list[Bucket] bucket_width: float @@ -190,7 +190,7 @@ class TabularDistributionSummary(DistributionSummary): """ @computed_field - def percentile_rows(self) -> list[dict[str, Union[str, float]]]: + def percentile_rows(self) -> list[dict[str, str | float]]: rows = [ {"percentile": name, "value": value} for name, value in self.percentiles.model_dump().items() diff --git a/src/guidellm/presentation/injector.py b/src/guidellm/presentation/injector.py index bb1fd684..1e78080e 100644 --- a/src/guidellm/presentation/injector.py +++ b/src/guidellm/presentation/injector.py @@ -1,6 +1,5 @@ import re from pathlib import Path -from typing import Union from loguru import logger @@ -8,7 +7,7 @@ from guidellm.utils.text import load_text -def create_report(js_data: dict, output_path: Union[str, Path]) -> Path: +def create_report(js_data: dict, output_path: str | Path) -> Path: """ Creates a report from the dictionary and saves it to the output path. diff --git a/src/guidellm/request/loader.py b/src/guidellm/request/loader.py index 607a7455..e4a6934e 100644 --- a/src/guidellm/request/loader.py +++ b/src/guidellm/request/loader.py @@ -4,8 +4,6 @@ from typing import ( Any, Literal, - Optional, - Union, ) from datasets import Dataset, DatasetDict, IterableDataset, IterableDatasetDict @@ -43,9 +41,9 @@ def description(self) -> RequestLoaderDescription: ... class GenerativeRequestLoaderDescription(RequestLoaderDescription): type_: Literal["generative_request_loader"] = "generative_request_loader" # type: ignore[assignment] data: str - data_args: Optional[dict[str, Any]] + data_args: dict[str, Any] | None processor: str - processor_args: Optional[dict[str, Any]] + processor_args: dict[str, Any] | None class GenerativeRequestLoader(RequestLoader): @@ -69,18 +67,10 @@ class GenerativeRequestLoader(RequestLoader): def __init__( self, - data: Union[ - str, - Path, - Iterable[Union[str, dict[str, Any]]], - Dataset, - DatasetDict, - IterableDataset, - IterableDatasetDict, - ], - data_args: Optional[dict[str, Any]], - processor: Optional[Union[str, Path, PreTrainedTokenizerBase]], - processor_args: Optional[dict[str, Any]], + data: str | Path | Iterable[str | dict[str, Any]] | Dataset | DatasetDict | IterableDataset | IterableDatasetDict, + data_args: dict[str, Any] | None, + processor: str | Path | PreTrainedTokenizerBase | None, + processor_args: dict[str, Any] | None, shuffle: bool = True, iter_type: Literal["finite", "infinite"] = "finite", random_seed: int = 42, @@ -202,7 +192,7 @@ def _extract_text_column(self) -> str: "'data_args' dictionary." ) - def _extract_prompt_tokens_count_column(self) -> Optional[str]: + def _extract_prompt_tokens_count_column(self) -> str | None: column_names = self._dataset_columns() if column_names and "prompt_tokens_count" in column_names: @@ -213,7 +203,7 @@ def _extract_prompt_tokens_count_column(self) -> Optional[str]: return None - def _extract_output_tokens_count_column(self) -> Optional[str]: + def _extract_output_tokens_count_column(self) -> str | None: column_names = self._dataset_columns() if column_names and "output_tokens_count" in column_names: @@ -224,7 +214,7 @@ def _extract_output_tokens_count_column(self) -> Optional[str]: return None - def _dataset_columns(self, err_msg: Optional[str] = None) -> Optional[list[str]]: + def _dataset_columns(self, err_msg: str | None = None) -> list[str] | None: try: column_names = self.dataset.column_names @@ -240,7 +230,7 @@ def _dataset_columns(self, err_msg: Optional[str] = None) -> Optional[list[str]] def _get_dataset_iter( self, scope_create_count: int - ) -> Optional[Iterator[dict[str, Any]]]: + ) -> Iterator[dict[str, Any]] | None: if scope_create_count > 0 and self.iter_type != "infinite": return None diff --git a/src/guidellm/request/request.py b/src/guidellm/request/request.py index bf4e59fb..83dc40f1 100644 --- a/src/guidellm/request/request.py +++ b/src/guidellm/request/request.py @@ -1,5 +1,5 @@ import uuid -from typing import Any, Literal, Optional +from typing import Any, Literal from pydantic import Field @@ -33,7 +33,7 @@ class GenerationRequest(StandardBaseModel): of output tokens. Used for controlling the behavior of the backend. """ - request_id: Optional[str] = Field( + request_id: str | None = Field( default_factory=lambda: str(uuid.uuid4()), description="The unique identifier for the request.", ) diff --git a/src/guidellm/scheduler/objects.py b/src/guidellm/scheduler/objects.py index 21d30ec8..e2583987 100644 --- a/src/guidellm/scheduler/objects.py +++ b/src/guidellm/scheduler/objects.py @@ -19,7 +19,6 @@ Literal, Protocol, TypeVar, - Union, runtime_checkable, ) @@ -56,10 +55,7 @@ MultiTurnRequestT = TypeAliasType( "MultiTurnRequestT", - Union[ - list[Union[RequestT, tuple[RequestT, float]]], - tuple[Union[RequestT, tuple[RequestT, float]]], - ], + list[RequestT | tuple[RequestT, float]] | tuple[RequestT | tuple[RequestT, float]], type_params=(RequestT,), ) """Multi-turn request structure supporting conversation history with optional delays.""" diff --git a/src/guidellm/utils/statistics.py b/src/guidellm/utils/statistics.py index 04484c2c..f71a2c24 100644 --- a/src/guidellm/utils/statistics.py +++ b/src/guidellm/utils/statistics.py @@ -389,8 +389,7 @@ def from_iterable_request_times( events[global_end] = 0 for (_, end), first_iter, first_iter_count, total_count in zip( - requests, first_iter_times, first_iter_counts, iter_counts, - strict=True + requests, first_iter_times, first_iter_counts, iter_counts, strict=True ): events[first_iter] += first_iter_count diff --git a/tests/integration/scheduler/test_scheduler.py b/tests/integration/scheduler/test_scheduler.py index 51abf59b..65bff95f 100644 --- a/tests/integration/scheduler/test_scheduler.py +++ b/tests/integration/scheduler/test_scheduler.py @@ -167,7 +167,7 @@ def _request_indices(): _request_indices(), received_updates.keys(), received_updates.values(), - received_responses, + received_responses, strict=False, ): assert req == f"req_{index}" assert resp in (f"response_for_{req}", f"mock_error_for_{req}") diff --git a/tests/unit/benchmark/test_output.py b/tests/unit/benchmark/test_output.py index 6763d978..67e65e2e 100644 --- a/tests/unit/benchmark/test_output.py +++ b/tests/unit/benchmark/test_output.py @@ -10,7 +10,10 @@ from guidellm.benchmark import ( GenerativeBenchmarksReport, ) -from guidellm.benchmark.output import GenerativeBenchmarkerConsole, GenerativeBenchmarkerCSV +from guidellm.benchmark.output import ( + GenerativeBenchmarkerConsole, + GenerativeBenchmarkerCSV, +) from tests.unit.mock_benchmark import mock_generative_benchmark @@ -80,6 +83,7 @@ def test_file_yaml(): mock_path.unlink() + @pytest.mark.asyncio async def test_file_csv(): mock_benchmark = mock_generative_benchmark() @@ -105,7 +109,8 @@ def test_console_benchmarks_profile_str(): console = GenerativeBenchmarkerConsole() mock_benchmark = mock_generative_benchmark() assert ( - console._get_profile_str(mock_benchmark) == "type=synchronous, strategies=['synchronous']" + console._get_profile_str(mock_benchmark) + == "type=synchronous, strategies=['synchronous']" ) diff --git a/tests/unit/dataset/test_synthetic.py b/tests/unit/dataset/test_synthetic.py index e3110fa3..544634c8 100644 --- a/tests/unit/dataset/test_synthetic.py +++ b/tests/unit/dataset/test_synthetic.py @@ -530,7 +530,7 @@ def mock_sampler_side_effect(*args, **kwargs): # Results should be identical with same seed assert len(items1) == len(items2) - for item1, item2 in zip(items1, items2): + for item1, item2 in zip(items1, items2, strict=False): assert item1["prompt"] == item2["prompt"] assert item1["prompt_tokens_count"] == item2["prompt_tokens_count"] assert item1["output_tokens_count"] == item2["output_tokens_count"] diff --git a/tests/unit/mock_backend.py b/tests/unit/mock_backend.py index 5ac069a8..3b7237e0 100644 --- a/tests/unit/mock_backend.py +++ b/tests/unit/mock_backend.py @@ -6,7 +6,7 @@ import random import time from collections.abc import AsyncIterator -from typing import Any, Optional +from typing import Any from lorem.text import TextLorem @@ -32,7 +32,7 @@ def __init__( self, target: str = "mock-target", model: str = "mock-model", - iter_delay: Optional[float] = None, + iter_delay: float | None = None, ): """ Initialize mock backend. @@ -53,7 +53,7 @@ def target(self) -> str: return self._target @property - def model(self) -> Optional[str]: + def model(self) -> str | None: """Model name for the mock backend.""" return self._model @@ -87,7 +87,7 @@ async def validate(self) -> None: if not self._in_process: raise RuntimeError("Backend not started up for process") - async def default_model(self) -> Optional[str]: + async def default_model(self) -> str | None: """ Return the default model for the mock backend. """ @@ -97,7 +97,7 @@ async def resolve( self, request: GenerationRequest, request_info: ScheduledRequestInfo, - history: Optional[list[tuple[GenerationRequest, GenerationResponse]]] = None, + history: list[tuple[GenerationRequest, GenerationResponse]] | None = None, ) -> AsyncIterator[tuple[GenerationResponse, ScheduledRequestInfo]]: """ Process a generation request and yield progressive responses. @@ -170,7 +170,7 @@ def _estimate_prompt_tokens(content: str) -> int: return len(str(content).split()) @staticmethod - def _get_tokens(token_count: Optional[int] = None) -> list[str]: + def _get_tokens(token_count: int | None = None) -> list[str]: """ Generate mock tokens for response. """ diff --git a/tests/unit/mock_benchmark.py b/tests/unit/mock_benchmark.py index cdf4375a..d7bfe7c9 100644 --- a/tests/unit/mock_benchmark.py +++ b/tests/unit/mock_benchmark.py @@ -1,4 +1,5 @@ """Mock benchmark objects for unit testing.""" + from guidellm.backends import GenerationRequestTimings from guidellm.benchmark import ( BenchmarkSchedulerStats, diff --git a/tests/unit/utils/test_encoding.py b/tests/unit/utils/test_encoding.py index cc4600cf..5664bcb0 100644 --- a/tests/unit/utils/test_encoding.py +++ b/tests/unit/utils/test_encoding.py @@ -476,7 +476,7 @@ def test_to_from_sequence_collections(self, collection): seq = inst.to_sequence(collection) out = inst.from_sequence(seq) assert len(out) == len(collection) - assert all(a == b for a, b in zip(out, list(collection))) + assert all(a == b for a, b in zip(out, list(collection), strict=False)) @pytest.mark.sanity def test_to_from_sequence_mapping(self): diff --git a/tests/unit/utils/test_typing.py b/tests/unit/utils/test_typing.py index fafa8765..009473f5 100644 --- a/tests/unit/utils/test_typing.py +++ b/tests/unit/utils/test_typing.py @@ -2,10 +2,9 @@ Test suite for the typing utilities module. """ -from typing import Annotated, Literal, Union +from typing import Annotated, Literal, TypeAlias, Union import pytest -from typing_extensions import TypeAlias from guidellm.utils.typing import get_literal_vals @@ -15,7 +14,7 @@ Literal["synchronous", "concurrent", "throughput", "constant", "poisson"], "Valid strategy type identifiers for scheduling request patterns", ] -StrategyProfileType: TypeAlias = Union[LocalStrategyType, LocalProfileType] +StrategyProfileType: TypeAlias = LocalStrategyType | LocalProfileType class TestGetLiteralVals: From 1e8974c2f60fe340cacdb1beaef0957c46e47398 Mon Sep 17 00:00:00 2001 From: Jared O'Connell Date: Thu, 9 Oct 2025 20:10:42 -0400 Subject: [PATCH 16/57] Fix remaining ruff errors Signed-off-by: Jared O'Connell --- pyproject.toml | 2 +- src/guidellm/__main__.py | 7 +++---- src/guidellm/backends/openai.py | 6 ++++-- src/guidellm/benchmark/profile.py | 3 ++- src/guidellm/dataset/hf_datasets.py | 4 ++-- src/guidellm/request/loader.py | 3 ++- tests/unit/benchmark/test_output.py | 2 +- tests/unit/mock_server/test_server.py | 2 +- tests/unit/scheduler/test_objects.py | 8 ++++---- tests/unit/scheduler/test_strategies.py | 2 +- tests/unit/utils/test_synchronous.py | 2 +- tests/unit/utils/test_typing.py | 6 +++--- 12 files changed, 25 insertions(+), 22 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 935587d0..f1624d3f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -167,7 +167,7 @@ ignore_missing_imports = true target-version = "py310" line-length = 88 indent-width = 4 -exclude = ["build", "dist", "env", ".venv"] +exclude = ["build", "dist", "env", ".venv*"] [tool.ruff.format] quote-style = "double" diff --git a/src/guidellm/__main__.py b/src/guidellm/__main__.py index 0a035551..dbc8e1da 100644 --- a/src/guidellm/__main__.py +++ b/src/guidellm/__main__.py @@ -28,7 +28,7 @@ import asyncio import codecs from pathlib import Path -from typing import Annotated, Union +from typing import Annotated import click from pydantic import ValidationError @@ -78,9 +78,8 @@ "run", ] -STRATEGY_PROFILE_CHOICES: Annotated[ - list[str], "Available strategy and profile choices for benchmark execution types" -] = list(get_literal_vals(Union[ProfileType, StrategyType])) +# Available strategy and profile choices for benchmark execution types +STRATEGY_PROFILE_CHOICES: list[str] = list(get_literal_vals(ProfileType | StrategyType)) def decode_escaped_str(_ctx, _param, value): diff --git a/src/guidellm/backends/openai.py b/src/guidellm/backends/openai.py index fd539063..c8eb70f3 100644 --- a/src/guidellm/backends/openai.py +++ b/src/guidellm/backends/openai.py @@ -33,6 +33,8 @@ __all__ = ["OpenAIHTTPBackend", "UsageStats"] +ContentT = str | list[str | dict[str, str | dict[str, str]] | Path | Image.Image] | Any + @dataclasses.dataclass class UsageStats: @@ -431,7 +433,7 @@ async def text_completions( async def chat_completions( self, - content: str | list[str | dict[str, str | dict[str, str]] | Path | Image.Image] | Any, + content: ContentT, request_id: str | None = None, # noqa: ARG002 output_token_count: int | None = None, raw_content: bool = False, @@ -537,7 +539,7 @@ def _get_params(self, endpoint_type: str) -> dict[str, str]: def _get_chat_messages( self, - content: str | list[str | dict[str, str | dict[str, str]] | Path | Image.Image] | Any, + content: ContentT, ) -> list[dict[str, Any]]: if isinstance(content, str): return [{"role": "user", "content": content}] diff --git a/src/guidellm/benchmark/profile.py b/src/guidellm/benchmark/profile.py index ec4fa839..87a9a2be 100644 --- a/src/guidellm/benchmark/profile.py +++ b/src/guidellm/benchmark/profile.py @@ -680,7 +680,8 @@ def next_strategy( ) if self.synchronous_rate <= 0 and self.throughput_rate <= 0: raise RuntimeError( - "Invalid rates in sweep; aborting. Were there any successful requests?" + "Invalid rates in sweep; aborting. " + "Were there any successful requests?" ) self.measured_rates = list( np.linspace( diff --git a/src/guidellm/dataset/hf_datasets.py b/src/guidellm/dataset/hf_datasets.py index bd8d8c23..d1be46c1 100644 --- a/src/guidellm/dataset/hf_datasets.py +++ b/src/guidellm/dataset/hf_datasets.py @@ -20,7 +20,7 @@ class HFDatasetsCreator(DatasetCreator): @classmethod def is_supported(cls, data: Any, data_args: dict[str, Any] | None) -> bool: # noqa: ARG003 if isinstance( - data, (Dataset, DatasetDict, IterableDataset, IterableDatasetDict) + data, Dataset | DatasetDict | IterableDataset | IterableDatasetDict ): # base type is supported return True @@ -55,7 +55,7 @@ def handle_create( ) if isinstance( - data, (Dataset, DatasetDict, IterableDataset, IterableDatasetDict) + data, Dataset | DatasetDict | IterableDataset | IterableDatasetDict ): return data diff --git a/src/guidellm/request/loader.py b/src/guidellm/request/loader.py index e4a6934e..ac34131e 100644 --- a/src/guidellm/request/loader.py +++ b/src/guidellm/request/loader.py @@ -67,7 +67,8 @@ class GenerativeRequestLoader(RequestLoader): def __init__( self, - data: str | Path | Iterable[str | dict[str, Any]] | Dataset | DatasetDict | IterableDataset | IterableDatasetDict, + data: str | Path | Iterable[str | dict[str, Any]] | Dataset | DatasetDict | \ + IterableDataset | IterableDatasetDict, data_args: dict[str, Any] | None, processor: str | Path | PreTrainedTokenizerBase | None, processor_args: dict[str, Any] | None, diff --git a/tests/unit/benchmark/test_output.py b/tests/unit/benchmark/test_output.py index 67e65e2e..6310da88 100644 --- a/tests/unit/benchmark/test_output.py +++ b/tests/unit/benchmark/test_output.py @@ -93,7 +93,7 @@ async def test_file_csv(): csv_benchmarker = GenerativeBenchmarkerCSV(output_path=mock_path) await csv_benchmarker.finalize(report) - with mock_path.open("r") as file: + with mock_path.open("r") as file: # noqa: ASYNC230 # This is a test. reader = csv.reader(file) headers = next(reader) rows = list(reader) diff --git a/tests/unit/mock_server/test_server.py b/tests/unit/mock_server/test_server.py index 008103c3..ba712fb6 100644 --- a/tests/unit/mock_server/test_server.py +++ b/tests/unit/mock_server/test_server.py @@ -162,7 +162,7 @@ async def test_health_endpoint(self, mock_server_instance): assert "status" in data assert data["status"] == "healthy" assert "timestamp" in data - assert isinstance(data["timestamp"], (int, float)) + assert isinstance(data["timestamp"], int | float) @pytest.mark.smoke @pytest.mark.asyncio diff --git a/tests/unit/scheduler/test_objects.py b/tests/unit/scheduler/test_objects.py index fc5610fd..2e0374e4 100644 --- a/tests/unit/scheduler/test_objects.py +++ b/tests/unit/scheduler/test_objects.py @@ -340,7 +340,7 @@ def test_class_signatures(self): for key in self.CHECK_KEYS: assert key in fields field_info = fields[key] - assert field_info.annotation in (Union[float, None], Optional[float]) + assert field_info.annotation in (Union[float, None], Optional[float]) # noqa: UP007 assert field_info.default is None @pytest.mark.smoke @@ -453,7 +453,7 @@ def test_class_signatures(self): for key in self.CHECK_KEYS: assert key in fields field_info = fields[key] - assert field_info.annotation in (Union[float, None], Optional[float]) + assert field_info.annotation in (Union[float, None], Optional[float]) # noqa: UP007 assert field_info.default is None @pytest.mark.smoke @@ -704,11 +704,11 @@ def test_marshalling(self, valid_instances): else: assert original_value is None or isinstance( original_value, - (RequestSchedulerTimings, MeasuredRequestTimings), + RequestSchedulerTimings | MeasuredRequestTimings, ) assert reconstructed_value is None or isinstance( reconstructed_value, - (RequestSchedulerTimings, MeasuredRequestTimings), + RequestSchedulerTimings | MeasuredRequestTimings, ) else: assert original_value == reconstructed_value diff --git a/tests/unit/scheduler/test_strategies.py b/tests/unit/scheduler/test_strategies.py index 67a2d77d..143a3130 100644 --- a/tests/unit/scheduler/test_strategies.py +++ b/tests/unit/scheduler/test_strategies.py @@ -225,7 +225,7 @@ def test_lifecycle( for index in range(max(5, startup_requests + 2)): offset = instance.next_offset() - assert isinstance(offset, (int, float)) + assert isinstance(offset, int | float) if index < startup_requests: expected_offset = initial_offset + (index + 1) * startup_delay diff --git a/tests/unit/utils/test_synchronous.py b/tests/unit/utils/test_synchronous.py index 1a9ea2c9..620ba3fa 100644 --- a/tests/unit/utils/test_synchronous.py +++ b/tests/unit/utils/test_synchronous.py @@ -226,7 +226,7 @@ async def test_invocation(self, objects_types, expected_result): async def set_target(): await asyncio.sleep(0.01) obj = objects[expected_result] - if isinstance(obj, (threading.Event, ProcessingEvent)): + if isinstance(obj, threading.Event | ProcessingEvent): obj.set() else: await asyncio.to_thread(obj.wait) diff --git a/tests/unit/utils/test_typing.py b/tests/unit/utils/test_typing.py index 009473f5..1e31ef8e 100644 --- a/tests/unit/utils/test_typing.py +++ b/tests/unit/utils/test_typing.py @@ -2,7 +2,7 @@ Test suite for the typing utilities module. """ -from typing import Annotated, Literal, TypeAlias, Union +from typing import Annotated, Literal, TypeAlias import pytest @@ -53,7 +53,7 @@ def test_inline_union_type(self): ### WRITTEN BY AI ### """ - result = get_literal_vals(Union[LocalProfileType, LocalStrategyType]) + result = get_literal_vals(LocalProfileType | LocalStrategyType) expected = frozenset( { "synchronous", @@ -117,6 +117,6 @@ def test_literal_union(self): ### WRITTEN BY AI ### """ - result = get_literal_vals(Union[Literal["test", "test2"], Literal["test3"]]) + result = get_literal_vals(Literal["test", "test2"] | Literal["test3"]) expected = frozenset({"test", "test2", "test3"}) assert result == expected From 121dcdc454e57c98ae26f86db2ed984f3c6d14a7 Mon Sep 17 00:00:00 2001 From: Samuel Monson Date: Fri, 10 Oct 2025 09:36:09 -0400 Subject: [PATCH 17/57] Configurable max_tokens/max_completion_tokens key (#399) ## Summary Makes the `max_tokens` request key configurable through an environment variable per endpoint type. Defaults to `max_tokens` for legacy `completions` and `max_completion_tokens` for `chat/completions` ## Details - Add the `GUIDELLM__OPENAI__MAX_OUTPUT_KEY` config option which is a dict mapping from route name -> output tokens key. Default is `{"text_completions": "max_tokens", "chat_completions": "max_completion_tokens"}` ## Test Plan - ## Related Issues - Closes #395 - Closes #269 - Related #210 --- - [x] "I certify that all code in this PR is my own, except as noted below." ## Use of AI - [ ] Includes AI-assisted code completion - [ ] Includes code generated by an AI application - [ ] Includes AI-generated tests (NOTE: AI written tests should have a docstring that includes `## WRITTEN BY AI ##`) --------- Signed-off-by: Tyler Michael Smith Signed-off-by: Samuel Monson Co-authored-by: Tyler Michael Smith --- src/guidellm/backend/openai.py | 15 +++++++++------ src/guidellm/config.py | 4 ++++ tests/unit/conftest.py | 4 +--- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/guidellm/backend/openai.py b/src/guidellm/backend/openai.py index 680578cc..a99962e9 100644 --- a/src/guidellm/backend/openai.py +++ b/src/guidellm/backend/openai.py @@ -31,10 +31,11 @@ TEXT_COMPLETIONS_PATH = "/v1/completions" CHAT_COMPLETIONS_PATH = "/v1/chat/completions" -EndpointType = Literal["chat_completions", "models", "text_completions"] -CHAT_COMPLETIONS: EndpointType = "chat_completions" +CompletionEndpointType = Literal["text_completions", "chat_completions"] +EndpointType = Union[Literal["models"], CompletionEndpointType] +CHAT_COMPLETIONS: CompletionEndpointType = "chat_completions" MODELS: EndpointType = "models" -TEXT_COMPLETIONS: EndpointType = "text_completions" +TEXT_COMPLETIONS: CompletionEndpointType = "text_completions" @Backend.register("openai_http") @@ -447,7 +448,7 @@ def _extra_body(self, endpoint_type: EndpointType) -> dict[str, Any]: def _completions_payload( self, - endpoint_type: EndpointType, + endpoint_type: CompletionEndpointType, orig_kwargs: Optional[dict], max_output_tokens: Optional[int], **kwargs, @@ -467,8 +468,10 @@ def _completions_payload( self.__class__.__name__, max_output_tokens or self.max_output_tokens, ) - payload["max_tokens"] = max_output_tokens or self.max_output_tokens - payload["max_completion_tokens"] = payload["max_tokens"] + max_output_key = settings.openai.max_output_key.get( + endpoint_type, "max_tokens" + ) + payload[max_output_key] = max_output_tokens or self.max_output_tokens if max_output_tokens: # only set stop and ignore_eos if max_output_tokens set at request level diff --git a/src/guidellm/config.py b/src/guidellm/config.py index d9dcef23..0b195c9f 100644 --- a/src/guidellm/config.py +++ b/src/guidellm/config.py @@ -88,6 +88,10 @@ class OpenAISettings(BaseModel): base_url: str = "http://localhost:8000" max_output_tokens: int = 16384 verify: bool = True + max_output_key: dict[Literal["text_completions", "chat_completions"], str] = { + "text_completions": "max_tokens", + "chat_completions": "max_completion_tokens", + } class ReportGenerationSettings(BaseModel): diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index a0457b6f..6ac25c4d 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -132,7 +132,6 @@ async def _mock_completions_response(request) -> AsyncIterable[str]: assert payload["stream_options"] == {"include_usage": True} assert payload["prompt"] is not None assert len(payload["prompt"]) > 0 - assert payload["max_completion_tokens"] > 0 assert payload["max_tokens"] > 0 return httpx.Response( # type: ignore @@ -141,7 +140,7 @@ async def _mock_completions_response(request) -> AsyncIterable[str]: type_="text", prompt=payload["prompt"], output_token_count=( - payload["max_completion_tokens"] + payload["max_tokens"] if payload.get("ignore_eos", False) else None ), @@ -162,7 +161,6 @@ async def _mock_chat_completions_response(request): assert payload["messages"] is not None assert len(payload["messages"]) > 0 assert payload["max_completion_tokens"] > 0 - assert payload["max_tokens"] > 0 return httpx.Response( # type: ignore 200, From d0dad5aa7f752e88d250284bb48cb2f8ed78d5ff Mon Sep 17 00:00:00 2001 From: Jared O'Connell Date: Fri, 10 Oct 2025 12:55:32 -0400 Subject: [PATCH 18/57] Fix unit tests Signed-off-by: Jared O'Connell --- tests/unit/scheduler/test_objects.py | 4 ++-- tests/unit/utils/test_synchronous.py | 32 +++++++++++++++++----------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/tests/unit/scheduler/test_objects.py b/tests/unit/scheduler/test_objects.py index 2e0374e4..2fc4c86f 100644 --- a/tests/unit/scheduler/test_objects.py +++ b/tests/unit/scheduler/test_objects.py @@ -3,6 +3,7 @@ import inspect import typing from collections.abc import AsyncIterator +from types import UnionType from typing import Any, Literal, Optional, TypeVar, Union import pytest @@ -62,8 +63,7 @@ def test_multi_turn_request_t(): assert MultiTurnRequestT.__name__ == "MultiTurnRequestT" value = MultiTurnRequestT.__value__ - assert hasattr(value, "__origin__") - assert value.__origin__ is Union + assert isinstance(value, UnionType) type_params = getattr(MultiTurnRequestT, "__type_params__", ()) assert len(type_params) == 1 diff --git a/tests/unit/utils/test_synchronous.py b/tests/unit/utils/test_synchronous.py index 620ba3fa..7acd5b4a 100644 --- a/tests/unit/utils/test_synchronous.py +++ b/tests/unit/utils/test_synchronous.py @@ -6,7 +6,7 @@ from functools import wraps from multiprocessing.synchronize import Barrier as ProcessingBarrier from multiprocessing.synchronize import Event as ProcessingEvent -from typing import Union +from typing import get_args import pytest @@ -32,17 +32,25 @@ async def new_func(*args, **kwargs): def test_sync_object_types_alias(): - """Test that SyncObjectTypesAlias is defined correctly as a type alias.""" - assert hasattr(SyncObjectTypesAlias, "__origin__") - if hasattr(SyncObjectTypesAlias, "__args__"): - actual_type = SyncObjectTypesAlias.__args__[0] - assert hasattr(actual_type, "__origin__") - assert actual_type.__origin__ is Union - union_args = actual_type.__args__ - assert threading.Event in union_args - assert ProcessingEvent in union_args - assert threading.Barrier in union_args - assert ProcessingBarrier in union_args + """ + Test that SyncObjectTypesAlias is defined correctly as a type alias. + + ## WRITTEN BY AI ## + """ + # Get the actual types from the union alias + actual_types = get_args(SyncObjectTypesAlias) + + # Define the set of expected types + expected_types = { + threading.Event, + ProcessingEvent, + threading.Barrier, + ProcessingBarrier, + } + + # Assert that the set of actual types matches the expected set. + # Using a set comparison is robust as it ignores the order. + assert set(actual_types) == expected_types class TestWaitForSyncEvent: From f862943890f4ad7a5112b93c9eb10f795d8d21f7 Mon Sep 17 00:00:00 2001 From: Mark Kurtz Date: Fri, 10 Oct 2025 16:28:25 -0400 Subject: [PATCH 19/57] Finalize general refactor implementation for data pathways and enabling multimodal --- src/guidellm/__main__.py | 17 +- src/guidellm/backends/__init__.py | 22 +- src/guidellm/backends/backend.py | 4 +- src/guidellm/backends/objects.py | 90 -- src/guidellm/backends/openai.py | 277 ++-- src/guidellm/backends/response_handlers.py | 283 ++++ src/guidellm/benchmark/__init__.py | 54 +- src/guidellm/benchmark/aggregator.py | 1261 --------------- src/guidellm/benchmark/benchmarker.py | 184 +-- src/guidellm/benchmark/entrypoints.py | 61 +- src/guidellm/benchmark/objects.py | 475 ------ src/guidellm/benchmark/output.py | 10 +- src/guidellm/benchmark/profile.py | 14 +- src/guidellm/benchmark/progress.py | 40 +- src/guidellm/benchmark/schemas.py | 1379 +++++++++++++++++ src/guidellm/data/__init__.py | 20 +- src/guidellm/data/collators.py | 2 +- src/guidellm/data/loaders.py | 123 +- src/guidellm/data/objects.py | 157 -- src/guidellm/data/preprocessors/formatters.py | 185 ++- src/guidellm/data/preprocessors/mappers.py | 2 +- src/guidellm/data/schemas.py | 13 + src/guidellm/scheduler/__init__.py | 10 +- src/guidellm/scheduler/constraints.py | 20 +- src/guidellm/scheduler/environments.py | 16 +- src/guidellm/scheduler/scheduler.py | 7 +- .../scheduler/{objects.py => schemas.py} | 174 +-- src/guidellm/scheduler/strategies.py | 12 +- src/guidellm/scheduler/worker.py | 41 +- src/guidellm/scheduler/worker_group.py | 30 +- src/guidellm/schemas/__init__.py | 20 + src/guidellm/schemas/info.py | 132 ++ src/guidellm/schemas/request.py | 164 ++ src/guidellm/schemas/response.py | 97 ++ src/guidellm/schemas/stats.py | 213 +++ src/guidellm/utils/statistics.py | 134 +- src/guidellm/utils/text.py | 11 - tests/unit/backend/test_backend.py | 4 +- tests/unit/backend/test_objects.py | 4 +- tests/unit/backend/test_openai_backend.py | 6 +- tests/unit/mock_benchmark.py | 2 +- tests/unit/utils/test_encoding.py | 4 +- 42 files changed, 2864 insertions(+), 2910 deletions(-) delete mode 100644 src/guidellm/backends/objects.py create mode 100644 src/guidellm/backends/response_handlers.py delete mode 100644 src/guidellm/benchmark/aggregator.py delete mode 100644 src/guidellm/benchmark/objects.py create mode 100644 src/guidellm/benchmark/schemas.py delete mode 100644 src/guidellm/data/objects.py create mode 100644 src/guidellm/data/schemas.py rename src/guidellm/scheduler/{objects.py => schemas.py} (61%) create mode 100644 src/guidellm/schemas/__init__.py create mode 100644 src/guidellm/schemas/info.py create mode 100644 src/guidellm/schemas/request.py create mode 100644 src/guidellm/schemas/response.py create mode 100644 src/guidellm/schemas/stats.py diff --git a/src/guidellm/__main__.py b/src/guidellm/__main__.py index 4bb43d0f..f45637fc 100644 --- a/src/guidellm/__main__.py +++ b/src/guidellm/__main__.py @@ -48,7 +48,6 @@ from guidellm.backends import BackendType from guidellm.benchmark import ( GenerativeConsoleBenchmarkerProgress, - InjectExtrasAggregator, ProfileType, benchmark_generative_text, reimport_benchmarks_report, @@ -56,10 +55,10 @@ from guidellm.benchmark.scenario import ( GenerativeTextScenario, ) -from guidellm.data import GenerativeRequestType from guidellm.mock_server import MockServer, MockServerConfig from guidellm.preprocess.dataset import ShortPromptStrategy, process_dataset from guidellm.scheduler import StrategyType +from guidellm.schemas import GenerativeRequestType from guidellm.settings import print_config from guidellm.utils import Console, DefaultGroupHandler, get_literal_vals from guidellm.utils import cli as cli_tools @@ -375,9 +374,9 @@ def benchmark(): ), ) @click.option( - "--request-samples", + "--sample-requests", "--output-sampling", # legacy alias - "request_samples", + "sample_requests", type=int, help=( "The number of samples for each request status and each benchmark to save " @@ -451,11 +450,10 @@ def run( disable_console_outputs, disable_progress, display_scheduler_stats, - # Aggregators configuration - output_extras, + # Benchmarker configuration + sample_requests, warmup, cooldown, - request_samples, # Constraints configuration max_seconds, max_requests, @@ -519,11 +517,10 @@ def run( else None ), print_updates=not disable_console_outputs, - # Aggregators configuration - add_aggregators={"extras": InjectExtrasAggregator(extras=output_extras)}, + # Benchmarker configuration + sample_requests=sample_requests, warmup=warmup, cooldown=cooldown, - sample_requests=request_samples, # Constraints configuration max_seconds=max_seconds, max_requests=max_requests, diff --git a/src/guidellm/backends/__init__.py b/src/guidellm/backends/__init__.py index 4bcf5683..b07c42ad 100644 --- a/src/guidellm/backends/__init__.py +++ b/src/guidellm/backends/__init__.py @@ -9,20 +9,22 @@ Backend, BackendType, ) -from .objects import ( - GenerationRequest, - GenerationRequestTimings, - GenerationResponse, - GenerationTokenStats, -) from .openai import OpenAIHTTPBackend +from .response_handlers import ( + AudioResponseHandler, + ChatCompletionsResponseHandler, + GenerationResponseHandler, + GenerationResponseHandlerFactory, + TextCompletionsResponseHandler, +) __all__ = [ + "AudioResponseHandler", "Backend", "BackendType", - "GenerationRequest", - "GenerationRequestTimings", - "GenerationResponse", - "GenerationTokenStats", + "ChatCompletionsResponseHandler", + "GenerationResponseHandler", + "GenerationResponseHandlerFactory", "OpenAIHTTPBackend", + "TextCompletionsResponseHandler", ] diff --git a/src/guidellm/backends/backend.py b/src/guidellm/backends/backend.py index a7d82979..6b122c7d 100644 --- a/src/guidellm/backends/backend.py +++ b/src/guidellm/backends/backend.py @@ -16,11 +16,11 @@ from abc import abstractmethod from typing import Literal -from guidellm.backends.objects import ( +from guidellm.scheduler import BackendInterface +from guidellm.schemas import ( GenerationRequest, GenerationResponse, ) -from guidellm.scheduler import BackendInterface from guidellm.utils import RegistryMixin __all__ = [ diff --git a/src/guidellm/backends/objects.py b/src/guidellm/backends/objects.py deleted file mode 100644 index 88d25949..00000000 --- a/src/guidellm/backends/objects.py +++ /dev/null @@ -1,90 +0,0 @@ -""" -Backend object models for request and response handling. - -Provides standardized models for generation requests, responses, and timing -information to ensure consistent data handling across different backend -implementations. -""" - -from __future__ import annotations - -from typing import Literal - -from pydantic import Field - -from guidellm.data import ( - GenerationRequest, - GenerationRequestArguments, - GenerationRequestTimings, -) -from guidellm.scheduler import ( - SchedulerMessagingPydanticRegistry, -) -from guidellm.utils import StandardBaseModel - -__all__ = [ - "GenerationRequest", - "GenerationRequestArguments", - "GenerationRequestTimings", - "GenerationResponse", - "GenerationTokenStats", -] - - -@SchedulerMessagingPydanticRegistry.register() -class GenerationTokenStats(StandardBaseModel): - """Token statistics for generation requests and responses.""" - - request: int | None = Field( - default=None, description="Number of tokens in the original request." - ) - response: int | None = Field( - default=None, description="Number of tokens in the generated response." - ) - - def value( - self, preference: Literal["request", "response"] | None = None - ) -> int | None: - if preference == "request": - return self.request - if preference == "response": - return self.response - return self.response if self.response is not None else self.request - - -@SchedulerMessagingPydanticRegistry.register() -class GenerationResponse(StandardBaseModel): - """Response model for backend generation operations.""" - - request_id: str = Field( - description="Unique identifier matching the original GenerationRequest." - ) - request_args: GenerationRequestArguments = Field( - description="Arguments passed to the backend for this request." - ) - text: str | None = Field( - default=None, - description="The generated response text.", - ) - iterations: int = Field( - default=0, description="Number of generation iterations completed." - ) - - prompt_stats: GenerationTokenStats = Field( - default_factory=GenerationTokenStats, - description="Token statistics from the prompt.", - ) - output_stats: GenerationTokenStats = Field( - default_factory=GenerationTokenStats, - description="Token statistics from the generated output.", - ) - - def total_tokens( - self, preference: Literal["request", "response"] | None = None - ) -> int | None: - prompt_tokens = self.prompt_stats.value(preference=preference) - output_tokens = self.output_stats.value(preference=preference) - - if prompt_tokens is None and output_tokens is None: - return None - return (prompt_tokens or 0) + (output_tokens or 0) diff --git a/src/guidellm/backends/openai.py b/src/guidellm/backends/openai.py index f8ccaafb..eb4b744a 100644 --- a/src/guidellm/backends/openai.py +++ b/src/guidellm/backends/openai.py @@ -13,22 +13,19 @@ from __future__ import annotations import asyncio -import json import time from collections.abc import AsyncIterator -from typing import Any, cast +from typing import Any import httpx -from pydantic import dataclasses from guidellm.backends.backend import Backend -from guidellm.backends.objects import ( +from guidellm.backends.response_handlers import GenerationResponseHandlerFactory +from guidellm.schemas import ( GenerationRequest, - GenerationRequestTimings, GenerationResponse, - GenerationTokenStats, + RequestInfo, ) -from guidellm.scheduler import ScheduledRequestInfo try: import orjson @@ -38,25 +35,7 @@ orjson = None HAS_ORJSON = False -__all__ = ["OpenAIHTTPBackend", "UsageStats"] - - -@dataclasses.dataclass -class UsageStats: - """Token usage statistics for generation requests.""" - - prompt_tokens: int | None = None - output_tokens: int | None = None - - -open_ai_paths: dict[str, str] = { - "health": "health", - "models": "v1/models", - "text_completions": "v1/completions", - "chat_completions": "v1/chat/completions", - "audio_transcriptions": "v1/audio/transcriptions", - "audio_translations": "v1/audio/translations", -} +__all__ = ["OpenAIHTTPBackend"] @Backend.register("openai_http") @@ -87,6 +66,8 @@ def __init__( self, target: str, model: str | None = None, + api_routes: dict[str, str] | None = None, + response_handlers: dict[str, Any] | None = None, timeout: float = 60.0, http2: bool = True, follow_redirects: bool = True, @@ -100,6 +81,15 @@ def __init__( self.model = model # Store configuration + self.api_routes = api_routes or { + "health": "health", + "models": "v1/models", + "text_completions": "v1/completions", + "chat_completions": "v1/chat/completions", + "audio_transcriptions": "v1/audio/transcriptions", + "audio_translations": "v1/audio/translations", + } + self.response_handlers = response_handlers self.timeout = timeout self.http2 = http2 self.follow_redirects = follow_redirects @@ -124,7 +114,7 @@ def info(self) -> dict[str, Any]: "http2": self.http2, "follow_redirects": self.follow_redirects, "verify": self.verify, - "openai_paths": open_ai_paths, + "openai_paths": self.api_routes, "validate_backend": self.validate_backend, } @@ -169,7 +159,8 @@ async def validate(self): :raises RuntimeError: If backend cannot connect or validate configuration. """ - self._check_in_process() + if self._async_client is None: + raise RuntimeError("Backend not started up for process.") if not self.validate_backend: return @@ -191,9 +182,10 @@ async def available_models(self) -> list[str]: :raises HTTPError: If models endpoint returns an error. :raises RuntimeError: If backend is not initialized. """ - self._check_in_process() + if self._async_client is None: + raise RuntimeError("Backend not started up for process.") - target = f"{self.target}/{open_ai_paths['models']}" + target = f"{self.target}/{self.api_routes['models']}" response = await self._async_client.get(target) response.raise_for_status() @@ -214,9 +206,9 @@ async def default_model(self) -> str | None: async def resolve( # noqa: C901 self, request: GenerationRequest, - request_info: ScheduledRequestInfo, + request_info: RequestInfo, history: list[tuple[GenerationRequest, GenerationResponse]] | None = None, - ) -> AsyncIterator[tuple[GenerationResponse, ScheduledRequestInfo]]: + ) -> AsyncIterator[tuple[GenerationResponse, RequestInfo]]: """ Process a generation request and yield progressive responses. @@ -229,181 +221,104 @@ async def resolve( # noqa: C901 :raises NotImplementedError: If history is provided. :yields: Tuples of (response, updated_request_info) as generation progresses. """ - self._check_in_process() + if self._async_client is None: + raise RuntimeError("Backend not started up for process.") + if history is not None: raise NotImplementedError( "Multi-turn requests with conversation history are not yet supported" ) - request_info.request_timings = GenerationRequestTimings() - request.arguments.url = ( - request.arguments.url or f"{self.target}/{request.arguments.path}" - if request.arguments.path is not None - else f"{self.target}/{open_ai_paths[request.request_type]}" + response_handler = ( + self.response_handlers.get(request.request_type) + if self.response_handlers + else None ) - request_info.request_timings.request_start = time.time() + if response_handler is None: + response_handler_class = ( + GenerationResponseHandlerFactory.get_registered_object( + request.request_type + ) + ) + if response_handler_class is None: + raise ValueError( + "No response handler registered for request type " + f"'{request.request_type}'" + ) + response_handler = response_handler_class() + + if (request_path := self.api_routes.get(request.request_type)) is None: + raise ValueError(f"Unsupported request type '{request.request_type}'") + request_url = f"{self.target}/{request_path}" + request_info.timings.request_start = time.time() if not request.arguments.stream: response = await self._async_client.request( request.arguments.method or "POST", - request.arguments.url, - content=request.arguments.content_body, - files=request.arguments.request_files, - json=request.arguments.json_body, + request_url, params=request.arguments.params, headers=request.arguments.headers, + json=request.arguments.body if not request.arguments.files else None, + data=request.arguments.body if request.arguments.files else None, + files=( + { + key: tuple(value) if isinstance(value, list) else value + for key, value in request.arguments.files.items() + } + if request.arguments.files + else None + ), ) + request_info.timings.request_end = time.time() response.raise_for_status() data = response.json() - prompt_stats, output_stats = self._extract_response_stats(data, request) - request_info.request_timings.request_end = time.time() - - yield ( - GenerationResponse( - request_id=request.request_id, - request_args=request.arguments, - text=self._extract_response_text(data), - iterations=0, - prompt_stats=prompt_stats, - output_stats=output_stats, - ), - request_info, - ) + yield response_handler.compile_non_streaming(request, data), request_info return - deltas = [] - prompt_stats = None - output_stats = None - end_reached = False - try: async with self._async_client.stream( request.arguments.method or "POST", - request.arguments.url, - content=request.arguments.content_body, - files=request.arguments.request_files, - json=request.arguments.json_body, + request_url, params=request.arguments.params, headers=request.arguments.headers, + json=request.arguments.body if not request.arguments.files else None, + data=request.arguments.body if request.arguments.files else None, + files=( + { + key: tuple(value) if isinstance(value, list) else value + for key, value in request.arguments.files.items() + } + if request.arguments.files + else None + ), ) as stream: stream.raise_for_status() - buffer = bytearray() + end_reached = False - async for chunk in stream.aiter_bytes(): - if not chunk or end_reached: + async for chunk in stream.aiter_lines(): + if end_reached: continue - buffer.extend(chunk) - - while (start := buffer.find(b"data:")) != -1 and ( - end := buffer.find(b"\n", start) - ) != -1: - line = buffer[start + len(b"data:") : end].strip() - buffer = buffer[end + 1 :] - - if not line: - continue - - if line == b"[DONE]": - if request_info.request_timings.request_end is None: - request_info.request_timings.request_end = time.time() - end_reached = True - break - - data = ( - json.loads(line) if not HAS_ORJSON else orjson.loads(line) - ) - - if "usage" in data and data["usage"] is not None: - request_info.request_timings.request_end = time.time() - prompt_stats, output_stats = self._extract_response_stats( - data, request - ) - else: - if request_info.request_timings.first_iteration is None: - request_info.request_timings.first_iteration = ( - time.time() - ) - request_info.request_timings.last_iteration = time.time() - deltas.append(self._extract_response_text(data)) - - yield ( - GenerationResponse( - request_id=request.request_id, - request_args=request.arguments, - text="".join(deltas) if deltas else None, - iterations=len(deltas), - prompt_stats=prompt_stats or GenerationTokenStats(), - output_stats=output_stats or GenerationTokenStats(), - ), - request_info, - ) - except asyncio.CancelledError as err: - yield ( # Ensure we yield what we have so far before stopping - GenerationResponse( - request_id=request.request_id, - request_args=request.arguments, - text="".join(deltas) if deltas else None, - iterations=len(deltas), - prompt_stats=prompt_stats or GenerationTokenStats(), - output_stats=output_stats or GenerationTokenStats(), - ), - request_info, - ) - raise err - - def _extract_response_text(self, data: dict) -> str: - if not data: - return None - - object_type = data.get("object") or data.get("type") - - if object_type == "text_completion": - return data.get("choices", [{}])[0].get("text", "") - - if object_type == "chat.completion": - return data.get("choices", [{}])[0].get("message", {}).get("content", "") - - if object_type == "chat.completion.chunk": - return data.get("choices", [{}])[0].get("delta", {}).get("content", "") - - if "text" in data: - return data.get("text", "") - - if "delta" in data: - return data.get("delta", "") - raise ValueError(f"Unsupported response format: {data}") - - def _extract_response_stats( - self, data: dict, request: GenerationRequest - ) -> tuple[GenerationTokenStats, GenerationTokenStats]: - prompt_stats = GenerationTokenStats() - output_stats = GenerationTokenStats() + if ( + iterations := response_handler.add_streaming_line(chunk) + ) is None or iterations < 0: + end_reached = end_reached or iterations is None + continue - if not data or not (usage := cast("dict", data.get("usage"))): - return prompt_stats, output_stats + if request_info.timings.first_iteration is None: + request_info.timings.first_iteration = time.time() + request_info.timings.last_iteration = time.time() - prompt_stats.request = request.stats.get("prompt_tokens") - prompt_stats.response = usage.get("prompt_tokens", usage.get("input_tokens")) - prompt_token_details = usage.get( - "prompt_tokens_details", usage.get("input_tokens_details") - ) - if prompt_token_details: - for key, val in prompt_token_details.items(): - setattr(prompt_stats, key, val) + if request_info.timings.iterations is None: + request_info.timings.iterations = 0 + request_info.timings.iterations += iterations - output_stats.request = request.stats.get("output_tokens") - output_stats.response = usage.get( - "completion_tokens", usage.get("output_tokens") - ) - output_token_details = usage.get( - "completion_tokens_details", usage.get("output_tokens_details") - ) - if output_token_details: - for key, val in output_token_details.items(): - setattr(output_stats, key, val) + request_info.timings.request_end = time.time() - return prompt_stats, output_stats + yield response_handler.compile_streaming(request), request_info + except asyncio.CancelledError as err: + yield response_handler.compile_streaming(request), request_info + raise err def _resolve_validate_kwargs( self, validate_backend: bool | str | dict[str, Any] @@ -414,8 +329,8 @@ def _resolve_validate_kwargs( if validate_kwargs is True: validate_kwargs = "health" - if isinstance(validate_kwargs, str) and validate_kwargs in open_ai_paths: - validate_kwargs = f"{self.target}/{open_ai_paths[validate_kwargs]}" + if isinstance(validate_kwargs, str) and validate_kwargs in self.api_routes: + validate_kwargs = f"{self.target}/{self.api_routes[validate_kwargs]}" if isinstance(validate_kwargs, str): validate_kwargs = { @@ -433,9 +348,3 @@ def _resolve_validate_kwargs( validate_kwargs["method"] = "GET" return validate_kwargs - - def _check_in_process(self): - if not self._in_process or self._async_client is None: - raise RuntimeError( - "Backend not started up for process, cannot process requests." - ) diff --git a/src/guidellm/backends/response_handlers.py b/src/guidellm/backends/response_handlers.py new file mode 100644 index 00000000..492f8e99 --- /dev/null +++ b/src/guidellm/backends/response_handlers.py @@ -0,0 +1,283 @@ +from __future__ import annotations + +import json +from typing import Any, Protocol, cast + +from guidellm.schemas import GenerationRequest, GenerationResponse, UsageMetrics +from guidellm.utils import RegistryMixin + +try: + import orjson +except ImportError: + orjson = None + +__all__ = [ + "AudioResponseHandler", + "ChatCompletionsResponseHandler", + "GenerationResponseHandler", + "GenerationResponseHandlerFactory", + "TextCompletionsResponseHandler", +] + + +class GenerationResponseHandler(Protocol): + def compile_non_streaming( + self, request: GenerationRequest, response: Any + ) -> GenerationResponse: ... + + def add_streaming_line(self, line: str) -> int | None: ... + + def compile_streaming(self, request: GenerationRequest) -> GenerationResponse: ... + + +class GenerationResponseHandlerFactory(RegistryMixin[type[GenerationResponseHandler]]): + pass + + +@GenerationResponseHandlerFactory.register("text_completions") +class TextCompletionsResponseHandler(GenerationResponseHandler): + def __init__(self): + self.streaming_texts: list[str] = [] + self.streaming_usage: dict[str, int | dict[str, int]] | None = None + + def compile_non_streaming( + self, request: GenerationRequest, response: dict + ) -> GenerationResponse: + choices = cast("list[dict]", response.get("choices", [])) + usage = cast("dict[str, int | dict[str, int]]", response.get("usage", {})) + input_metrics, output_metrics = self.extract_metrics(usage) + + return GenerationResponse( + request_id=request.request_id, + request_args=str( + request.arguments.model_dump() if request.arguments else None + ), + text=choices[0].get("text", "") if choices else "", + input_metrics=input_metrics, + output_metrics=output_metrics, + ) + + def add_streaming_line(self, line: str) -> int | None: + if line == "data: [DONE]": + return None + + if not line or not (line := line.strip()) or not line.startswith("data:"): + return 0 + + line = line[len("data:") :].strip() + data = cast( + "dict[str, Any]", + json.loads(line) if orjson is None else orjson.loads(line), + ) + updated = False + + if (choices := cast("list[dict]", data.get("choices"))) and ( + text := choices[0].get("text") + ): + self.streaming_texts.append(text) + updated = True + + if usage := cast("dict[str, int | dict[str, int]]", data.get("usage")): + self.streaming_usage = usage + + return 1 if updated else 0 + + def compile_streaming(self, request: GenerationRequest) -> GenerationResponse: + input_metrics, output_metrics = self.extract_metrics(self.streaming_usage) + + return GenerationResponse( + request_id=request.request_id, + request_args=str( + request.arguments.model_dump() if request.arguments else None + ), + text="".join(self.streaming_texts), + input_metrics=input_metrics, + output_metrics=output_metrics, + ) + + def extract_metrics( + self, usage: dict[str, int | dict[str, int]] | None + ) -> tuple[UsageMetrics, UsageMetrics]: + if not usage: + return UsageMetrics(), UsageMetrics() + + input_details = cast("dict[str, int]", usage.get("prompt_tokens_details", {})) + output_details = cast( + "dict[str, int]", usage.get("completion_tokens_details", {}) + ) + + return UsageMetrics( + text_tokens=input_details.get("prompt_tokens") + or cast("int", usage.get("prompt_tokens")), + image_tokens=input_details.get("image_tokens"), + video_tokens=input_details.get("video_tokens"), + audio_tokens=input_details.get("audio_tokens"), + audio_seconds=input_details.get("seconds"), + ), UsageMetrics( + text_tokens=output_details.get("completion_tokens") + or cast("int", usage.get("completion_tokens")), + image_tokens=output_details.get("image_tokens"), + video_tokens=output_details.get("video_tokens"), + audio_tokens=output_details.get("audio_tokens"), + audio_seconds=output_details.get("seconds"), + ) + + +@GenerationResponseHandlerFactory.register("chat_completions") +class ChatCompletionsResponseHandler(TextCompletionsResponseHandler): + def compile_non_streaming( + self, request: GenerationRequest, response: dict + ) -> GenerationResponse: + choices = cast("list[dict]", response.get("choices", [])) + usage = cast("dict[str, int | dict[str, int]]", response.get("usage", {})) + input_metrics, output_metrics = self.extract_metrics(usage) + + return GenerationResponse( + request_id=request.request_id, + request_args=str( + request.arguments.model_dump() if request.arguments else None + ), + text=cast("dict", choices[0].get("message", {})).get("content", "") + if choices + else "", + input_metrics=input_metrics, + output_metrics=output_metrics, + ) + + def add_streaming_line(self, line: str) -> int | None: + if line == "data: [DONE]": + return None + + if not line or not (line := line.strip()) or not line.startswith("data:"): + return 0 + + line = line[len("data:") :].strip() + data = cast( + "dict[str, Any]", + json.loads(line) if orjson is None else orjson.loads(line), + ) + updated = False + + # Extract delta content for chat completion chunks + if choices := cast("list[dict]", data.get("choices")): + delta = choices[0].get("delta", {}) + if content := delta.get("content"): + self.streaming_texts.append(content) + updated = True + + if usage := cast("dict[str, int | dict[str, int]]", data.get("usage")): + self.streaming_usage = usage + + return 1 if updated else 0 + + def compile_streaming(self, request: GenerationRequest) -> GenerationResponse: + input_metrics, output_metrics = self.extract_metrics(self.streaming_usage) + + return GenerationResponse( + request_id=request.request_id, + request_args=str( + request.arguments.model_dump() if request.arguments else None + ), + text="".join(self.streaming_texts), + input_metrics=input_metrics, + output_metrics=output_metrics, + ) + + +@GenerationResponseHandlerFactory.register( + ["audio_transcriptions", "audio_translations"] +) +class AudioResponseHandler: + def __init__(self): + self.streaming_buffer: bytearray = bytearray() + self.streaming_texts: list[str] = [] + self.streaming_usage: dict[str, int | dict[str, int]] | None = None + + def compile_non_streaming( + self, request: GenerationRequest, response: dict + ) -> GenerationResponse: + usage = cast("dict[str, int]", response.get("usage", {})) + input_details = cast("dict[str, int]", usage.get("input_token_details", {})) + output_details = cast("dict[str, int]", usage.get("output_token_details", {})) + text = response.get("text", "") + + return GenerationResponse( + request_id=request.request_id, + request_args=str( + request.arguments.model_dump() if request.arguments else None + ), + text=text, + input_metrics=UsageMetrics( + text_tokens=input_details.get("text_tokens", usage.get("input_tokens")), + audio_tokens=input_details.get( + "audio_tokens", usage.get("input_tokens") + ), + audio_seconds=input_details.get("seconds", usage.get("seconds")), + ), + output_metrics=UsageMetrics( + text_tokens=output_details.get( + "text_tokens", usage.get("output_tokens") + ), + ), + ) + + def add_streaming_line(self, line: str) -> int | None: + if line == "data: [DONE]": + return None + + if not line or not (line := line.strip()) or not line.startswith("{"): + return 0 + + data = cast( + "dict[str, Any]", + json.loads(line) if orjson is None else orjson.loads(line), + ) + updated = False + + if text := data.get("text"): + self.streaming_texts.append(text) + updated = True + + if usage := cast("dict[str, int | dict[str, int]]", data.get("usage")): + self.streaming_usage = usage + + return 1 if updated else 0 + + def compile_streaming(self, request: GenerationRequest) -> GenerationResponse: + input_metrics, output_metrics = self.extract_metrics(self.streaming_usage) + + return GenerationResponse( + request_id=request.request_id, + request_args=str( + request.arguments.model_dump() if request.arguments else None + ), + text="".join(self.streaming_texts), + input_metrics=input_metrics, + output_metrics=output_metrics, + ) + + def extract_metrics( + self, usage: dict[str, int | dict[str, int]] | None + ) -> tuple[UsageMetrics, UsageMetrics]: + if not usage: + return UsageMetrics(), UsageMetrics() + + input_details = cast("dict[str, int]", usage.get("input_token_details", {})) + output_details = cast("dict[str, int]", usage.get("output_token_details", {})) + + return UsageMetrics( + text_tokens=( + input_details.get("text_tokens") + or cast("int", usage.get("input_tokens")) + ), + audio_tokens=( + input_details.get("audio_tokens") + or cast("int", usage.get("audio_tokens")) + ), + audio_seconds=( + input_details.get("seconds") or cast("int", usage.get("seconds")) + ), + ), UsageMetrics( + text_tokens=output_details.get("text_tokens") + or cast("int", usage.get("output_tokens")), + ) diff --git a/src/guidellm/benchmark/__init__.py b/src/guidellm/benchmark/__init__.py index 76324a65..57756d15 100644 --- a/src/guidellm/benchmark/__init__.py +++ b/src/guidellm/benchmark/__init__.py @@ -1,25 +1,5 @@ -from .aggregator import ( - Aggregator, - AggregatorState, - CompilableAggregator, - GenerativeRequestsAggregator, - GenerativeStatsProgressAggregator, - InjectExtrasAggregator, - SchedulerStatsAggregator, - SerializableAggregator, -) from .benchmarker import Benchmarker from .entrypoints import benchmark_generative_text, reimport_benchmarks_report -from .objects import ( - Benchmark, - BenchmarkMetrics, - BenchmarkSchedulerStats, - BenchmarkT, - GenerativeBenchmark, - GenerativeBenchmarksReport, - GenerativeMetrics, - GenerativeRequestStats, -) from .output import ( GenerativeBenchmarkerConsole, GenerativeBenchmarkerCSV, @@ -40,20 +20,34 @@ BenchmarkerProgressGroup, GenerativeConsoleBenchmarkerProgress, ) +from .schemas import ( + Benchmark, + BenchmarkArgs, + BenchmarkerDict, + BenchmarkSchedulerStats, + EstimatedBenchmarkState, + GenerativeAudioMetricsSummary, + GenerativeBenchmark, + GenerativeBenchmarksReport, + GenerativeImageMetricsSummary, + GenerativeMetrics, + GenerativeMetricsSummary, + GenerativeVideoMetricsSummary, + SchedulerDict, +) __all__ = [ - "Aggregator", - "AggregatorState", "AsyncProfile", "Benchmark", - "BenchmarkMetrics", + "BenchmarkArgs", "BenchmarkSchedulerStats", - "BenchmarkT", "Benchmarker", + "BenchmarkerDict", "BenchmarkerProgress", "BenchmarkerProgressGroup", - "CompilableAggregator", "ConcurrentProfile", + "EstimatedBenchmarkState", + "GenerativeAudioMetricsSummary", "GenerativeBenchmark", "GenerativeBenchmarkerCSV", "GenerativeBenchmarkerConsole", @@ -61,15 +55,13 @@ "GenerativeBenchmarkerOutput", "GenerativeBenchmarksReport", "GenerativeConsoleBenchmarkerProgress", + "GenerativeImageMetricsSummary", "GenerativeMetrics", - "GenerativeRequestStats", - "GenerativeRequestsAggregator", - "GenerativeStatsProgressAggregator", - "InjectExtrasAggregator", + "GenerativeMetricsSummary", + "GenerativeVideoMetricsSummary", "Profile", "ProfileType", - "SchedulerStatsAggregator", - "SerializableAggregator", + "SchedulerDict", "SweepProfile", "SynchronousProfile", "ThroughputProfile", diff --git a/src/guidellm/benchmark/aggregator.py b/src/guidellm/benchmark/aggregator.py deleted file mode 100644 index 2dc3c56f..00000000 --- a/src/guidellm/benchmark/aggregator.py +++ /dev/null @@ -1,1261 +0,0 @@ -""" -Benchmark result aggregation and compilation interfaces. - -Provides protocols and implementations for collecting, processing, and compiling -benchmark data from scheduler executions into final metrics and statistics. - -Classes: - Aggregator: Protocol for processing benchmark data updates. - CompilableAggregator: Protocol for aggregators that can compile final results. - SchedulerStatsAggregator: Aggregates scheduler timing and performance metrics. - GenerativeRequestsStatsProgressAggregator: Tracks generation metrics during run. - GenerativeRequestsAggregator: Compiles complete generative benchmark results. - -Functions: - add_aggregate_metric: Helper for accumulating timing and count metrics. - -Type Variables: - RequestT: Generic request object type. - ResponseT: Generic response object type. - RequestTimingsT: Generic request timing object type. -""" - -from __future__ import annotations - -import math -import random -from abc import ABC, abstractmethod -from typing import ( - Any, - ClassVar, - Generic, - Literal, - Protocol, - runtime_checkable, -) - -from pydantic import Field, PrivateAttr - -from guidellm.backends import ( - GenerationRequest, - GenerationResponse, -) -from guidellm.benchmark.objects import ( - BenchmarkSchedulerStats, - GenerativeMetrics, - GenerativeRequestStats, -) -from guidellm.scheduler import ( - RequestT, - ResponseT, - ScheduledRequestInfo, - SchedulerState, -) -from guidellm.settings import settings -from guidellm.utils import ( - InfoMixin, - PydanticClassRegistryMixin, - StatusBreakdown, - StatusDistributionSummary, - all_defined, - safe_divide, - safe_getattr, -) - -__all__ = [ - "Aggregator", - "AggregatorState", - "CompilableAggregator", - "GenerativeRequestsAggregator", - "GenerativeStatsProgressAggregator", - "InjectExtrasAggregator", - "SchedulerStatsAggregator", - "SerializableAggregator", -] - - -class AggregatorState(dict[str, Any]): - def add_metric( - self, - key: str, - value: int | float | None, - start_val: int | float | None = 0.0, - count: int | None = 1, - duration: float | None = None, - duration_div: Literal["total", "avg"] = "total", - prefix: str | None = None, - ): - """ - Add timing or count metrics to aggregation state. - """ - if prefix: - self.add_metric( - key=f"{prefix}_{key}", - value=value, - start_val=start_val, - count=count, - duration=duration, - duration_div=duration_div, - ) - return - - if not all_defined(value, start_val, count): - return - - delta_val = value - start_val - self[f"{key}_total"] = self.get(f"{key}_total", 0) + delta_val - self[f"{key}_count"] = self.get(f"{key}_count", 0) + count - self[f"{key}_avg"] = safe_divide( - self.get(f"{key}_total"), self.get(f"{key}_count") - ) - - if all_defined(duration): - self[f"{key}_duration"] = duration - self[f"{key}_rate"] = safe_divide( - self.get(f"{key}_{duration_div}"), duration - ) - - def set_metric( - self, - key: str, - value: int | float | None, - type_: Literal["total", "count", "avg", "duration", "rate"], - prefix: str | None = None, - ): - if prefix: - self.set_metric( - key=f"{prefix}_{key}", - value=value, - type_=type_, - prefix=None, - ) - return - - self[f"{key}_{type_}"] = value - - def get_metric( - self, - key: str, - type_: Literal["total", "count", "avg", "duration", "rate"], - default: int | float | None = None, - prefix: str | None = None, - ) -> int | float | None: - if prefix: - return self.get_metric( - key=f"{prefix}_{key}", - type_=type_, - default=default, - ) - - return self.get(f"{key}_{type_}", default) - - -@runtime_checkable -class Aggregator(Protocol[ResponseT, RequestT]): - """ - Protocol for processing benchmark data updates during execution. - - Defines the interface for aggregators that collect and process request/response - data from scheduler executions. Implementations update aggregation state with - each completed request for eventual compilation into final metrics. - """ - - def __call__( - self, - state: AggregatorState, - response: ResponseT | None, - request: RequestT, - request_info: ScheduledRequestInfo, - scheduler_state: SchedulerState, - ) -> dict[str, Any] | None: - """ - Process a completed request and update aggregation state. - - :param state: Current aggregation state to update in-place. - :param response: Response generated for the request, if successful. - :param request: The processed request object. - :param request_info: Scheduling metadata and timing information. - :param scheduler_state: Current scheduler execution state. - :return: Optional intermediate updates for progress reporting. - """ - - -@runtime_checkable -class CompilableAggregator(Protocol[ResponseT, RequestT]): - """ - Protocol for aggregators that compile final results from aggregated state. - - Extends the Aggregator protocol with the ability to transform accumulated - state into final benchmark results and metrics after execution completes. - """ - - def __call__( - self, - state: AggregatorState, - response: ResponseT | None, - request: RequestT, - request_info: ScheduledRequestInfo, - scheduler_state: SchedulerState, - ) -> dict[str, Any] | None: - """ - Process a completed request and update aggregation state. - - :param state: Current aggregation state to update in-place. - :param response: Response generated for the request, if successful. - :param request: The processed request object. - :param request_info: Scheduling metadata and timing information. - :param scheduler_state: Current scheduler execution state. - :return: Optional intermediate updates for progress reporting. - """ - - def compile( - self, state: AggregatorState, scheduler_state: SchedulerState - ) -> dict[str, Any]: - """ - Compile aggregated state into final benchmark results. - - :param agg_state: The accumulated aggregation state. - :param scheduler_state: Final scheduler execution state. - :return: Compiled benchmark results and metrics. - """ - - -class SerializableAggregator( - PydanticClassRegistryMixin[type["SerializableAggregator"]], - ABC, - Generic[ResponseT, RequestT], -): - schema_discriminator: ClassVar[str] = "type_" - - @classmethod - def __pydantic_schema_base_type__(cls) -> type[SerializableAggregator]: - if cls.__name__ == "SerializableAggregator": - return cls - - return SerializableAggregator - - @classmethod - @abstractmethod - def validated_kwargs(cls, *args, **kwargs) -> dict[str, Any]: - """ - Validate and process arguments for constraint creation. - - Must be implemented by subclasses to handle their specific parameter patterns. - - :param args: Positional arguments passed to the constraint - :param kwargs: Keyword arguments passed to the constraint - :return: Validated dictionary of parameters for constraint creation - :raises NotImplementedError: Must be implemented by subclasses - """ - ... - - @classmethod - def resolve( - cls, - aggregators: dict[ - str, - Any | dict[str, Any] | Aggregator | CompilableAggregator, - ], - ) -> dict[str, Aggregator | CompilableAggregator]: - """ - Resolve mixed aggregator specifications to callable aggregators. - - :param aggregators: Dictionary mapping aggregator keys to specifications - :return: Dictionary mapping aggregator keys to callable functions - :raises ValueError: If any key is not registered in the factory - """ - resolved = {} - - for key, val in aggregators.items(): - if isinstance(val, (Aggregator, CompilableAggregator)): - resolved[key] = val - else: - aggregator_class = cls.get_registered_object(key) - kwargs = aggregator_class.validated_kwargs(**val) - resolved[key] = aggregator_class(**kwargs) - - return resolved - - type_: Literal["aggregator"] = Field(default="aggregator", description="") - - @abstractmethod - def __call__( - self, - state: AggregatorState, - response: ResponseT | None, - request: RequestT, - request_info: ScheduledRequestInfo, - scheduler_state: SchedulerState, - ) -> dict[str, Any] | None: - """ - Process a completed request and update aggregation state. - - :param agg_state: Current aggregation state to update in-place. - :param response: Response generated for the request, if successful. - :param request: The processed request object. - :param request_info: Scheduling metadata and timing information. - :param scheduler_state: Current scheduler execution state. - :return: Optional intermediate updates for progress reporting. - """ - - @abstractmethod - def compile( - self, state: AggregatorState, scheduler_state: SchedulerState - ) -> dict[str, Any]: - """ - Compile aggregated state into final benchmark results. - - :param agg_state: The accumulated aggregation state. - :param scheduler_state: Final scheduler execution state. - :return: Compiled benchmark results and metrics. - """ - - -@SerializableAggregator.register("inject_extras") -class InjectExtrasAggregator(SerializableAggregator[ResponseT, RequestT], InfoMixin): - """ - Aggregator for injecting extra metadata into the output. - """ - - @classmethod - def validated_kwargs(cls, extras: dict[str, Any], **_kwargs) -> dict[str, Any]: - return {"extras": extras} - - type_: Literal["inject_extras"] = Field(default="inject_extras") - extras: dict[str, Any] | None = Field(default_factory=None) - - def __call__( - self, - state: AggregatorState, - response: ResponseT | None, - request: RequestT, - request_info: ScheduledRequestInfo, - scheduler_state: SchedulerState, - ) -> dict[str, Any] | None: - """ - Inject extra metadata into the aggregation state. - - :param agg_state: Current aggregation state to update. - :param response: Response generated for the request, if successful. - :param request: The processed request object. - :param request_info: Scheduling metadata and timing information. - :param scheduler_state: Current scheduler execution state. - :return: Updated aggregation state with injected extras. - """ - _ = (state, response, request, request_info, scheduler_state) # unused - return None - - def compile( - self, state: AggregatorState, scheduler_state: SchedulerState - ) -> dict[str, Any]: - _ = (state, scheduler_state) # unused - return {"extras": self.extras} if self.extras else {} - - -@SerializableAggregator.register("scheduler_stats") -class SchedulerStatsAggregator(SerializableAggregator[ResponseT, RequestT], InfoMixin): - """ - Aggregates scheduler timing and performance metrics. - - Collects timing data for various scheduler phases including queuing, - resolution, and processing delays to generate performance statistics. - """ - - @classmethod - def validated_kwargs(cls, *_args, **_kwargs) -> dict[str, Any]: - return {} - - type_: Literal["scheduler_stats"] = Field(default="scheduler_stats") - - def __call__( - self, - state: AggregatorState, - response: ResponseT | None, - request: RequestT, - request_info: ScheduledRequestInfo, - scheduler_state: SchedulerState, - ) -> dict[str, Any] | None: - """ - Aggregate scheduler timing metrics for a completed request. - - :param agg_state: Current aggregation state to update. - :param response: Response generated for the request, if successful. - :param request: The processed request object. - :param request_info: Scheduling metadata and timing information. - :param scheduler_state: Current scheduler execution state. - :return: Updated aggregation state for intermediate reporting. - """ - _ = (response, request, scheduler_state) # unused - if request_info.status not in ("completed", "errored", "cancelled"): - # Only compile scheduler stats for processed requests - return None - - state["updated_scheduler_stats"] = True - state.add_metric( - key="queued_time", - value=request_info.scheduler_timings.dequeued, - start_val=request_info.scheduler_timings.queued, - ) - state.add_metric( - key="worker_resolve_start_delay", - value=request_info.scheduler_timings.resolve_start, - start_val=request_info.scheduler_timings.scheduled_at, - ) - state.add_metric( - key="worker_resolve_time", - value=request_info.scheduler_timings.resolve_end, - start_val=request_info.scheduler_timings.resolve_start, - ) - state.add_metric( - key="worker_resolve_end_delay", - value=request_info.scheduler_timings.resolve_end, - start_val=safe_getattr(request_info.request_timings, "request_end"), - ) - state.add_metric( - key="finalized_delay", - value=request_info.scheduler_timings.finalized, - start_val=request_info.scheduler_timings.resolve_end, - ) - state.add_metric( - key="worker_targeted_start_delay", - value=request_info.scheduler_timings.resolve_start, - start_val=request_info.scheduler_timings.targeted_start, - ) - state.add_metric( - key="request_start_delay", - value=request_info.scheduler_timings.resolve_start, - start_val=safe_getattr(request_info.request_timings, "request_start"), - ) - state.add_metric( - key="request_time", - value=safe_getattr(request_info.request_timings, "request_end"), - start_val=safe_getattr(request_info.request_timings, "request_start"), - ) - state.add_metric( - key="request_targeted_start_delay", - value=safe_getattr(request_info.request_timings, "request_start"), - start_val=request_info.scheduler_timings.targeted_start, - ) - - return state - - def compile( - self, state: AggregatorState, scheduler_state: SchedulerState - ) -> dict[Literal["run_stats"], BenchmarkSchedulerStats]: - """ - Compile scheduler timing metrics into benchmark statistics. - - :param agg_state: Accumulated timing data and counts. - :param scheduler_state: Final scheduler execution state. - :return: Dictionary containing compiled scheduler statistics. - """ - return { - "run_stats": BenchmarkSchedulerStats( - start_time=scheduler_state.start_time, - end_time=scheduler_state.end_time, - requests_made=StatusBreakdown[int, int, int, int]( - successful=scheduler_state.successful_requests, - incomplete=scheduler_state.cancelled_requests, - errored=scheduler_state.errored_requests, - total=( - scheduler_state.successful_requests - + scheduler_state.cancelled_requests - + scheduler_state.errored_requests - ), - ), - queued_time_avg=state.get_metric( - key="queued_time", type_="avg", default=0.0 - ), - worker_resolve_start_delay_avg=state.get_metric( - key="worker_resolve_start_delay", type_="avg", default=0.0 - ), - worker_resolve_time_avg=state.get_metric( - key="worker_resolve_time", type_="avg", default=0.0 - ), - worker_resolve_end_delay_avg=state.get_metric( - key="worker_resolve_end_delay", type_="avg", default=0.0 - ), - finalized_delay_avg=state.get_metric( - key="finalized_delay", type_="avg", default=0.0 - ), - worker_targeted_start_delay_avg=state.get_metric( - key="worker_targeted_start_delay", type_="avg", default=0.0 - ), - request_start_delay_avg=state.get_metric( - key="request_start_delay", type_="avg", default=0.0 - ), - request_time_avg=state.get_metric( - key="request_time", type_="avg", default=0.0 - ), - request_targeted_start_delay_avg=state.get_metric( - key="request_targeted_start_delay", type_="avg", default=0.0 - ), - ), - } - - -@SerializableAggregator.register("generative_stats_progress") -class GenerativeStatsProgressAggregator( - SerializableAggregator[GenerationResponse, GenerationRequest] -): - """ - Tracks generative model metrics during benchmark execution. - - Aggregates token-level metrics including time to first token, inter-token - latency, and token counts for real-time progress monitoring. - """ - - @classmethod - def validated_kwargs(cls, *_args, **_kwargs) -> dict[str, Any]: - return {} - - type_: Literal["generative_stats_progress"] = Field( - default="generative_stats_progress" - ) - - def __call__( - self, - state: AggregatorState, - response: GenerationResponse | None, - request: GenerationRequest, - request_info: ScheduledRequestInfo, - scheduler_state: SchedulerState, - ) -> dict[str, Any] | None: - """ - Aggregate generative model metrics for a completed request. - - :param agg_state: Current aggregation state to update. - :param response: Generation response with token and timing data. - :param request: The processed generation request. - :param request_info: Scheduling metadata and timing information. - :param scheduler_state: Current scheduler execution state. - :return: Updated aggregation state for progress reporting. - """ - _ = (request,) # unused - - # Request Concurrency - state.set_metric( - key="requests", - value=scheduler_state.processing_requests, - type_="avg", - ) - - if request_info.status in {"completed", "errored", "cancelled"}: - # Only compile progress stats for processed requests - state["updated_generative_stats"] = True - start_time = scheduler_state.start_time - end_time = ( - safe_getattr(request_info.request_timings, "request_end") - or request_info.scheduler_timings.resolve_end - ) - duration = end_time - start_time if end_time else None - - for prefix in (request_info.status, None): - requests_count = ( - scheduler_state.processed_requests - if prefix is None - else scheduler_state.successful_requests - if request_info.status == "completed" - else scheduler_state.cancelled_requests - if request_info.status == "cancelled" - else scheduler_state.errored_requests - ) - - # Requests per Second - if duration is not None: - state.set_metric( - key="requests", - value=safe_divide(requests_count, duration), - type_="rate", - prefix=prefix, - ) - - # Request Latency - state.add_metric( - key="request_latency", - value=safe_getattr(request_info.request_timings, "request_end"), - start_val=safe_getattr( - request_info.request_timings, "request_start" - ), - prefix=prefix, - ) - - # Time to First Token - state.add_metric( - key="time_to_first_token", - value=safe_getattr(request_info.request_timings, "first_iteration"), - start_val=safe_getattr( - request_info.request_timings, "request_start" - ), - prefix=prefix, - ) - - output_tokens = response.output_stats.value() if response else None - prompt_tokens = response.prompt_stats.value() if response else None - total_tokens = response.total_tokens() if response else None - - # Inter Token Latency - state.add_metric( - key="inter_token_latency", - value=safe_getattr(request_info.request_timings, "last_iteration"), - start_val=safe_getattr( - request_info.request_timings, "first_iteration" - ), - count=( - output_tokens - 1 - if output_tokens and output_tokens > 1 - else None - ), - prefix=prefix, - ) - - # Time per Output Token - state.add_metric( - key="time_per_output_token", - value=safe_getattr(request_info.request_timings, "request_start"), - start_val=safe_getattr( - request_info.request_timings, "last_iteration" - ), - count=output_tokens, - prefix=prefix, - ) - - # Prompt Tokens - state.add_metric( - key="prompt_tokens", - value=prompt_tokens, - duration=duration, - prefix=prefix, - ) - - # Output Tokens - state.add_metric( - key="output_tokens", - value=output_tokens, - duration=duration, - prefix=prefix, - ) - - # Total Tokens - state.add_metric( - key="total_tokens", - value=total_tokens, - duration=duration, - prefix=prefix, - ) - - return state - - def compile( - self, state: AggregatorState, scheduler_state: SchedulerState - ) -> dict[str, Any]: - """ - Compile progress metrics into final results. - - GenerativeStatsProgressAggregator is primarily for progress tracking, - so compilation returns the aggregated state as-is. - - :param agg_state: The accumulated aggregation state. - :param scheduler_state: Final scheduler execution state. - :return: The aggregated state as final results. - """ - _ = (state, scheduler_state) # unused - return {} - - -@SerializableAggregator.register("generative_requests") -class GenerativeRequestsAggregator( - SerializableAggregator[GenerationResponse, GenerationRequest], -): - """ - Compiles complete generative benchmark results with warmup/cooldown filtering. - - Aggregates request data during execution and compiles comprehensive metrics - including timing distributions, token statistics, and throughput measurements. - Supports filtering warmup and cooldown periods from final results. - """ - - @classmethod - def validated_kwargs( - cls, - sample_requests: int | None = 20, - warmup: int | float | None = None, - cooldown: int | float | None = None, - **_kwargs, - ) -> dict[str, Any]: - return { - "sample_requests": sample_requests, - "warmup": warmup, - "cooldown": cooldown, - } - - type_: Literal["generative_requests"] = Field(default="generative_requests") - - sample_requests: int | None = Field(default=20, description="") - warmup: int | float | None = Field( - default=None, - description="Number of warmup requests to ignore at benchmark start", - ) - cooldown: int | float | None = Field( - default=None, - description="Number of cooldown requests to ignore at benchmark end", - ) - _in_cooldown: bool = PrivateAttr(False) - _in_warmup: bool = PrivateAttr(False) - - def __call__( - self, - state: AggregatorState, - response: GenerationResponse | None, - request: GenerationRequest, - request_info: ScheduledRequestInfo, - scheduler_state: SchedulerState, - ) -> dict[str, Any] | None: - """ - Collect completed requests for final compilation. - - Filters requests based on warmup/cooldown settings and categorizes by - completion status for comprehensive benchmark analysis. - - :param agg_state: Current aggregation state to update. - :param response: Generation response data. - :param request: The processed generation request. - :param request_info: Scheduling metadata and timing information. - :param scheduler_state: Current scheduler execution state. - :return: None, as this aggregator only collects for final compilation. - """ - # Skip invalid requests - if request_info.status not in {"completed", "canceled", "errored"} or ( - request_info.status == "canceled" - and safe_getattr(request_info.scheduler_timings, "resolve_start") is None - # Canceled requests that never started should not be kept - ): - return None - - status = { - "updated_generative_requests": True, - "requests_in_warmup": False, - "requests_in_cooldown": False, - } - - if self._is_in_warmup(request_info, scheduler_state): - status["requests_in_warmup"] = True - return status - - if self._is_in_cooldown(request_info, scheduler_state): - status["requests_in_cooldown"] = True - return status - - if "completed" not in state: - state["completed"] = [] - state["errored"] = [] - state["incomplete"] = [] - - # Categorize request by status - if request_info.status == "completed": - state["completed"].append((response, request, request_info)) - elif request_info.status == "canceled": - state["incomplete"].append((response, request, request_info)) - else: - state["errored"].append((response, request, request_info)) - - return status - - def compile( - self, - state: AggregatorState, - scheduler_state: SchedulerState, # noqa: ARG002 - ) -> dict[str, Any]: - """ - Compile aggregated requests into comprehensive benchmark results. - - Transforms collected request data into detailed metrics including timing - distributions, token statistics, throughput measurements, and status breakdowns. - - :param agg_state: Accumulated request data categorized by completion status. - :param scheduler_state: Final scheduler execution state. - :return: Complete benchmark results with metrics and request statistics. - """ - successful: list[GenerativeRequestStats] = [ - self._create_generative_request_stats(response, request, request_info) - for (response, request, request_info) in state.get("completed", []) - ] - incomplete: list[GenerativeRequestStats] = [ - self._create_generative_request_stats(response, request, request_info) - for (response, request, request_info) in state.get("incomplete", []) - ] - errored: list[GenerativeRequestStats] = [ - self._create_generative_request_stats(response, request, request_info) - for (response, request, request_info) in state.get("errored", []) - ] - - # Use all requests for metrics calculations (not sampled) - total: list[GenerativeRequestStats] = successful + incomplete + errored - total_types: list[Literal["successful", "incomplete", "error"]] = [ - *["successful"] * len(successful), - *["incomplete"] * len(incomplete), - *["error"] * len(errored), - ] - start_time = min( - [math.inf] - + [ - req.scheduler_info.request_timings.request_start - for req in total - if req.scheduler_info.request_timings.request_start is not None - ] - ) - end_time = max( - [-1 * math.inf] - + [ - req.scheduler_info.request_timings.request_end - for req in total - if req.scheduler_info.request_timings.request_end is not None - ] - ) - - return { - "start_time": start_time, - "end_time": end_time, - "request_totals": StatusBreakdown[int, int, int, int]( - successful=len(successful), - incomplete=len(incomplete), - errored=len(errored), - total=len(total), - ), - "requests": StatusBreakdown[ - list[GenerativeRequestStats], - list[GenerativeRequestStats], - list[GenerativeRequestStats], - list[GenerativeRequestStats], - ]( - successful=self._sample_request_stats(successful, self.sample_requests), - incomplete=self._sample_request_stats(incomplete, self.sample_requests), - errored=self._sample_request_stats(errored, self.sample_requests), - ), - "metrics": GenerativeMetrics( - requests_per_second=self._calculate_requests_per_second( - statuses=total_types, requests=total - ), - request_concurrency=self._calculate_request_concurrency( - statuses=total_types, requests=total - ), - request_latency=self._calculate_request_latency( - statuses=total_types, requests=total - ), - prompt_token_count=self._calculate_prompt_token_count( - statuses=total_types, requests=total - ), - output_token_count=self._calculate_output_token_count( - statuses=total_types, requests=total - ), - total_token_count=self._calculate_total_token_count( - statuses=total_types, requests=total - ), - time_to_first_token_ms=self._calculate_time_to_first_token_ms( - statuses=total_types, requests=total - ), - time_per_output_token_ms=self._calculate_time_per_output_token_ms( - statuses=total_types, requests=total - ), - inter_token_latency_ms=self._calculate_inter_token_latency_ms( - statuses=total_types, requests=total - ), - output_tokens_per_second=self._calculate_output_tokens_per_second( - statuses=total_types, requests=total - ), - tokens_per_second=self._calculate_tokens_per_second( - statuses=total_types, requests=total - ), - ), - } - - def _is_in_warmup( - self, - request_info: ScheduledRequestInfo, - scheduler_state: SchedulerState, - ) -> bool: - """Check if the current request is within the warmup period.""" - if self.warmup is None: - return False - - if 0 < self.warmup < 1: # Percentage-based warmup - return ( - scheduler_state.remaining_fraction is not None - and scheduler_state.remaining_fraction > (1 - self.warmup) - ) - - if self.warmup >= 1: # Count/time-based warmup - if scheduler_state.processed_requests < self.warmup: - return True - - current_time = request_info.scheduler_timings.targeted_start - return ( - current_time is not None - and (current_time - scheduler_state.start_time) < self.warmup - ) - - return False - - def _is_in_cooldown( - self, - request_info: ScheduledRequestInfo, - scheduler_state: SchedulerState, - ) -> bool: - """Check if the current request is within the cooldown period.""" - if self.cooldown is None: - return False - - if 0 < self.cooldown < 1: # Percentage-based cooldown - return ( - scheduler_state.remaining_fraction is not None - and scheduler_state.remaining_fraction < self.cooldown - ) - - if self.cooldown >= 1: # Count/time-based cooldown - if scheduler_state.remaining_requests <= self.cooldown: - return True - - current_time = ( - request_info.scheduler_timings.resolve_end - or request_info.scheduler_timings.targeted_start - ) - return ( - current_time is not None - and scheduler_state.remaining_duration is not None - and scheduler_state.remaining_duration < self.cooldown - ) - - return False - - @classmethod - def _create_generative_request_stats( - cls, - response: GenerationResponse | None, - request: GenerationRequest, - request_info: ScheduledRequestInfo, - ) -> GenerativeRequestStats: - return GenerativeRequestStats( - request_id=request.request_id, - request_type=request.request_type, - request_args=str(request.arguments), - output=response.text if response else None, - iterations=response.iterations if response else 0, - prompt_tokens=( - response.prompt_stats.value(settings.preferred_prompt_tokens_source) - if response - else None - ), - output_tokens=( - response.output_stats.value(settings.preferred_output_tokens_source) - if response - else None - ), - total_tokens=( - response.total_tokens(settings.preferred_output_tokens_source) - if response - else None - ), - scheduler_info=request_info, - ) - - @classmethod - def _sample_request_stats( - cls, stats: list[GenerativeRequestStats], sample_size: int | None - ) -> list[GenerativeRequestStats]: - if sample_size is None or sample_size <= 0 or not stats: - return stats - - return random.sample(stats, min(sample_size, len(stats))) - - @classmethod - def _calculate_requests_per_second( - cls, - statuses: list[Literal["successful", "incomplete", "error"]], - requests: list[GenerativeRequestStats], - ) -> StatusDistributionSummary: - filtered_statuses = [] - filtered_times = [] - - for status, request in zip(statuses, requests): - if not all_defined( - safe_getattr(request.scheduler_info.request_timings, "request_start"), - safe_getattr(request.scheduler_info.request_timings, "request_end"), - ): - continue - - filtered_statuses.append(status) - filtered_times.append( - ( - request.scheduler_info.request_timings.request_start, - request.scheduler_info.request_timings.request_end, - ) - ) - - return StatusDistributionSummary.from_request_times( - request_types=filtered_statuses, - requests=filtered_times, - distribution_type="rate", - ) - - @classmethod - def _calculate_request_concurrency( - cls, - statuses: list[Literal["successful", "incomplete", "error"]], - requests: list[GenerativeRequestStats], - ) -> StatusDistributionSummary: - filtered_statuses = [] - filtered_times = [] - - for status, request in zip(statuses, requests): - if not all_defined( - safe_getattr(request.scheduler_info.request_timings, "request_start"), - safe_getattr(request.scheduler_info.request_timings, "request_end"), - ): - continue - - filtered_statuses.append(status) - filtered_times.append( - ( - request.scheduler_info.request_timings.request_start, - request.scheduler_info.request_timings.request_end, - ) - ) - - return StatusDistributionSummary.from_request_times( - request_types=filtered_statuses, - requests=filtered_times, - distribution_type="concurrency", - ) - - @classmethod - def _calculate_request_latency( - cls, - statuses: list[Literal["successful", "incomplete", "error"]], - requests: list[GenerativeRequestStats], - ) -> StatusDistributionSummary: - filtered_statuses = [] - filtered_values = [] - - for status, request in zip(statuses, requests): - if not all_defined(request.request_latency): - continue - - filtered_statuses.append(status) - filtered_values.append(request.request_latency) - - return StatusDistributionSummary.from_values( - value_types=filtered_statuses, - values=filtered_values, - ) - - @classmethod - def _calculate_prompt_token_count( - cls, - statuses: list[Literal["successful", "incomplete", "error"]], - requests: list[GenerativeRequestStats], - ) -> StatusDistributionSummary: - filtered_statuses = [] - filtered_values = [] - - for status, request in zip(statuses, requests): - if not all_defined(request.prompt_tokens): - continue - - filtered_statuses.append(status) - filtered_values.append(request.prompt_tokens) - - return StatusDistributionSummary.from_values( - value_types=filtered_statuses, - values=filtered_values, - ) - - @classmethod - def _calculate_output_token_count( - cls, - statuses: list[Literal["successful", "incomplete", "error"]], - requests: list[GenerativeRequestStats], - ) -> StatusDistributionSummary: - filtered_statuses = [] - filtered_values = [] - - for status, request in zip(statuses, requests): - if not all_defined(request.output_tokens): - continue - - filtered_statuses.append(status) - filtered_values.append(request.output_tokens) - - return StatusDistributionSummary.from_values( - value_types=filtered_statuses, - values=filtered_values, - ) - - @classmethod - def _calculate_total_token_count( - cls, - statuses: list[Literal["successful", "incomplete", "error"]], - requests: list[GenerativeRequestStats], - ) -> StatusDistributionSummary: - filtered_statuses = [] - filtered_values = [] - - for status, request in zip(statuses, requests): - if not all_defined(request.total_tokens): - continue - - filtered_statuses.append(status) - filtered_values.append(request.total_tokens) - - return StatusDistributionSummary.from_values( - value_types=filtered_statuses, - values=filtered_values, - ) - - @classmethod - def _calculate_time_to_first_token_ms( - cls, - statuses: list[Literal["successful", "incomplete", "error"]], - requests: list[GenerativeRequestStats], - ) -> StatusDistributionSummary: - filtered_statuses = [] - filtered_values = [] - - for status, request in zip(statuses, requests): - if not all_defined(request.time_to_first_token_ms): - continue - - filtered_statuses.append(status) - filtered_values.append(request.time_to_first_token_ms) - - return StatusDistributionSummary.from_values( - value_types=filtered_statuses, - values=filtered_values, - ) - - @classmethod - def _calculate_time_per_output_token_ms( - cls, - statuses: list[Literal["successful", "incomplete", "error"]], - requests: list[GenerativeRequestStats], - ) -> StatusDistributionSummary: - filtered_statuses = [] - filtered_values = [] - filtered_weights = [] - - for status, request in zip(statuses, requests): - if not all_defined(request.time_to_first_token_ms): - continue - - # Add time to first token separately to better reflect in distribution - filtered_statuses.append(status) - filtered_values.append(request.time_to_first_token_ms) - filtered_weights.append(1) - - if not all_defined(request.inter_token_latency_ms): - continue - - # Add tokens after the first token to get the full distribution - filtered_statuses.append(status) - filtered_values.append(request.inter_token_latency_ms) - filtered_weights.append(request.output_tokens - 1) - - return StatusDistributionSummary.from_values( - value_types=filtered_statuses, - values=filtered_values, - weights=filtered_weights, - ) - - @classmethod - def _calculate_inter_token_latency_ms( - cls, - statuses: list[Literal["successful", "incomplete", "error"]], - requests: list[GenerativeRequestStats], - ) -> StatusDistributionSummary: - filtered_statuses = [] - filtered_values = [] - filtered_weights = [] - - for status, request in zip(statuses, requests): - if not all_defined(request.inter_token_latency_ms): - continue - - filtered_statuses.append(status) - filtered_values.append(request.inter_token_latency_ms) - filtered_weights.append(request.output_tokens - 1) - - return StatusDistributionSummary.from_values( - value_types=filtered_statuses, - values=filtered_values, - weights=filtered_weights, - ) - - @classmethod - def _calculate_output_tokens_per_second( - cls, - statuses: list[Literal["successful", "incomplete", "error"]], - requests: list[GenerativeRequestStats], - ) -> StatusDistributionSummary: - filtered_statuses = [] - filtered_request_times = [] - filtered_first_iter_times = [] - filtered_iter_counts = [] - - for status, request in zip(statuses, requests): - if not all_defined(request.output_tokens_per_second): - continue - - filtered_statuses.append(status) - filtered_request_times.append( - ( - request.scheduler_info.request_timings.request_start, - request.scheduler_info.request_timings.request_end, - ) - ) - filtered_first_iter_times.append( - request.scheduler_info.request_timings.first_iteration - ) - filtered_iter_counts.append(request.output_tokens) - - return StatusDistributionSummary.from_iterable_request_times( - request_types=filtered_statuses, - requests=filtered_request_times, - first_iter_times=filtered_first_iter_times, - iter_counts=filtered_iter_counts, - ) - - @classmethod - def _calculate_tokens_per_second( - cls, - statuses: list[Literal["successful", "incomplete", "error"]], - requests: list[GenerativeRequestStats], - ) -> StatusDistributionSummary: - filtered_statuses = [] - filtered_request_times = [] - filtered_first_iter_times = [] - filtered_iter_counts = [] - filtered_first_iter_counts = [] - - for status, request in zip(statuses, requests): - if not all_defined(request.tokens_per_second): - continue - - filtered_statuses.append(status) - filtered_request_times.append( - ( - request.scheduler_info.request_timings.request_start, - request.scheduler_info.request_timings.request_end, - ) - ) - filtered_first_iter_times.append( - request.scheduler_info.request_timings.first_iteration - ) - filtered_iter_counts.append(request.output_tokens - 1) - filtered_first_iter_counts.append(request.prompt_tokens + 1) - - return StatusDistributionSummary.from_iterable_request_times( - request_types=filtered_statuses, - requests=filtered_request_times, - first_iter_times=filtered_first_iter_times, - iter_counts=filtered_iter_counts, - first_iter_counts=filtered_first_iter_counts, - ) diff --git a/src/guidellm/benchmark/benchmarker.py b/src/guidellm/benchmark/benchmarker.py index ae591c23..fd8c1aa8 100644 --- a/src/guidellm/benchmark/benchmarker.py +++ b/src/guidellm/benchmark/benchmarker.py @@ -20,21 +20,16 @@ import uuid from abc import ABC from collections.abc import AsyncIterator, Iterable -from typing import ( - Any, - Generic, -) +from typing import Generic -from guidellm.benchmark.aggregator import ( - Aggregator, - AggregatorState, - CompilableAggregator, -) -from guidellm.benchmark.objects import BenchmarkerDict, BenchmarkT, SchedulerDict from guidellm.benchmark.profile import Profile +from guidellm.benchmark.schemas import ( + BenchmarkArgs, + BenchmarkT, + EstimatedBenchmarkState, +) from guidellm.scheduler import ( BackendInterface, - Constraint, Environment, NonDistributedEnvironment, RequestT, @@ -43,8 +38,7 @@ SchedulerState, SchedulingStrategy, ) -from guidellm.utils import InfoMixin, ThreadSafeSingletonMixin -from guidellm.utils.pydantic_utils import StandardBaseDict +from guidellm.utils import ThreadSafeSingletonMixin __all__ = ["Benchmarker"] @@ -67,18 +61,18 @@ class Benchmarker( async def run( self, + benchmark_class: type[BenchmarkT], requests: Iterable[RequestT | Iterable[RequestT | tuple[RequestT, float]]], backend: BackendInterface[RequestT, ResponseT], profile: Profile, - benchmark_class: type[BenchmarkT], - benchmark_aggregators: dict[ - str, - Aggregator[ResponseT, RequestT] | CompilableAggregator[ResponseT, RequestT], - ], environment: Environment | None = None, + sample_requests: int | None = 20, + warmup: float | None = None, + cooldown: float | None = None, + prefer_response_metrics: bool = True, ) -> AsyncIterator[ tuple[ - AggregatorState | None, + EstimatedBenchmarkState | None, BenchmarkT | None, SchedulingStrategy, SchedulerState | None, @@ -110,9 +104,16 @@ async def run( while strategy is not None: yield None, None, strategy, None - aggregators_state = { - key: AggregatorState() for key in benchmark_aggregators - } + args = BenchmarkArgs( + run_id=run_id, + run_index=len(profile.completed_strategies), + sample_requests=sample_requests, + warmup=warmup, + cooldown=cooldown, + prefer_response_metrics=prefer_response_metrics, + ) + estimated_state = EstimatedBenchmarkState() + scheduler_state = None async for ( response, @@ -126,33 +127,30 @@ async def run( env=environment, **constraints, ): - aggregators_update = AggregatorState() - for key, aggregator in benchmark_aggregators.items(): - update = aggregator( - aggregators_state[key], - response, - request, - request_info, - scheduler_state, - ) - if update: - aggregators_update.update(update) - yield aggregators_update, None, strategy, scheduler_state + benchmark_class.update_estimate( + args, + estimated_state, + response, + request, + request_info, + scheduler_state, + ) + yield estimated_state, None, strategy, scheduler_state - benchmark_kwargs = self._compile_benchmark_kwargs( - run_id=run_id, - run_index=len(profile.completed_strategies), + if scheduler_state is None: + raise RuntimeError("Scheduler state is None after execution.") + + benchmark = benchmark_class.compile( + args=args, + estimated_state=estimated_state, + scheduler_state=scheduler_state, profile=profile, requests=requests, backend=backend, environment=environment, - aggregators=benchmark_aggregators, - aggregators_state=aggregators_state, strategy=strategy, constraints=constraints, - scheduler_state=scheduler_state, ) - benchmark = benchmark_class(**benchmark_kwargs) yield None, benchmark, strategy, None try: @@ -160,107 +158,3 @@ async def run( except StopIteration: strategy = None constraints = None - - @classmethod - def _compile_benchmark_kwargs( - cls, - run_id: str, - run_index: int, - profile: Profile, - requests: Iterable[RequestT | Iterable[RequestT | tuple[RequestT, float]]], - backend: BackendInterface[RequestT, ResponseT], - environment: Environment, - aggregators: dict[ - str, - Aggregator[ResponseT, RequestT] | CompilableAggregator[ResponseT, RequestT], - ], - aggregators_state: dict[str, dict[str, Any]], - strategy: SchedulingStrategy, - constraints: dict[str, Any | dict[str, Any] | Constraint], - scheduler_state: SchedulerState | None, - ) -> dict[str, Any]: - """ - Compile benchmark construction parameters from execution results. - - Aggregates metadata from scheduler execution and compiles it into - structured parameters for benchmark object construction. - - :param run_id: Unique identifier for the benchmark run. - :param run_index: Index of this strategy in the benchmark profile. - :param profile: Benchmark profile containing strategy configuration. - :param requests: Request datasets used for the benchmark. - :param backend: Backend interface used for request processing. - :param environment: Execution environment for coordination. - :param aggregators: Metric aggregation functions by name. - :param aggregators_state: Current state of metric aggregators. - :param strategy: Scheduling strategy that was executed. - :param constraints: Runtime constraints applied during execution. - :param scheduler_state: Final state of scheduler execution. - :return: Dictionary of parameters for benchmark object construction. - :raises ValueError: If aggregator output conflicts with existing keys. - """ - benchmark_kwargs = { - "run_id": run_id, - "run_index": run_index, - "scheduler": SchedulerDict( - strategy=strategy, - constraints={ - key: InfoMixin.extract_from_obj(val) - for key, val in constraints.items() - }, - state=scheduler_state, - ), - "benchmarker": BenchmarkerDict( - profile=profile, - requests=InfoMixin.extract_from_obj(requests), - backend=backend.info, - environment=environment.info, - aggregators={ - key: InfoMixin.extract_from_obj(aggregator) - for key, aggregator in aggregators.items() - }, - ), - "env_args": StandardBaseDict(), - "extras": StandardBaseDict(), - } - - def _combine( - existing: dict[str, Any] | StandardBaseDict, - addition: dict[str, Any] | StandardBaseDict, - ) -> dict[str, Any] | StandardBaseDict: - if not isinstance(existing, (dict, StandardBaseDict)): - raise ValueError( - f"Existing value {existing} (type: {type(existing).__name__}) " - f"is not a valid type for merging." - ) - if not isinstance(addition, (dict, StandardBaseDict)): - raise ValueError( - f"Addition value {addition} (type: {type(addition).__name__}) " - f"is not a valid type for merging." - ) - - add_kwargs = ( - addition if isinstance(addition, dict) else addition.model_dump() - ) - - if isinstance(existing, dict): - return {**add_kwargs, **existing} - - return existing.__class__(**{**add_kwargs, **existing.model_dump()}) - - for key, aggregator in aggregators.items(): - if not isinstance(aggregator, CompilableAggregator): - continue - - compiled = aggregator.compile(aggregators_state[key], scheduler_state) - - for field_name, field_val in compiled.items(): - if field_name in benchmark_kwargs: - # If the key already exists, merge the values - benchmark_kwargs[field_name] = _combine( - benchmark_kwargs[field_name], field_val - ) - else: - benchmark_kwargs[field_name] = field_val - - return benchmark_kwargs diff --git a/src/guidellm/benchmark/entrypoints.py b/src/guidellm/benchmark/entrypoints.py index e400907a..a94c6282 100644 --- a/src/guidellm/benchmark/entrypoints.py +++ b/src/guidellm/benchmark/entrypoints.py @@ -4,35 +4,17 @@ from typing import Any, Callable, Literal from torch.utils.data import Sampler -from transformers import ( # type: ignore[import] - PreTrainedTokenizerBase, -) +from transformers import PreTrainedTokenizerBase # type: ignore[import] -from guidellm.backends import ( - Backend, - BackendType, - GenerationRequest, - GenerationResponse, -) -from guidellm.benchmark.aggregator import ( - Aggregator, - CompilableAggregator, - GenerativeRequestsAggregator, - GenerativeStatsProgressAggregator, - SchedulerStatsAggregator, - SerializableAggregator, -) +from guidellm.backends import Backend, BackendType from guidellm.benchmark.benchmarker import Benchmarker -from guidellm.benchmark.objects import GenerativeBenchmark, GenerativeBenchmarksReport from guidellm.benchmark.output import ( GenerativeBenchmarkerConsole, GenerativeBenchmarkerOutput, ) from guidellm.benchmark.profile import Profile, ProfileType -from guidellm.benchmark.progress import ( - BenchmarkerProgress, - BenchmarkerProgressGroup, -) +from guidellm.benchmark.progress import BenchmarkerProgress, BenchmarkerProgressGroup +from guidellm.benchmark.schemas import GenerativeBenchmark, GenerativeBenchmarksReport from guidellm.data import ( DataLoader, DatasetPreprocessor, @@ -46,6 +28,7 @@ NonDistributedEnvironment, StrategyType, ) +from guidellm.schemas import GenerationRequest, GenerationResponse from guidellm.utils import Console, InfoMixin __all__ = [ @@ -96,13 +79,11 @@ async def benchmark_generative_text( # noqa: C901, PLR0915, PLR0912 # Updates configuration progress: tuple[str, ...] | list[str] | list[BenchmarkerProgress] | None = None, print_updates: bool = False, - # Aggregators configuration - add_aggregators: ( - dict[str, str | dict[str, Any] | Aggregator | CompilableAggregator] | None - ) = None, + # Benchmarker configuration + benchmark_cls: type[GenerativeBenchmark] = GenerativeBenchmark, + sample_requests: int | None = 10, warmup: float | None = None, cooldown: float | None = None, - sample_requests: int | None = 10, # Constraints configuration max_seconds: int | float | None = None, max_requests: int | None = None, @@ -241,25 +222,6 @@ async def benchmark_generative_text( # noqa: C901, PLR0915, PLR0912 status_level="success", ) - with console.print_update_step( - title="Creating benchmark aggregators" - ) as console_step: - aggregators = { - "scheduler_stats": SchedulerStatsAggregator(), - "requests_progress": GenerativeStatsProgressAggregator(), - "requests": GenerativeRequestsAggregator( - request_samples=sample_requests, - warmup=warmup, - cooldown=cooldown, - ), - **SerializableAggregator.resolve(add_aggregators or {}), - } - console_step.finish( - title="Benchmark aggregators created", - details={key: str(val) for key, val in aggregators.items()}, - status_level="success", - ) - with console.print_update_step(title="Resolving output formats") as console_step: output_formats = GenerativeBenchmarkerOutput.resolve( output_formats=(output_formats or {}), output_path=output_path @@ -291,12 +253,15 @@ async def benchmark_generative_text( # noqa: C901, PLR0915, PLR0912 GenerationRequest, GenerationResponse, ]().run( + benchmark_class=benchmark_cls, requests=request_loader, backend=backend, profile=profile, environment=NonDistributedEnvironment(), - benchmark_aggregators=aggregators, - benchmark_class=GenerativeBenchmark, + sample_requests=sample_requests, + warmup=warmup, + cooldown=cooldown, + prefer_response_metrics=True, ), ): if benchmark: diff --git a/src/guidellm/benchmark/objects.py b/src/guidellm/benchmark/objects.py deleted file mode 100644 index c3481303..00000000 --- a/src/guidellm/benchmark/objects.py +++ /dev/null @@ -1,475 +0,0 @@ -""" -Benchmark data models and metrics for performance measurement and analysis. - -Provides comprehensive data structures for capturing, storing, and analyzing -benchmark results from scheduler executions. Includes timing measurements, -token statistics, and performance metrics for generative AI workloads. - -Classes: - BenchmarkSchedulerStats: Scheduler timing and performance statistics. - BenchmarkMetrics: Core benchmark metrics and distributions. - BenchmarkRequestStats: Individual request processing statistics. - Benchmark: Base benchmark result container with generic metrics. - GenerativeRequestStats: Request statistics for generative AI workloads. - GenerativeMetrics: Comprehensive metrics for generative benchmarks. - GenerativeBenchmark: Complete generative benchmark results and analysis. - GenerativeBenchmarksReport: Container for multiple benchmark results. - -Type Variables: - BenchmarkMetricsT: Generic benchmark metrics type. - BenchmarkRequestStatsT: Generic request statistics type. - BenchmarkT: Generic benchmark container type. -""" - -from __future__ import annotations - -import json -import uuid -from pathlib import Path -from typing import Any, ClassVar, Generic, Literal, TypeVar - -import yaml -from pydantic import Field, computed_field - -from guidellm.benchmark.profile import ( - Profile, -) -from guidellm.data import ( - GenerativeRequestType, -) -from guidellm.scheduler import ( - ScheduledRequestInfo, - SchedulerState, - SchedulingStrategy, -) -from guidellm.utils import ( - StandardBaseDict, - StandardBaseModel, - StatusBreakdown, - StatusDistributionSummary, -) - -__all__ = [ - "Benchmark", - "BenchmarkMetrics", - "BenchmarkSchedulerStats", - "BenchmarkT", - "GenerativeBenchmark", - "GenerativeBenchmarksReport", - "GenerativeMetrics", - "GenerativeRequestStats", -] - - -class BenchmarkSchedulerStats(StandardBaseDict): - """Scheduler timing and performance statistics.""" - - start_time: float = Field( - description="Unix timestamp when the benchmark run started" - ) - end_time: float = Field(description="Unix timestamp when the benchmark run ended") - requests_made: StatusBreakdown[int, int, int, int] = Field( - description="Request counts by status: successful, incomplete, errored, total" - ) - queued_time_avg: float = Field( - description="Avg time requests spent in the queue (seconds)" - ) - worker_resolve_start_delay_avg: float = Field( - description="Avg delay before worker begins resolving req after dequeue (sec)" - ) - worker_resolve_time_avg: float = Field( - description="Avg time for worker to resolve requests (seconds)" - ) - worker_resolve_end_delay_avg: float = Field( - description="Avg delay after request end till worker resolves (seconds)" - ) - finalized_delay_avg: float = Field( - description="Avg delay after resolve til finalized with in scheduler (sec)" - ) - worker_targeted_start_delay_avg: float = Field( - description="Avg delay from targeted start to actual worker start (seconds)" - ) - request_start_delay_avg: float = Field( - description="Avg delay after resolve til request start (seconds)" - ) - request_time_avg: float = Field(description="Avg request processing time (seconds)") - request_targeted_start_delay_avg: float = Field( - description="Avg delay from targeted start to actual request start" - ) - - -class SchedulerDict(StandardBaseDict): - """Scheduler configuration and execution state dictionary.""" - - strategy: SchedulingStrategy - constraints: dict[str, dict[str, Any]] - state: SchedulerState - - -class BenchmarkerDict(StandardBaseDict): - """Benchmarker configuration and component settings dictionary.""" - - profile: Profile - requests: dict[str, Any] - backend: dict[str, Any] - environment: dict[str, Any] - aggregators: dict[str, dict[str, Any]] - - -class BenchmarkMetrics(StandardBaseDict): - """Core benchmark metrics and statistical distributions.""" - - requests_per_second: StatusDistributionSummary = Field( - description="Distribution of requests per second across benchmark execution" - ) - request_concurrency: StatusDistributionSummary = Field( - description="Distribution of concurrent request counts during execution" - ) - request_latency: StatusDistributionSummary = Field( - description="Distribution of request latencies for completed requests" - ) - - -BenchmarkMetricsT = TypeVar("BenchmarkMetricsT", bound=BenchmarkMetrics) - - -class BenchmarkRequestStats(StandardBaseDict): - """Individual request processing statistics and scheduling metadata.""" - - scheduler_info: ScheduledRequestInfo = Field( - description="Scheduler metadata and timing information for the request" - ) - - -BenchmarkRequestStatsT = TypeVar("BenchmarkRequestStatsT", bound=BenchmarkRequestStats) - - -class Benchmark(StandardBaseDict, Generic[BenchmarkMetricsT, BenchmarkRequestStatsT]): - """Base benchmark result container with execution metadata.""" - - type_: Literal["benchmark"] = "benchmark" - id_: str = Field( - default_factory=lambda: str(uuid.uuid4()), - description="Unique identifier for this benchmark execution", - ) - run_id: str = Field( - description="Identifier for the benchmarker run containing this benchmark" - ) - run_index: int = Field( - description="Sequential index of this benchmark within the benchmarker run" - ) - scheduler: SchedulerDict = Field( - description="Scheduler configuration and execution state" - ) - benchmarker: BenchmarkerDict = Field( - description="Benchmarker configuration and component settings" - ) - env_args: StandardBaseDict = Field( - description="Environment arguments and runtime configuration" - ) - extras: StandardBaseDict = Field( - description="Additional metadata and custom benchmark parameters" - ) - run_stats: BenchmarkSchedulerStats = Field( - description="Scheduler timing and performance statistics" - ) - start_time: float = Field( - default=-1.0, description="Unix timestamp when the first request was initiated" - ) - end_time: float = Field( - default=-1.0, description="Unix timestamp when the last request completed" - ) - - @computed_field # type: ignore[misc] - @property - def duration(self) -> float: - """ - Benchmark execution duration in seconds. - - :return: Time elapsed from first request start to last request completion. - """ - return self.end_time - self.start_time - - metrics: BenchmarkMetricsT = Field( - description="Performance metrics and statistical distributions" - ) - request_totals: StatusBreakdown[int, int, int, int] = Field( - description="Request counts by status: successful, incomplete, errored, total" - ) - requests: StatusBreakdown[ - list[BenchmarkRequestStatsT], - list[BenchmarkRequestStatsT], - list[BenchmarkRequestStatsT], - None, - ] = Field( - description="Request details grouped by status: successful, incomplete, errored" - ) - - -BenchmarkT = TypeVar("BenchmarkT", bound=Benchmark) - - -class GenerativeRequestStats(BenchmarkRequestStats): - """Request statistics for generative AI text generation workloads.""" - - type_: Literal["generative_request_stats"] = "generative_request_stats" - request_id: str = Field(description="Unique identifier for the request") - request_type: GenerativeRequestType | str = Field( - description="Type of generative request: text or chat completion" - ) - request_args: str | None = Field( - default=None, description="Arguments passed to the backend for this request" - ) - output: str | None = Field( - description="Generated text output, if request completed successfully" - ) - iterations: int = Field( - description="Number of processing iterations for the request" - ) - prompt_tokens: int | None = Field( - description="Number of tokens in the input prompt" - ) - output_tokens: int | None = Field( - description="Number of tokens in the generated output" - ) - - @computed_field # type: ignore[misc] - @property - def total_tokens(self) -> int | None: - """ - Total token count including prompt and output tokens. - - :return: Sum of prompt and output tokens, or None if either is unavailable. - """ - if self.prompt_tokens is None and self.output_tokens is None: - return None - - return (self.prompt_tokens or 0) + (self.output_tokens or 0) - - @computed_field # type: ignore[misc] - @property - def request_latency(self) -> float | None: - """ - End-to-end request processing latency in seconds. - - :return: Duration from request start to completion, or None if unavailable. - """ - if ( - not self.scheduler_info.request_timings.request_end - or not self.scheduler_info.request_timings.request_start - ): - return None - - return ( - self.scheduler_info.request_timings.request_end - - self.scheduler_info.request_timings.request_start - ) - - @computed_field # type: ignore[misc] - @property - def time_to_first_token_ms(self) -> float | None: - """ - Time to first token generation in milliseconds. - - :return: Latency from request start to first token, or None if unavailable. - """ - if ( - not self.scheduler_info.request_timings.first_iteration - or not self.scheduler_info.request_timings.request_start - ): - return None - - return 1000 * ( - self.scheduler_info.request_timings.first_iteration - - self.scheduler_info.request_timings.request_start - ) - - @computed_field # type: ignore[misc] - @property - def time_per_output_token_ms(self) -> float | None: - """ - Average time per output token in milliseconds. - - Includes time for first token and all subsequent tokens. - - :return: Average milliseconds per output token, or None if unavailable. - """ - if ( - not self.scheduler_info.request_timings.request_start - or not self.scheduler_info.request_timings.last_iteration - or not self.output_tokens - ): - return None - - return ( - 1000 - * ( - self.scheduler_info.request_timings.last_iteration - - self.scheduler_info.request_timings.request_start - ) - / self.output_tokens - ) - - @computed_field # type: ignore[misc] - @property - def inter_token_latency_ms(self) -> float | None: - """ - Average inter-token latency in milliseconds. - - Measures time between token generations, excluding first token. - - :return: Average milliseconds between tokens, or None if unavailable. - """ - if ( - not self.scheduler_info.request_timings.first_iteration - or not self.scheduler_info.request_timings.last_iteration - or not self.output_tokens - or self.output_tokens <= 1 - ): - return None - - return ( - 1000 - * ( - self.scheduler_info.request_timings.last_iteration - - self.scheduler_info.request_timings.first_iteration - ) - / (self.output_tokens - 1) - ) - - @computed_field # type: ignore[misc] - @property - def tokens_per_second(self) -> float | None: - """ - Overall token throughput including prompt and output tokens. - - :return: Total tokens per second, or None if unavailable. - """ - if not (latency := self.request_latency) or not (tokens := self.total_tokens): - return None - - return tokens / latency - - @computed_field # type: ignore[misc] - @property - def output_tokens_per_second(self) -> float | None: - """ - Output token generation throughput. - - :return: Output tokens per second, or None if unavailable. - """ - if not (latency := self.request_latency) or not self.output_tokens: - return None - - return self.output_tokens / latency - - -class GenerativeMetrics(BenchmarkMetrics): - """Comprehensive metrics for generative AI benchmarks.""" - - prompt_token_count: StatusDistributionSummary = Field( - description="Distribution of prompt token counts by request status" - ) - output_token_count: StatusDistributionSummary = Field( - description="Distribution of output token counts by request status" - ) - total_token_count: StatusDistributionSummary = Field( - description="Distribution of total token counts by request status" - ) - time_to_first_token_ms: StatusDistributionSummary = Field( - description="Distribution of first token latencies in milliseconds" - ) - time_per_output_token_ms: StatusDistributionSummary = Field( - description="Distribution of average time per output token in milliseconds" - ) - inter_token_latency_ms: StatusDistributionSummary = Field( - description="Distribution of inter-token latencies in milliseconds" - ) - output_tokens_per_second: StatusDistributionSummary = Field( - description="Distribution of output token generation rates" - ) - tokens_per_second: StatusDistributionSummary = Field( - description="Distribution of total token throughput including prompt and output" - ) - - -class GenerativeBenchmark(Benchmark[GenerativeMetrics, GenerativeRequestStats]): - """Complete generative AI benchmark results with specialized metrics.""" - - type_: Literal["generative_benchmark"] = "generative_benchmark" # type: ignore[assignment] - - -class GenerativeBenchmarksReport(StandardBaseModel): - """Container for multiple benchmark results with load/save functionality.""" - - DEFAULT_FILE: ClassVar[str] = "benchmarks.json" - - @staticmethod - def load_file( - path: str | Path, type_: Literal["json", "yaml"] | None = None - ) -> GenerativeBenchmarksReport: - """ - Load a report from a file. - - :param path: The path to load the report from. - :param type_: File type override, auto-detected from extension if None. - :return: The loaded report. - :raises ValueError: If file type is unsupported. - """ - path = Path(path) if not isinstance(path, Path) else path - - if path.is_dir(): - path = path / GenerativeBenchmarksReport.DEFAULT_FILE - - path.parent.mkdir(parents=True, exist_ok=True) - path_suffix = path.suffix.lower()[1:] - - with path.open("r") as file: - if (type_ or path_suffix) == "json": - model_dict = json.loads(file.read()) - elif (type_ or path_suffix) in ["yaml", "yml"]: - model_dict = yaml.safe_load(file) - else: - raise ValueError(f"Unsupported file type: {type_} for {path}.") - - return GenerativeBenchmarksReport.model_validate(model_dict) - - benchmarks: list[GenerativeBenchmark] = Field( - description="The list of completed benchmarks contained within the report.", - default_factory=list, - ) - - def save_file( - self, path: str | Path | None, type_: Literal["json", "yaml"] | None = None - ) -> Path: - """ - Save the report to a file. - - :param path: The path to save the report to. - :param type_: File type override, auto-detected from extension if None. - :return: The path to the saved report. - :raises ValueError: If file type is unsupported. - """ - if path is None: - path = Path.cwd() - elif not isinstance(path, Path): - path = Path(path) - - if path.is_dir(): - path = path / GenerativeBenchmarksReport.DEFAULT_FILE - - path.parent.mkdir(parents=True, exist_ok=True) - path_suffix = path.suffix.lower()[1:] - model_dict = self.model_dump() - - if (type_ or path_suffix) == "json": - save_str = json.dumps(model_dict) - elif (type_ or path_suffix) in ["yaml", "yml"]: - save_str = yaml.dump(model_dict) - else: - raise ValueError(f"Unsupported file type: {type_} for {path}.") - - with path.open("w") as file: - file.write(save_str) - - return path diff --git a/src/guidellm/benchmark/output.py b/src/guidellm/benchmark/output.py index 95b51d70..8b8213ca 100644 --- a/src/guidellm/benchmark/output.py +++ b/src/guidellm/benchmark/output.py @@ -14,17 +14,17 @@ from rich.padding import Padding from rich.text import Text -from guidellm.benchmark.objects import ( - GenerativeBenchmark, - GenerativeBenchmarksReport, - GenerativeMetrics, -) from guidellm.benchmark.profile import ( AsyncProfile, ConcurrentProfile, SweepProfile, ThroughputProfile, ) +from guidellm.benchmark.schemas import ( + GenerativeBenchmark, + GenerativeBenchmarksReport, + GenerativeMetrics, +) from guidellm.presentation import UIDataBuilder from guidellm.presentation.injector import create_report from guidellm.settings import settings diff --git a/src/guidellm/benchmark/profile.py b/src/guidellm/benchmark/profile.py index fd2a3850..93e86cba 100644 --- a/src/guidellm/benchmark/profile.py +++ b/src/guidellm/benchmark/profile.py @@ -46,7 +46,7 @@ from guidellm.utils import PydanticClassRegistryMixin if TYPE_CHECKING: - from guidellm.benchmark.objects import Benchmark + from guidellm.benchmark.schemas import Benchmark __all__ = [ "AsyncProfile", @@ -667,9 +667,9 @@ def next_strategy( return SynchronousStrategy() if prev_strategy.type_ == "synchronous": - self.synchronous_rate = ( - prev_benchmark.metrics.requests_per_second.successful.mean - ) + self.synchronous_rate = prev_benchmark.get_request_metrics_sample()[ + "request_throughput" + ] return ThroughputStrategy( max_concurrency=self.max_concurrency, @@ -677,9 +677,9 @@ def next_strategy( ) if prev_strategy.type_ == "throughput": - self.throughput_rate = ( - prev_benchmark.metrics.requests_per_second.successful.mean - ) + self.throughput_rate = prev_benchmark.get_request_metrics_sample()[ + "request_throughput" + ] if self.synchronous_rate <= 0 and self.throughput_rate <= 0: raise RuntimeError( "Invalid rates in sweep; aborting. " diff --git a/src/guidellm/benchmark/progress.py b/src/guidellm/benchmark/progress.py index f93b3a83..9389c742 100644 --- a/src/guidellm/benchmark/progress.py +++ b/src/guidellm/benchmark/progress.py @@ -37,14 +37,10 @@ TimeRemainingColumn, ) -from guidellm.benchmark.aggregator import AggregatorState -from guidellm.benchmark.objects import BenchmarkT, GenerativeBenchmark +from guidellm.benchmark.aggregator import EstimatedBenchmarkState from guidellm.benchmark.profile import Profile -from guidellm.scheduler import ( - SchedulerState, - SchedulingStrategy, - StrategyType, -) +from guidellm.benchmark.schemas import BenchmarkT, GenerativeBenchmark +from guidellm.scheduler import SchedulerState, SchedulingStrategy, StrategyType from guidellm.utils import Colors, format_value_display __all__ = [ @@ -98,7 +94,7 @@ def __call__( profile: Profile, agen: AsyncIterable[ tuple[ - AggregatorState | None, + EstimatedBenchmarkState | None, BenchmarkT | None, SchedulingStrategy, SchedulerState | None, @@ -106,7 +102,7 @@ def __call__( ], ) -> AsyncIterator[ tuple[ - AggregatorState | None, + EstimatedBenchmarkState | None, BenchmarkT | None, SchedulingStrategy, SchedulerState | None, @@ -125,7 +121,7 @@ def __call__( async def aiterator() -> AsyncIterator[ tuple[ - AggregatorState | None, + EstimatedBenchmarkState | None, BenchmarkT | None, SchedulingStrategy, SchedulerState | None, @@ -181,7 +177,9 @@ async def on_benchmark_start(self, strategy: SchedulingStrategy): @abstractmethod async def on_benchmark_update( - self, aggregator_update: AggregatorState, scheduler_state: SchedulerState + self, + aggregator_update: EstimatedBenchmarkState, + scheduler_state: SchedulerState, ): """ Handle benchmark execution progress update. @@ -205,7 +203,7 @@ async def on_finalize(self): async def on_raw_update( self, profile: Profile, - aggregator_update: AggregatorState | None, + aggregator_update: EstimatedBenchmarkState | None, benchmark: BenchmarkT | None, strategy: SchedulingStrategy, scheduler_state: SchedulerState | None, @@ -290,7 +288,9 @@ async def on_benchmark_start(self, strategy: SchedulingStrategy): ) async def on_benchmark_update( - self, aggregator_update: AggregatorState, scheduler_state: SchedulerState + self, + aggregator_update: EstimatedBenchmarkState, + scheduler_state: SchedulerState, ): """ Distribute benchmark updates to all handlers. @@ -322,7 +322,7 @@ async def on_finalize(self): async def on_raw_update( self, profile: Profile, - aggregator_update: AggregatorState | None, + aggregator_update: EstimatedBenchmarkState | None, benchmark: BenchmarkT | None, strategy: SchedulingStrategy, scheduler_state: SchedulerState | None, @@ -432,7 +432,9 @@ async def on_benchmark_start(self, strategy: SchedulingStrategy): self._sync_run_progress() async def on_benchmark_update( - self, aggregator_update: AggregatorState | None, scheduler_state: SchedulerState + self, + aggregator_update: EstimatedBenchmarkState | None, + scheduler_state: SchedulerState, ): """ Update display with current benchmark progress. @@ -545,7 +547,9 @@ def start_benchmark(self, strategy: SchedulingStrategy): ) def update_benchmark( - self, aggregator_update: AggregatorState, scheduler_state: SchedulerState + self, + aggregator_update: EstimatedBenchmarkState, + scheduler_state: SchedulerState, ): self.benchmark_task_states[self.current_index].update( aggregator_update, scheduler_state @@ -800,7 +804,9 @@ def start(self, strategy: SchedulingStrategy): self.strategy_type = strategy.type_ def update( - self, aggregator_update: AggregatorState, scheduler_state: SchedulerState + self, + aggregator_update: EstimatedBenchmarkState, + scheduler_state: SchedulerState, ): self.progress = ( (1.0 - scheduler_state.remaining_fraction) diff --git a/src/guidellm/benchmark/schemas.py b/src/guidellm/benchmark/schemas.py new file mode 100644 index 00000000..1b11aae6 --- /dev/null +++ b/src/guidellm/benchmark/schemas.py @@ -0,0 +1,1379 @@ +""" +Benchmark data models and metrics for performance measurement and analysis. + +Provides comprehensive data structures for capturing, storing, and analyzing +benchmark results from scheduler executions. Includes timing measurements, +token statistics, and performance metrics for generative AI workloads. + +Classes: + BenchmarkSchedulerStats: Scheduler timing and performance statistics. + BenchmarkMetrics: Core benchmark metrics and distributions. + BenchmarkRequestStats: Individual request processing statistics. + Benchmark: Base benchmark result container with generic metrics. + GenerativeRequestStats: Request statistics for generative AI workloads. + GenerativeMetrics: Comprehensive metrics for generative benchmarks. + GenerativeBenchmark: Complete generative benchmark results and analysis. + GenerativeBenchmarksReport: Container for multiple benchmark results. + +Type Variables: + BenchmarkMetricsT: Generic benchmark metrics type. + BenchmarkRequestStatsT: Generic request statistics type. + BenchmarkT: Generic benchmark container type. +""" + +from __future__ import annotations + +import json +import random +import time +import uuid +from abc import ABC, abstractmethod +from collections.abc import Iterable +from pathlib import Path +from typing import Any, ClassVar, Literal, TypeVar, cast + +import yaml +from pydantic import Field, computed_field + +from guidellm.benchmark.profile import Profile +from guidellm.benchmark.schemas import BenchmarkerDict, SchedulerDict +from guidellm.scheduler import ( + BackendInterface, + Environment, + SchedulerState, + SchedulingStrategy, +) +from guidellm.schemas import ( + GenerationRequest, + GenerationResponse, + GenerativeRequestStats, + RequestInfo, +) +from guidellm.schemas.request import UsageMetrics +from guidellm.utils import ( + InfoMixin, + StandardBaseDict, + StandardBaseModel, + StatusBreakdown, + StatusDistributionSummary, +) +from guidellm.utils.pydantic_utils import StandardBaseDict + +__all__ = [ + "Benchmark", + "BenchmarkArgs", + "BenchmarkSchedulerStats", + "BenchmarkT", + "BenchmarkerDict", + "EstimatedBenchmarkState", + "GenerativeAudioMetricsSummary", + "GenerativeBenchmark", + "GenerativeBenchmarksReport", + "GenerativeImageMetricsSummary", + "GenerativeMetrics", + "GenerativeMetricsSummary", + "GenerativeTextMetricsSummary", + "GenerativeVideoMetricsSummary", + "SchedulerDict", +] + + +class EstimatedBenchmarkState(dict[str, Any]): + benchmark_state_group: ClassVar[Literal["benchmark_state"]] = "benchmark_state" + benchmark_metrics_group: ClassVar[Literal["benchmark_metrics"]] = ( + "benchmark_metrics" + ) + scheduler_state_group: ClassVar[Literal["scheduler_state"]] = "scheduler_state" + + def get_metric( + self, + group: str, + key: str, + default: int | float | None = None, + ) -> int | float | None: + return self.get(f"{group}_{key}", default) + + def set_metric( + self, + group: str, + key: str, + value: bool | int | float | None, + start_val: bool | int | float | None = None, + ) -> bool | int | float | None: + if value is None: + return None + + if start_val is not None: + value -= start_val + self[f"{group}_{key}"] = value + + return value + + def add_avg_metric( + self, + group: str, + key: str, + value: bool | int | float | None, + start_val: bool | int | float | None = 0.0, + count: int | None = 1, + ): + if value is None or count is None: + return + + if start_val is not None: + value -= start_val + + total_key = f"{group}_{key}_total" + count_key = f"{group}_{key}_count" + self[total_key] = self.get(total_key, 0) + value + self[count_key] = self.get(count_key, 0) + count + + average = self[total_key] / self[count_key] + self.set_metric( + group=group, + key=key, + value=average, + ) + + def add_avg_rate_metric( + self, + group: str, + key: str, + value: bool | int | float | None, + start_val: bool | int | float | None = 0.0, + start_time: float | None = None, + end_time: float | None = None, + numerator_type: Literal["avg", "total", "count"] = "total", + ): + if value is None: + return + + self.add_avg_metric( + group=group, + key=key, + value=value, + start_val=start_val, + ) + start_time_key = f"{group}_{key}_start_time" + if self.get(start_time_key) is None: + if start_time is None: + start_time = time.time() + else: + self[start_time_key] = start_time or self[start_time_key] + + end_time = end_time or time.time() + elapsed_time = end_time - self[start_time_key] + + if elapsed_time > 0: + numerator_key = ( + f"{group}_{key}_{numerator_type}" + if numerator_type != "avg" + else f"{group}_{key}" + ) + rate = self[numerator_key] / elapsed_time + self.set_metric( + group=group, + key=f"{key}_per_second", + value=rate, + ) + + def add_time_averaged_metric( + self, + group: str, + key: str, + value: bool | int | float | None, + recorded_time: float | None = None, + ): + if value is None: + return + + if recorded_time is None: + recorded_time = time.time() + + time_avg_numerator_key = f"{group}_{key}_time_avg_numerator" + time_avg_denominator_key = f"{group}_{key}_time_avg_denominator" + last_recorded_time_key = f"{group}_{key}_last_recorded_time" + + if last_recorded_time_key not in self: + self[last_recorded_time_key] = recorded_time + self[time_avg_numerator_key] = value + self[time_avg_denominator_key] = 0.0 + else: + time_delta = recorded_time - self[last_recorded_time_key] + self[time_avg_numerator_key] += value * time_delta + self[time_avg_denominator_key] += time_delta + self[last_recorded_time_key] = recorded_time + + if self[time_avg_denominator_key] > 0: + average = self[time_avg_numerator_key] / self[time_avg_denominator_key] + else: + average = value + + self.set_metric( + group=group, + key=key, + value=average, + ) + + +class BenchmarkArgs(StandardBaseDict): + run_id: str = Field( + default_factory=lambda: str(uuid.uuid4()), + description="Unique identifier for the benchmark run", + ) + run_index: int = Field(default=0, description="Index of the benchmark run") + sample_requests: int | None = Field( + default=20, + description="Number of requests to sample and keep in the final benchmark for metrics", + ) + warmup: int | float | None = Field( + default=None, description="Warmup time before benchmarking starts" + ) + cooldown: int | float | None = Field( + default=None, description="Cooldown time after benchmarking ends" + ) + prefer_response_metrics: bool = Field( + default=True, + description="Whether to prefer response metrics over request metrics", + ) + + def is_in_warmup( + self, request_info: RequestInfo, scheduler_state: SchedulerState + ) -> bool: + if self.warmup is not None and 0 < self.warmup < 1: + # Percentage-based warmup + return ( + scheduler_state.remaining_fraction is not None + and scheduler_state.remaining_fraction > (1 - self.warmup) + ) + + if self.warmup is not None and self.warmup > 1: + # Count/time-based warmup + if scheduler_state.processed_requests < self.warmup: + return True + + current_time = request_info.timings.targeted_start + return ( + current_time is not None + and (current_time - scheduler_state.start_time) < self.warmup + ) + + return False + + def is_in_cooldown( + self, request_info: RequestInfo, scheduler_state: SchedulerState + ) -> bool: + if self.cooldown is not None and 0 < self.cooldown < 1: + # Percentage-based cooldown + return ( + scheduler_state.remaining_fraction is not None + and scheduler_state.remaining_fraction < self.cooldown + ) + + if self.cooldown is not None and self.cooldown > 1: + # Count/time-based cooldown + if ( + scheduler_state.remaining_requests is not None + and scheduler_state.remaining_requests <= self.cooldown + ): + return True + + current_time = ( + request_info.timings.resolve_end or request_info.timings.targeted_start + ) + return ( + current_time is not None + and scheduler_state.remaining_duration is not None + and scheduler_state.remaining_duration < self.cooldown + ) + + return False + + +class Benchmark(ABC): + @abstractmethod + def get_run_metrics_sample( + self, + ) -> dict[Literal["start_time", "end_time", "duration"], float]: ... + + @abstractmethod + def get_request_metrics_sample( + self, + ) -> dict[ + Literal[ + "request_count", + "request_latency", + "request_throughput", + "request_concurrency", + ], + float, + ]: ... + + @abstractmethod + @classmethod + def update_estimate( + cls, + args: BenchmarkArgs, + state: EstimatedBenchmarkState, + response: Any, + request: Any, + request_info: RequestInfo, + scheduler_state: SchedulerState, + ): ... + + @abstractmethod + @classmethod + def compile( + cls, + args: BenchmarkArgs, + estimated_state: EstimatedBenchmarkState, + scheduler_state: SchedulerState, + profile: Profile, + requests: Iterable, + backend: BackendInterface, + environment: Environment, + strategy: SchedulingStrategy, + constraints: dict[str, dict[str, Any]], + ) -> Any: ... + + +BenchmarkT = TypeVar("BenchmarkT", bound=Benchmark) + + +class BenchmarkSchedulerStats(StandardBaseDict): + """Scheduler timing and performance statistics.""" + + group_name: ClassVar[Literal["scheduler_stats"]] = "scheduler_stats" + + start_time: float = Field( + description="Unix timestamp when the benchmark run started" + ) + end_time: float = Field(description="Unix timestamp when the benchmark run ended") + requests_made: StatusBreakdown[int, int, int, int] = Field( + description="Request counts by status: successful, incomplete, errored, total" + ) + queued_time_avg: float = Field( + description="Avg time requests spent in the queue (seconds)" + ) + worker_resolve_start_delay_avg: float = Field( + description="Avg delay before worker begins resolving req after dequeue (sec)" + ) + worker_resolve_time_avg: float = Field( + description="Avg time for worker to resolve requests (seconds)" + ) + worker_resolve_end_delay_avg: float = Field( + description="Avg delay after request end till worker resolves (seconds)" + ) + finalized_delay_avg: float = Field( + description="Avg delay after resolve til finalized with in scheduler (sec)" + ) + worker_targeted_start_delay_avg: float = Field( + description="Avg delay from targeted start to actual worker start (seconds)" + ) + request_start_delay_avg: float = Field( + description="Avg delay after resolve til request start (seconds)" + ) + request_time_avg: float = Field(description="Avg request processing time (seconds)") + request_targeted_start_delay_avg: float = Field( + description="Avg delay from targeted start to actual request start" + ) + + @classmethod + def update_estimate(cls, state: EstimatedBenchmarkState, request_info: RequestInfo): + state.set_metric(group=cls.group_name, key="updated", value=True) + state.add_avg_metric( + group=cls.group_name, + key="queued_time", + value=request_info.timings.dequeued, + start_val=request_info.timings.queued, + ) + state.add_avg_metric( + group=cls.group_name, + key="worker_resolve_start_delay", + value=request_info.timings.resolve_start, + start_val=request_info.timings.scheduled_at, + ) + state.add_avg_metric( + group=cls.group_name, + key="worker_resolve_time", + value=request_info.timings.resolve_end, + start_val=request_info.timings.resolve_start, + ) + state.add_avg_metric( + group=cls.group_name, + key="worker_resolve_end_delay", + value=request_info.timings.request_end, + start_val=request_info.timings.resolve_end, + ) + state.add_avg_metric( + group=cls.group_name, + key="finalized_delay", + value=request_info.timings.finalized, + start_val=request_info.timings.resolve_end, + ) + state.add_avg_metric( + group=cls.group_name, + key="worker_targeted_start_delay", + value=request_info.timings.resolve_start, + start_val=request_info.timings.targeted_start, + ) + state.add_avg_metric( + group=cls.group_name, + key="request_start_delay", + value=request_info.timings.request_start, + start_val=request_info.timings.resolve_start, + ) + state.add_avg_metric( + group=cls.group_name, + key="request_time", + value=request_info.timings.request_end, + start_val=request_info.timings.request_start, + ) + state.add_avg_metric( + group=cls.group_name, + key="request_targeted_start_delay", + value=request_info.timings.request_start, + start_val=request_info.timings.targeted_start, + ) + + @classmethod + def compile( + cls, estimated_state: EstimatedBenchmarkState, scheduler_state: SchedulerState + ) -> BenchmarkSchedulerStats: + return BenchmarkSchedulerStats( + start_time=scheduler_state.start_time, + end_time=scheduler_state.end_time or scheduler_state.start_time, + requests_made=StatusBreakdown[int, int, int, int]( + successful=scheduler_state.successful_requests, + incomplete=scheduler_state.cancelled_requests, + errored=scheduler_state.errored_requests, + total=( + scheduler_state.successful_requests + + scheduler_state.cancelled_requests + + scheduler_state.errored_requests + ), + ), + queued_time_avg=cast( + "float", + estimated_state.get_metric( + group=cls.group_name, key="queued_time", default=-1.0 + ), + ), + worker_resolve_start_delay_avg=cast( + "float", + estimated_state.get_metric( + group=cls.group_name, key="worker_resolve_start_delay", default=-1.0 + ), + ), + worker_resolve_time_avg=cast( + "float", + estimated_state.get_metric( + group=cls.group_name, key="worker_resolve_time", default=-1.0 + ), + ), + worker_resolve_end_delay_avg=cast( + "float", + estimated_state.get_metric( + group=cls.group_name, key="worker_resolve_end_delay", default=-1.0 + ), + ), + finalized_delay_avg=cast( + "float", + estimated_state.get_metric( + group=cls.group_name, key="finalized_delay", default=-1.0 + ), + ), + worker_targeted_start_delay_avg=cast( + "float", + estimated_state.get_metric( + group=cls.group_name, + key="worker_targeted_start_delay", + default=-1.0, + ), + ), + request_start_delay_avg=cast( + "float", + estimated_state.get_metric( + group=cls.group_name, key="request_start_delay", default=-1.0 + ), + ), + request_time_avg=cast( + "float", + estimated_state.get_metric( + group=cls.group_name, key="request_time", default=-1.0 + ), + ), + request_targeted_start_delay_avg=cast( + "float", + estimated_state.get_metric( + group=cls.group_name, + key="request_targeted_start_delay", + default=-1.0, + ), + ), + ) + + +class GenerativeMetricsSummary(StandardBaseDict): + input: StatusDistributionSummary = Field(description="") + input_per_second: StatusDistributionSummary = Field(description="") + input_concurrency: StatusDistributionSummary = Field(description="") + + output: StatusDistributionSummary = Field(description="") + output_per_second: StatusDistributionSummary = Field(description="") + output_concurrency: StatusDistributionSummary = Field(description="") + + total: StatusDistributionSummary = Field(description="") + total_per_second: StatusDistributionSummary = Field(description="") + total_concurrency: StatusDistributionSummary = Field(description="") + + @classmethod + def compile( + cls, + request_types: list[Literal["successful", "incomplete", "error"]], + request_times: list[tuple[float, float]], + input_values: list[int | float], + output_values: list[int | float], + ) -> GenerativeMetricsSummary: + total_values = [ + input_val + output_val + for input_val, output_val in zip(input_values, output_values) + ] + + return GenerativeMetricsSummary( + input=StatusDistributionSummary.from_values( + value_types=request_types, + values=input_values, + ), + input_per_second=StatusDistributionSummary.from_request_times( + request_types=request_types, + requests=request_times, + distribution_type="rate", + weights=input_values, + ), + input_concurrency=StatusDistributionSummary.from_request_times( + request_types=request_types, + requests=request_times, + distribution_type="concurrency", + weights=input_values, + ), + output=StatusDistributionSummary.from_values( + value_types=request_types, + values=output_values, + ), + output_per_second=StatusDistributionSummary.from_request_times( + request_types=request_types, + requests=request_times, + distribution_type="rate", + weights=output_values, + ), + output_concurrency=StatusDistributionSummary.from_request_times( + request_types=request_types, + requests=request_times, + distribution_type="concurrency", + weights=output_values, + ), + total=StatusDistributionSummary.from_values( + value_types=request_types, + values=total_values, + ), + total_per_second=StatusDistributionSummary.from_request_times( + request_types=request_types, + requests=request_times, + distribution_type="rate", + weights=total_values, + ), + total_concurrency=StatusDistributionSummary.from_request_times( + request_types=request_types, + requests=request_times, + distribution_type="concurrency", + weights=total_values, + ), + ) + + +class GenerativeTextMetricsSummary(StandardBaseDict): + tokens: GenerativeMetricsSummary = Field(description="") + words: GenerativeMetricsSummary = Field(description="") + characters: GenerativeMetricsSummary = Field(description="") + bytes: GenerativeMetricsSummary = Field(description="") + + @classmethod + def compile( + cls, + request_types: list[Literal["successful", "incomplete", "error"]], + request_times: list[tuple[float, float]], + input_metrics: list[UsageMetrics], + output_metrics: list[UsageMetrics], + ) -> GenerativeTextMetricsSummary: + return GenerativeTextMetricsSummary( + tokens=GenerativeMetricsSummary.compile( + request_types=request_types, + request_times=request_times, + input_values=[metrics.text_tokens or 0 for metrics in input_metrics], + output_values=[metrics.text_tokens or 0 for metrics in output_metrics], + ), + words=GenerativeMetricsSummary.compile( + request_types=request_types, + request_times=request_times, + input_values=[metrics.text_words or 0 for metrics in input_metrics], + output_values=[metrics.text_words or 0 for metrics in output_metrics], + ), + characters=GenerativeMetricsSummary.compile( + request_types=request_types, + request_times=request_times, + input_values=[ + metrics.text_characters or 0 for metrics in input_metrics + ], + output_values=[ + metrics.text_characters or 0 for metrics in output_metrics + ], + ), + bytes=GenerativeMetricsSummary.compile( + request_types=request_types, + request_times=request_times, + input_values=[metrics.text_bytes or 0 for metrics in input_metrics], + output_values=[metrics.text_bytes or 0 for metrics in output_metrics], + ), + ) + + +class GenerativeImageMetricsSummary(StandardBaseDict): + tokens: GenerativeMetricsSummary = Field(description="") + images: GenerativeMetricsSummary = Field(description="") + pixels: GenerativeMetricsSummary = Field(description="") + bytes: GenerativeMetricsSummary = Field(description="") + + @classmethod + def compile( + cls, + request_types: list[Literal["successful", "incomplete", "error"]], + request_times: list[tuple[float, float]], + input_metrics: list[UsageMetrics], + output_metrics: list[UsageMetrics], + ) -> GenerativeImageMetricsSummary: + return GenerativeImageMetricsSummary( + tokens=GenerativeMetricsSummary.compile( + request_types=request_types, + request_times=request_times, + input_values=[metrics.image_tokens or 0 for metrics in input_metrics], + output_values=[metrics.image_tokens or 0 for metrics in output_metrics], + ), + images=GenerativeMetricsSummary.compile( + request_types=request_types, + request_times=request_times, + input_values=[metrics.image_count or 0 for metrics in input_metrics], + output_values=[metrics.image_count or 0 for metrics in output_metrics], + ), + pixels=GenerativeMetricsSummary.compile( + request_types=request_types, + request_times=request_times, + input_values=[metrics.image_pixels or 0 for metrics in input_metrics], + output_values=[metrics.image_pixels or 0 for metrics in output_metrics], + ), + bytes=GenerativeMetricsSummary.compile( + request_types=request_types, + request_times=request_times, + input_values=[metrics.image_bytes or 0 for metrics in input_metrics], + output_values=[metrics.image_bytes or 0 for metrics in output_metrics], + ), + ) + + +class GenerativeVideoMetricsSummary(StandardBaseDict): + tokens: GenerativeMetricsSummary = Field(description="") + frames: GenerativeMetricsSummary = Field(description="") + seconds: GenerativeMetricsSummary = Field(description="") + bytes: GenerativeMetricsSummary = Field(description="") + + @classmethod + def compile( + cls, + request_types: list[Literal["successful", "incomplete", "error"]], + request_times: list[tuple[float, float]], + input_metrics: list[UsageMetrics], + output_metrics: list[UsageMetrics], + ) -> GenerativeVideoMetricsSummary: + return GenerativeVideoMetricsSummary( + tokens=GenerativeMetricsSummary.compile( + request_types=request_types, + request_times=request_times, + input_values=[metrics.video_tokens or 0 for metrics in input_metrics], + output_values=[metrics.video_tokens or 0 for metrics in output_metrics], + ), + frames=GenerativeMetricsSummary.compile( + request_types=request_types, + request_times=request_times, + input_values=[metrics.video_frames or 0 for metrics in input_metrics], + output_values=[metrics.video_frames or 0 for metrics in output_metrics], + ), + seconds=GenerativeMetricsSummary.compile( + request_types=request_types, + request_times=request_times, + input_values=[metrics.video_seconds or 0 for metrics in input_metrics], + output_values=[ + metrics.video_seconds or 0 for metrics in output_metrics + ], + ), + bytes=GenerativeMetricsSummary.compile( + request_types=request_types, + request_times=request_times, + input_values=[metrics.video_bytes or 0 for metrics in input_metrics], + output_values=[metrics.video_bytes or 0 for metrics in output_metrics], + ), + ) + + +class GenerativeAudioMetricsSummary(StandardBaseDict): + tokens: GenerativeMetricsSummary = Field(description="") + samples: GenerativeMetricsSummary = Field(description="") + seconds: GenerativeMetricsSummary = Field(description="") + bytes: GenerativeMetricsSummary = Field(description="") + + @classmethod + def compile( + cls, + request_types: list[Literal["successful", "incomplete", "error"]], + request_times: list[tuple[float, float]], + input_metrics: list[UsageMetrics], + output_metrics: list[UsageMetrics], + ) -> GenerativeAudioMetricsSummary: + return GenerativeAudioMetricsSummary( + tokens=GenerativeMetricsSummary.compile( + request_types=request_types, + request_times=request_times, + input_values=[metrics.audio_tokens or 0 for metrics in input_metrics], + output_values=[metrics.audio_tokens or 0 for metrics in output_metrics], + ), + samples=GenerativeMetricsSummary.compile( + request_types=request_types, + request_times=request_times, + input_values=[metrics.audio_samples or 0 for metrics in input_metrics], + output_values=[ + metrics.audio_samples or 0 for metrics in output_metrics + ], + ), + seconds=GenerativeMetricsSummary.compile( + request_types=request_types, + request_times=request_times, + input_values=[metrics.audio_seconds or 0 for metrics in input_metrics], + output_values=[ + metrics.audio_seconds or 0 for metrics in output_metrics + ], + ), + bytes=GenerativeMetricsSummary.compile( + request_types=request_types, + request_times=request_times, + input_values=[metrics.audio_bytes or 0 for metrics in input_metrics], + output_values=[metrics.audio_bytes or 0 for metrics in output_metrics], + ), + ) + + +class GenerativeMetrics(StandardBaseDict): + """Comprehensive metrics for generative AI benchmarks.""" + + # Request stats + requests_per_second: StatusDistributionSummary = Field( + description="Distribution of requests per second across benchmark execution" + ) + request_concurrency: StatusDistributionSummary = Field( + description="Distribution of concurrent request counts during execution" + ) + request_latency: StatusDistributionSummary = Field( + description="Distribution of request latencies for completed requests" + ) + + # General token stats + prompt_token_count: StatusDistributionSummary = Field( + description="Distribution of prompt token counts by request status" + ) + output_token_count: StatusDistributionSummary = Field( + description="Distribution of output token counts by request status" + ) + total_token_count: StatusDistributionSummary = Field( + description="Distribution of total token counts by request status" + ) + time_to_first_token_ms: StatusDistributionSummary = Field( + description="Distribution of first token latencies in milliseconds" + ) + time_per_output_token_ms: StatusDistributionSummary = Field( + description="Distribution of average time per output token in milliseconds" + ) + inter_token_latency_ms: StatusDistributionSummary = Field( + description="Distribution of inter-token latencies in milliseconds" + ) + output_tokens_per_second: StatusDistributionSummary = Field( + description="Distribution of output token generation rates" + ) + tokens_per_second: StatusDistributionSummary = Field( + description="Distribution of total token throughput including prompt and output" + ) + + # Domain specific stats + text: GenerativeTextMetricsSummary = Field(description="") + image: GenerativeImageMetricsSummary = Field(description="") + video: GenerativeVideoMetricsSummary = Field(description="") + audio: GenerativeAudioMetricsSummary = Field(description="") + + @classmethod + def update_estimate( + cls, + state: EstimatedBenchmarkState, + response: GenerationResponse | None, + request: GenerationRequest, + request_info: RequestInfo, + scheduler_state: SchedulerState, + ): + # Always track concurrency + state.add_time_averaged_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="concurrency", + value=scheduler_state.processing_requests, + ) + + if request_info.status not in {"completed", "errored", "cancelled"}: + return + + state.set_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="updated", + value=True, + ) + start_time = scheduler_state.start_time + end_time = request_info.timings.request_end or request_info.timings.resolve_end + duration = end_time - start_time if end_time else None + + for prefix in (request_info.status, "total"): + requests_count = ( + scheduler_state.successful_requests + if prefix == "successful" + else scheduler_state.errored_requests + if prefix == "errored" + else scheduler_state.cancelled_requests + if prefix == "cancelled" + else scheduler_state.processed_requests + ) + + # Request stats + state.set_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key=f"{prefix}_requests", + value=requests_count, + ) + state.set_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key=f"{prefix}_requests_per_second", + value=requests_count / duration if duration else None, + ) + state.add_avg_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key=f"{prefix}_request_latency", + value=( + request_info.timings.request_end or request_info.timings.resolve_end + ), + start_val=( + request_info.timings.request_start + or request_info.timings.resolve_start + ), + ) + + # Input/output token stats + state.add_avg_rate_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="input_tokens", + value=(response.input_metrics.total_tokens if response else None) + or request.input_metrics.total_tokens, + ) + state.add_avg_rate_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="input_text_tokens", + value=(response.input_metrics.text_tokens if response else None) + or request.input_metrics.text_tokens, + ) + state.add_avg_rate_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="input_images", + value=(response.input_metrics.image_count if response else None) + or request.input_metrics.image_count, + ) + state.add_avg_rate_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="input_video_frames", + value=(response.input_metrics.video_frames if response else None) + or request.input_metrics.video_frames, + ) + state.add_avg_rate_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="input_audio_seconds", + value=request.input_metrics.audio_seconds if request else None, + ) + state.add_avg_rate_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="output_tokens", + value=(response.output_metrics.total_tokens if response else None) + or request.output_metrics.total_tokens, + ) + + # General stats + output_tokens = ( + response.output_metrics.total_tokens if response else None + ) or request.output_metrics.total_tokens + state.add_avg_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key=f"{prefix}_time_to_first_token", + value=request_info.timings.first_iteration, + start_val=request_info.timings.request_start + or request_info.timings.resolve_start, + ) + state.add_avg_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key=f"{prefix}_inter_token_latency", + value=request_info.timings.last_iteration, + start_val=request_info.timings.first_iteration, + count=output_tokens - 1 + if output_tokens and output_tokens > 1 + else None, + ) + state.add_avg_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key=f"{prefix}_time_per_output_token", + value=( + request_info.timings.request_end or request_info.timings.resolve_end + ), + start_val=( + request_info.timings.first_iteration + or request_info.timings.request_start + or request_info.timings.resolve_start + ), + count=output_tokens, + ) + + @classmethod + def compile( + cls, + completed: list[GenerativeRequestStats], + errored: list[GenerativeRequestStats], + incomplete: list[GenerativeRequestStats], + ) -> GenerativeMetrics: + requests = completed + errored + incomplete + request_types = cast( + "list[Literal['successful', 'error', 'incomplete']]", + ["successful"] * len(completed) + + ["error"] * len(errored) + + ["incomplete"] * len(incomplete), + ) + request_times = [ + ( + req.info.timings.request_start or req.info.timings.resolve_start or 0, + req.info.timings.request_end or req.info.timings.resolve_end or 0, + ) + for req in requests + ] + input_metrics = [req.input_metrics for req in requests] + output_metrics = [req.output_metrics for req in requests] + + return GenerativeMetrics( + # Request stats + requests_per_second=StatusDistributionSummary.from_request_times( + request_types=request_types, + requests=request_times, + distribution_type="rate", + ), + request_concurrency=StatusDistributionSummary.from_request_times( + request_types=request_types, + requests=request_times, + distribution_type="concurrency", + ), + request_latency=StatusDistributionSummary.from_values( + value_types=request_types, + values=[req.request_latency or 0.0 for req in requests], + ), + # General token stats + prompt_token_count=StatusDistributionSummary.from_values( + value_types=request_types, + values=[float(req.prompt_tokens or 0) for req in requests], + ), + output_token_count=StatusDistributionSummary.from_values( + value_types=request_types, + values=[float(req.output_tokens or 0) for req in requests], + ), + total_token_count=StatusDistributionSummary.from_values( + value_types=request_types, + values=[float(req.total_tokens or 0) for req in requests], + ), + time_to_first_token_ms=StatusDistributionSummary.from_values( + value_types=request_types, + values=[req.time_to_first_token_ms or 0.0 for req in requests], + ), + time_per_output_token_ms=StatusDistributionSummary.from_values( + value_types=request_types, + values=[req.time_per_output_token_ms or 0.0 for req in requests], + ), + inter_token_latency_ms=StatusDistributionSummary.from_values( + value_types=request_types, + values=[req.inter_token_latency_ms or 0.0 for req in requests], + ), + output_tokens_per_second=StatusDistributionSummary.from_values( + value_types=request_types, + values=[req.output_tokens_per_second or 0.0 for req in requests], + ), + tokens_per_second=StatusDistributionSummary.from_values( + value_types=request_types, + values=[req.tokens_per_second or 0.0 for req in requests], + ), + # Domain-specific stats + text=GenerativeTextMetricsSummary.compile( + request_types=request_types, + request_times=request_times, + input_metrics=input_metrics, + output_metrics=output_metrics, + ), + image=GenerativeImageMetricsSummary.compile( + request_types=request_types, + request_times=request_times, + input_metrics=input_metrics, + output_metrics=output_metrics, + ), + video=GenerativeVideoMetricsSummary.compile( + request_types=request_types, + request_times=request_times, + input_metrics=input_metrics, + output_metrics=output_metrics, + ), + audio=GenerativeAudioMetricsSummary.compile( + request_types=request_types, + request_times=request_times, + input_metrics=input_metrics, + output_metrics=output_metrics, + ), + ) + + +class SchedulerDict(StandardBaseDict): + """Scheduler configuration and execution state dictionary.""" + + strategy: SchedulingStrategy + constraints: dict[str, dict[str, Any]] + state: SchedulerState + + +class BenchmarkerDict(StandardBaseDict): + """Benchmarker configuration and component settings dictionary.""" + + args: BenchmarkArgs + profile: Profile + requests: dict[str, Any] + backend: dict[str, Any] + environment: dict[str, Any] + + +class GenerativeBenchmark(Benchmark, StandardBaseDict): + """Complete generative AI benchmark results with specialized metrics.""" + + group_name: ClassVar[Literal["generative_benchmark"]] = "generative_benchmark" + + type_: Literal["generative_benchmark"] = "generative_benchmark" # type: ignore[assignment] + id_: str = Field( + default_factory=lambda: str(uuid.uuid4()), + description="Unique identifier for this benchmark execution", + ) + run_id: str = Field( + description="Identifier for the benchmarker run containing this benchmark" + ) + run_index: int = Field( + description="Sequential index of this benchmark within the benchmarker run" + ) + scheduler: SchedulerDict = Field( + description="Scheduler configuration and execution state" + ) + benchmarker: BenchmarkerDict = Field( + description="Benchmarker configuration and component settings" + ) + run_stats: BenchmarkSchedulerStats = Field( + description="Scheduler timing and performance statistics" + ) + start_time: float = Field( + default=-1.0, description="Unix timestamp when the first request was initiated" + ) + end_time: float = Field( + default=-1.0, description="Unix timestamp when the last request completed" + ) + + def get_run_metrics_sample( + self, + ) -> dict[Literal["start_time", "end_time", "duration"], float]: + return { + "start_time": self.start_time, + "end_time": self.end_time, + "duration": self.duration, + } + + def get_request_metrics_sample( + self, + ) -> dict[ + Literal[ + "request_count", + "request_latency", + "request_throughput", + "request_concurrency", + ], + float, + ]: + return { + "request_count": self.request_totals.successful, + "request_latency": self.metrics.request_latency.successful.mean, + "request_throughput": self.metrics.requests_per_second.successful.mean, + "request_concurrency": self.metrics.request_concurrency.successful.mean, + } + + @computed_field # type: ignore[misc] + @property + def duration(self) -> float: + """ + Benchmark execution duration in seconds. + + :return: Time elapsed from first request start to last request completion. + """ + return self.end_time - self.start_time + + metrics: GenerativeMetrics = Field( + description="Performance metrics and statistical distributions" + ) + request_totals: StatusBreakdown[int, int, int, int] = Field( + description="Request counts by status: successful, incomplete, errored, total" + ) + requests: StatusBreakdown[ + list[GenerativeRequestStats], + list[GenerativeRequestStats], + list[GenerativeRequestStats], + None, + ] = Field( + description="Request details grouped by status: successful, incomplete, errored" + ) + + @classmethod + def update_estimate( + cls, + args: BenchmarkArgs, + state: EstimatedBenchmarkState, + response: GenerationResponse | None, + request: GenerationRequest, + request_info: RequestInfo, + scheduler_state: SchedulerState, + ): + # Update child metric groups + BenchmarkSchedulerStats.update_estimate(state, request_info) + GenerativeMetrics.update_estimate( + state, response, request, request_info, scheduler_state + ) + + # Store requests and sampling info, update counts + if "requests_completed" not in state: + state["requests_completed"] = [] + state["samples_completed"] = [] + state["requests_errored"] = [] + state["samples_errored"] = [] + state["requests_incomplete"] = [] + state["samples_incomplete"] = [] + + if request_info.status not in {"completed", "errored", "cancelled"}: + # Must be fully resolved to be added + return + + if ( + request_info.status == "cancelled" + and request_info.timings.resolve_start is None + ): + # Cancelled requests that never started should not be added + return + + state.set_metric(group=cls.group_name, key="updated", value=True) + if state.set_metric( + group=cls.group_name, + key="in_warmup", + value=args.is_in_warmup(request_info, scheduler_state), + ) or state.set_metric( + group=cls.group_name, + key="in_cooldown", + value=args.is_in_cooldown(request_info, scheduler_state), + ): + return + + if response is None: + response = GenerationResponse( + request_id=request.request_id, request_args=str(request.arguments) + ) + + stats = response.compile_stats( + request, request_info, args.prefer_response_metrics + ) + + # Determine status and get corresponding lists + if request_info.status == "completed": + requests_list = state["requests_completed"] + samples_list = state["samples_completed"] + elif request_info.status == "errored": + requests_list = state["requests_errored"] + samples_list = state["samples_errored"] + else: # cancelled (incomplete) + requests_list = state["requests_incomplete"] + samples_list = state["samples_incomplete"] + + # Add to requests list + requests_list.append(stats) + current_index = len(requests_list) - 1 + + # Handle request sampling logic + if args.sample_requests is None: + # No sampling, add index to samples list + samples_list.append(current_index) + elif args.sample_requests > 0 and len(samples_list) < args.sample_requests: + # Space in samples list, add index + samples_list.append(current_index) + elif ( + args.sample_requests > 0 + and (replace_index := random.randrange(len(requests_list))) + < args.sample_requests + ): + # No space, adding based on reservoir sampling + samples_list[replace_index] = current_index + # Sampling set to 0, don't keep any requests + + @classmethod + def compile( + cls, + args: BenchmarkArgs, + estimated_state: EstimatedBenchmarkState, + scheduler_state: SchedulerState, + profile: Profile, + requests: Iterable, + backend: BackendInterface, + environment: Environment, + strategy: SchedulingStrategy, + constraints: dict[str, dict[str, Any]], + ) -> GenerativeBenchmark: + return GenerativeBenchmark( + run_id=args.run_id, + run_index=args.run_index, + scheduler=SchedulerDict( + strategy=strategy, + constraints={ + key: InfoMixin.extract_from_obj(val) + for key, val in constraints.items() + }, + state=scheduler_state, + ), + benchmarker=BenchmarkerDict( + args=args, + profile=profile, + requests=InfoMixin.extract_from_obj(requests), + backend=backend.info, + environment=environment.info, + ), + run_stats=BenchmarkSchedulerStats.compile(estimated_state, scheduler_state), + start_time=scheduler_state.start_time or -1.0, + end_time=scheduler_state.end_time or -1.0, + metrics=GenerativeMetrics.compile( + completed=estimated_state.get("requests_completed", []), + errored=estimated_state.get("requests_errored", []), + incomplete=estimated_state.get("requests_incomplete", []), + ), + request_totals=StatusBreakdown[int, int, int, int]( + successful=len(estimated_state.get("requests_completed", [])), + incomplete=len(estimated_state.get("requests_incomplete", [])), + errored=len(estimated_state.get("requests_errored", [])), + total=( + len(estimated_state.get("requests_completed", [])) + + len(estimated_state.get("requests_incomplete", [])) + + len(estimated_state.get("requests_errored", [])) + ), + ), + requests=StatusBreakdown[ + list[GenerativeRequestStats], + list[GenerativeRequestStats], + list[GenerativeRequestStats], + None, + ]( + successful=estimated_state.get("requests_completed", []), + incomplete=estimated_state.get("requests_incomplete", []), + errored=estimated_state.get("requests_errored", []), + total=None, + ), + ) + + +class GenerativeBenchmarksReport(StandardBaseModel): + """Container for multiple benchmark results with load/save functionality.""" + + DEFAULT_FILE: ClassVar[str] = "benchmarks.json" + + @staticmethod + def load_file( + path: str | Path, type_: Literal["json", "yaml"] | None = None + ) -> GenerativeBenchmarksReport: + """ + Load a report from a file. + + :param path: The path to load the report from. + :param type_: File type override, auto-detected from extension if None. + :return: The loaded report. + :raises ValueError: If file type is unsupported. + """ + path = Path(path) if not isinstance(path, Path) else path + + if path.is_dir(): + path = path / GenerativeBenchmarksReport.DEFAULT_FILE + + path.parent.mkdir(parents=True, exist_ok=True) + path_suffix = path.suffix.lower()[1:] + + with path.open("r") as file: + if (type_ or path_suffix) == "json": + model_dict = json.loads(file.read()) + elif (type_ or path_suffix) in ["yaml", "yml"]: + model_dict = yaml.safe_load(file) + else: + raise ValueError(f"Unsupported file type: {type_} for {path}.") + + return GenerativeBenchmarksReport.model_validate(model_dict) + + benchmarks: list[GenerativeBenchmark] = Field( + description="The list of completed benchmarks contained within the report.", + default_factory=list, + ) + + def save_file( + self, path: str | Path | None, type_: Literal["json", "yaml"] | None = None + ) -> Path: + """ + Save the report to a file. + + :param path: The path to save the report to. + :param type_: File type override, auto-detected from extension if None. + :return: The path to the saved report. + :raises ValueError: If file type is unsupported. + """ + if path is None: + path = Path.cwd() + elif not isinstance(path, Path): + path = Path(path) + + if path.is_dir(): + path = path / GenerativeBenchmarksReport.DEFAULT_FILE + + path.parent.mkdir(parents=True, exist_ok=True) + path_suffix = path.suffix.lower()[1:] + model_dict = self.model_dump() + + if (type_ or path_suffix) == "json": + save_str = json.dumps(model_dict) + elif (type_ or path_suffix) in ["yaml", "yml"]: + save_str = yaml.dump(model_dict) + else: + raise ValueError(f"Unsupported file type: {type_} for {path}.") + + with path.open("w") as file: + file.write(save_str) + + return path diff --git a/src/guidellm/data/__init__.py b/src/guidellm/data/__init__.py index d25c719a..0bff1b64 100644 --- a/src/guidellm/data/__init__.py +++ b/src/guidellm/data/__init__.py @@ -4,39 +4,25 @@ DatasetDeserializer, DatasetDeserializerFactory, ) -from .loaders import DataLoader -from .objects import ( - GenerationRequest, - GenerationRequestArguments, - GenerationRequestTimings, - GenerativeDatasetColumnType, - GenerativeRequestType, -) +from .loaders import DataLoader, DatasetsIterator from .preprocessors import ( DataDependentPreprocessor, DatasetPreprocessor, PreprocessorRegistry, ) from .processor import ProcessorFactory +from .schemas import GenerativeDatasetColumnType __all__ = [ - "ColumnMapper", - "ColumnMapperRegistry", "DataDependentPreprocessor", "DataLoader", "DataNotSupportedError", "DatasetDeserializer", "DatasetDeserializerFactory", "DatasetPreprocessor", - "GenerationRequest", - "GenerationRequestArguments", - "GenerationRequestTimings", - "GenerativeDatasetArgs", + "DatasetsIterator", "GenerativeDatasetColumnType", "GenerativeRequestCollator", - "GenerativeRequestType", "PreprocessorRegistry", "ProcessorFactory", - "RequestFormatter", - "RequestFormatterRegistry", ] diff --git a/src/guidellm/data/collators.py b/src/guidellm/data/collators.py index 4d12f0c0..f9e1ade4 100644 --- a/src/guidellm/data/collators.py +++ b/src/guidellm/data/collators.py @@ -1,6 +1,6 @@ from __future__ import annotations -from guidellm.data.objects import GenerationRequest +from guidellm.schemas import GenerationRequest __all__ = ["GenerativeRequestCollator"] diff --git a/src/guidellm/data/loaders.py b/src/guidellm/data/loaders.py index 89098964..f397ad51 100644 --- a/src/guidellm/data/loaders.py +++ b/src/guidellm/data/loaders.py @@ -1,42 +1,82 @@ from __future__ import annotations import contextlib -import math from collections.abc import Callable, Iterator from typing import Any, Literal -from datasets import Dataset, IterableDataset +import torch from torch.utils.data import Sampler from torch.utils.data.dataloader import DataLoader as PyTorchDataLoader +from torch.utils.data.dataset import IterableDataset as TorchIterableDataset from transformers import PreTrainedTokenizerBase from guidellm.data.deserializers import DatasetDeserializerFactory -from guidellm.data.objects import GenerationRequest from guidellm.data.preprocessors import DataDependentPreprocessor, DatasetPreprocessor -__all__ = ["DataIterator", "DataLoader"] +__all__ = ["DataLoader", "DatasetsIterator"] -class DataIterator: +class DatasetsIterator(TorchIterableDataset): def __init__( self, - datasets: list[Dataset | IterableDataset], + data: list[Any], + data_args: list[dict[str, Any]] | None, + data_samples: int, + processor_factory: Callable[[], PreTrainedTokenizerBase], preprocessors: list[DatasetPreprocessor | DataDependentPreprocessor], - precache_size: int | None = None, + random_seed: int, ): - self.datasets = datasets + if not data or not isinstance(data, list): + raise ValueError(f"Data must be a non-empty list, got {data}.") + + if data_args is None: + data_args = [{} for _ in data] + + if len(data) != len(data_args): + raise ValueError( + f"Length of data ({len(data)}) must match length of data_args " + f"({len(data_args)})." + ) + + self.datasets = [] + for datum, data_kwargs in zip(data, data_args): + self.datasets.append( + DatasetDeserializerFactory.deserialize( + data=datum, + processor_factory=processor_factory, + random_seed=random_seed, + **data_kwargs, + ) + ) self.preprocessors = preprocessors - self.precache = ( - None if not precache_size else list(self.generator(precache_size)) + for preprocessor in self.preprocessors: + if isinstance(preprocessor, DataDependentPreprocessor): + preprocessor.setup_data( + datasets=self.datasets, + data_args=data_args, + ) + self.precache: list[Any] | None = ( + list(self.generator(data_samples)) if data_samples else None ) def __iter__(self): + worker_info = torch.utils.data.get_worker_info() + modulus = worker_info.num_workers if worker_info is not None else 1 + index = worker_info.id if worker_info is not None else 0 + if self.precache is not None: - yield from self.precache + for index, item in enumerate(self.precache): + if index == index % modulus: + yield item else: - yield from self.generator() + yield from self.generator(modulus=modulus, offset=index) - def generator(self, max_items: int | None = None) -> Iterator[Any]: + def generator( + self, + max_items: int | None = None, + modulus: int | None = None, + offset: int | None = None, + ) -> Iterator[Any]: gen_count = 0 with contextlib.suppress(StopIteration): @@ -44,10 +84,18 @@ def generator(self, max_items: int | None = None) -> Iterator[Any]: while max_items is None or gen_count < max_items: row = {"items": [next(dataset_iter) for dataset_iter in dataset_iters]} + gen_count += 1 + + if ( + modulus is not None + and offset is not None + and (gen_count % modulus) != offset + ): + continue + for preprocessor in self.preprocessors: row = preprocessor(row) yield row - gen_count += 1 if max_items is not None and gen_count < max_items: raise ValueError( @@ -56,7 +104,7 @@ def generator(self, max_items: int | None = None) -> Iterator[Any]: ) -class DataLoader(PyTorchDataLoader[GenerationRequest]): +class DataLoader(PyTorchDataLoader): def __init__( self, data: list[Any], @@ -70,50 +118,21 @@ def __init__( random_seed: int = 42, **kwargs: Any, ): - if not data or not isinstance(data, list): - raise ValueError(f"Data must be a non-empty list, got {data}.") - - if data_args is None: - data_args = [{} for _ in data] - - if len(data) != len(data_args): - raise ValueError( - f"Length of data ({len(data)}) must match length of data_args " - f"({len(data_args)})." - ) - - datasets = [] - for datum, data_kwargs in zip(data, data_args): - datasets.append( - DatasetDeserializerFactory.deserialize( - data=datum, - processor_factory=processor_factory, - random_seed=random_seed, - **data_kwargs, - ) - ) - for preprocessor in preprocessors: - if isinstance(preprocessor, DataDependentPreprocessor): - preprocessor.setup_data( - datasets=datasets, - data_args=data_args, - ) - - data_iterator = DataIterator( - datasets=datasets, + iterator = DatasetsIterator( + data=data, + data_args=data_args, + data_samples=data_samples, + processor_factory=processor_factory, preprocessors=preprocessors, - precache_size=data_samples - if data_samples != math.inf and data_samples > 0 - else None, + random_seed=random_seed, ) - dataset = IterableDataset.from_generator(data_iterator.__iter__) super().__init__( - dataset=dataset, + dataset=iterator, batch_size=1, shuffle=sampler == "shuffle", sampler=sampler if sampler != "shuffle" else None, collate_fn=collator, - num_workers=num_workers, + num_workers=num_workers or 0, **kwargs, ) diff --git a/src/guidellm/data/objects.py b/src/guidellm/data/objects.py deleted file mode 100644 index 095014d3..00000000 --- a/src/guidellm/data/objects.py +++ /dev/null @@ -1,157 +0,0 @@ -from __future__ import annotations - -import uuid -from typing import Any, Literal - -from pydantic import Field - -from guidellm.scheduler import ( - MeasuredRequestTimings, - SchedulerMessagingPydanticRegistry, -) -from guidellm.utils import StandardBaseDict, StandardBaseModel - -__all__ = [ - "GenerationRequest", - "GenerationRequestArguments", - "GenerationRequestTimings", - "GenerativeDatasetColumnType", - "GenerativeRequestType", -] - - -GenerativeRequestType = Literal[ - "text_completions", - "chat_completions", - "audio_transcriptions", - "audio_translations", -] - -GenerativeDatasetColumnType = Literal[ - "prompt_tokens_count_column", - "output_tokens_count_column", - "prefix_column", - "text_column", - "image_column", - "video_column", - "audio_column", -] - - -class GenerationRequestArguments(StandardBaseDict): - @classmethod - def model_combine_dict( # noqa: C901, PLR0912 - cls, *arguments: GenerationRequestArguments | dict[str, Any] - ) -> dict[str, Any]: - combined = {} - - for args in arguments: - args_dict = args if isinstance(args, dict) else args.model_dump() - combined["url"] = args_dict.get("url", combined.get("url")) - combined["path"] = args_dict.get("path", combined.get("path")) - combined["method"] = args_dict.get("method", combined.get("method")) - combined["stream"] = args_dict.get("stream", combined.get("stream")) - combined["content_body"] = args_dict.get( - "content_body", combined.get("content_body") - ) - - if (json_body := args_dict.get("json_body")) is not None: - combined["json_body"] = combined.get("json_body", {}) + json_body - if (files := args_dict.get("files")) is not None: - combined["files"] = combined.get("files", {}) + files - if (params := args_dict.get("params")) is not None: - combined["params"] = combined.get("params", {}) + params - if (headers := args_dict.get("headers")) is not None: - combined["headers"] = combined.get("headers", {}) + headers - - return combined - - url: str | None = Field( - default=None, - description="The URL endpoint to which the request will be sent.", - ) - path: str | None = Field( - default=None, - description="The path to append to the base URL for the request.", - ) - method: str | None = Field( - default=None, - description="The HTTP method to use for the request (e.g., 'POST', 'GET').", - ) - stream: bool | None = Field( - default=None, - description="Whether to stream the response, if applicable.", - ) - content_body: Any | None = Field( - default=None, - description="Raw content to send in the request body, if applicable.", - ) - json_body: dict[str, Any] | None = Field( - default=None, - description="JSON content to include in the request body, if applicable.", - ) - files: dict[str, Any] | None = Field( - default=None, - description="Files to include in the request, if applicable.", - ) - params: dict[str, Any] | None = Field( - default=None, - description="Query parameters to include in the request URL, if applicable.", - ) - headers: dict[str, str] | None = Field( - default=None, - description="HTTP headers to include in the request, if applicable.", - ) - - @property - def request_files(self) -> dict[str, Any] | None: - if not self.files: - return None - - return { - key: value if not isinstance(value, list) else tuple(value) - for key, value in self.files.items() - } - - -@SchedulerMessagingPydanticRegistry.register() -class GenerationRequest(StandardBaseModel): - """Request model for backend generation operations.""" - - request_id: str = Field( - default_factory=lambda: str(uuid.uuid4()), - description="Unique identifier for the request.", - ) - request_type: GenerativeRequestType | str = Field( - description=( - "Type of request. If url is not provided in arguments, " - "this will be used to determine the request url." - ), - ) - arguments: GenerationRequestArguments = Field( - description=( - "Payload for the request, structured as a dictionary of arguments to pass " - "to the respective backend method. For example, can contain " - "'json', 'headers', 'files', etc." - ) - ) - stats: dict[Literal["prompt_tokens", "output_tokens"], int] = Field( - default_factory=dict, - description="Request statistics including prompt and output token counts.", - ) - - -@SchedulerMessagingPydanticRegistry.register() -@MeasuredRequestTimings.register("generation_request_timings") -class GenerationRequestTimings(MeasuredRequestTimings): - """Timing model for tracking generation request lifecycle events.""" - - timings_type: Literal["generation_request_timings"] = "generation_request_timings" - first_iteration: float | None = Field( - default=None, - description="Unix timestamp when the first generation iteration began.", - ) - last_iteration: float | None = Field( - default=None, - description="Unix timestamp when the last generation iteration completed.", - ) diff --git a/src/guidellm/data/preprocessors/formatters.py b/src/guidellm/data/preprocessors/formatters.py index 02bb7398..76b0083b 100644 --- a/src/guidellm/data/preprocessors/formatters.py +++ b/src/guidellm/data/preprocessors/formatters.py @@ -2,21 +2,18 @@ from typing import Any -from guidellm.data.objects import ( - GenerationRequest, - GenerationRequestArguments, - GenerativeDatasetColumnType, -) from guidellm.data.preprocessors.preprocessor import ( DatasetPreprocessor, PreprocessorRegistry, ) +from guidellm.data.schemas import GenerativeDatasetColumnType from guidellm.data.utils import ( encode_audio_as_dict, encode_audio_as_file, encode_image, encode_video, ) +from guidellm.schemas import GenerationRequest, GenerationRequestArguments, UsageMetrics __all__ = [ "GenerativeAudioTranscriptionRequestFormatter", @@ -48,49 +45,52 @@ def __init__( def __call__( self, columns: dict[GenerativeDatasetColumnType, list[Any]] ) -> GenerationRequest: - arguments = {"json_body": {}} - stats = {} + body: dict[str, Any] = {} + arguments: GenerationRequestArguments = GenerationRequestArguments(body=body) + input_metrics = UsageMetrics() + output_metrics = UsageMetrics() # Add model if self.model is not None: - arguments["json_body"]["model"] = self.model + body["model"] = self.model # Configure streaming if self.stream: - arguments["json_body"].update( - {"stream": True, "stream_options": {"include_usage": True}} - ) - arguments["stream"] = True + arguments.stream = True + body["stream"] = True # Handle output tokens - if output_tokens := columns.get("output_tokens_count_column", []): - output_count = output_tokens[0] - stats["output_tokens"] = output_count - arguments["json_body"].update( - {"max_tokens": output_count, "stop": None, "ignore_eos": True} - ) + if output_tokens := sum( + count for count in columns.get("output_tokens_count_column", []) if count + ): + output_metrics.text_tokens = output_tokens + body["max_tokens"] = output_tokens + body["stop"] = None + body["ignore_eos"] = True elif self.max_tokens is not None: - arguments["json_body"]["max_tokens"] = self.max_tokens + body["max_tokens"] = self.max_tokens # Handle prompt tokens - if prompt_tokens := columns.get("prompt_tokens_count_column", []): - stats["prompt_tokens"] = prompt_tokens[0] + if prompt_tokens := sum( + count for count in columns.get("prompt_tokens_count_column", []) if count + ): + input_metrics.text_tokens = prompt_tokens # Apply extra arguments if self.extras: - arguments = GenerationRequestArguments.model_combine_dict( - arguments, self.extras - ) + arguments.model_combine(self.extras) # Build prompt - arguments["json_body"]["prompt"] = "".join( - columns.get("prefix_column", []) + columns.get("text_column", []) - ) + prefix = "".join(pre for pre in columns.get("prefix_column", []) if pre) + text = "".join(txt for txt in columns.get("text_column", []) if txt) + if prefix or text: + body["prompt"] = prefix + text return GenerationRequest( request_type="text_completions", - arguments=GenerationRequestArguments(**arguments), - stats=stats, + arguments=arguments, + input_metrics=input_metrics, + output_metrics=output_metrics, ) @@ -126,53 +126,56 @@ def __init__( def __call__( self, columns: dict[GenerativeDatasetColumnType, list[Any]] ) -> GenerationRequest: - arguments = {"json_body": {}} - stats = {} + body: dict[str, Any] = {} + arguments = GenerationRequestArguments(body=body) + input_metrics = UsageMetrics() + output_metrics = UsageMetrics() # Add model if self.model is not None: - arguments["json_body"]["model"] = self.model + body["model"] = self.model # Configure streaming if self.stream: - arguments["json_body"].update( - {"stream": True, "stream_options": {"include_usage": True}} - ) - arguments["stream"] = True + arguments.stream = True + body.update({"stream": True, "stream_options": {"include_usage": True}}) # Handle output tokens - if output_tokens := columns.pop("output_tokens_count_column", []): - output_count = output_tokens[0] - stats["output_tokens"] = output_count - arguments["json_body"].update( + if output_tokens := sum( + count for count in columns.get("output_tokens_count_column", []) if count + ): + output_metrics.text_tokens = output_tokens + body.update( { - "max_completion_tokens": output_count, + "max_completion_tokens": output_tokens, "stop": None, "ignore_eos": True, } ) elif self.max_completion_tokens is not None: - arguments["json_body"]["max_completion_tokens"] = self.max_completion_tokens + body["max_completion_tokens"] = self.max_completion_tokens # Handle prompt tokens - if prompt_tokens := columns.pop("prompt_tokens_count_column", []): - stats["prompt_tokens"] = prompt_tokens[0] + if prompt_tokens := sum( + count for count in columns.get("prompt_tokens_count_column", []) if count + ): + input_metrics.text_tokens = prompt_tokens # Apply extra arguments if self.extras: - arguments = GenerationRequestArguments.model_combine_dict( - arguments, self.extras - ) + arguments.model_combine(self.extras) # Build messages - arguments["json_body"]["messages"] = ( + body["messages"] = ( [ {"role": "system", "content": prefix} - for prefix in columns.pop("prefix_column", []) + for prefix in columns.get("prefix_column", []) + if prefix ] + [ {"role": "user", "content": [{"type": "text", "text": text}]} - for text in columns.pop("text_column", []) + for text in columns.get("text_column", []) + if text ] + [ { @@ -186,7 +189,8 @@ def __call__( } ], } - for image in columns.pop("image_column", []) + for image in columns.get("image_column", []) + if image ] + [ { @@ -200,7 +204,8 @@ def __call__( } ], } - for video in columns.pop("video_column", []) + for video in columns.get("video_column", []) + if video ] + [ { @@ -214,14 +219,16 @@ def __call__( } ], } - for audio in columns.pop("audio_column", []) + for audio in columns.get("audio_column", []) + if audio ] ) return GenerationRequest( request_type="chat_completions", - arguments=GenerationRequestArguments(**arguments), - stats=stats, + arguments=arguments, + input_metrics=input_metrics, + output_metrics=output_metrics, ) @@ -230,63 +237,72 @@ class GenerativeAudioTranscriptionRequestFormatter(DatasetPreprocessor): def __init__( self, model: str, - extra_args: dict[str, Any] | GenerationRequestArguments | None = None, + extras: dict[str, Any] | GenerationRequestArguments | None = None, stream: bool = True, encode_kwargs: dict[str, Any] | None = None, ): self.model = model - self.extra_args = extra_args + self.extras = ( + GenerationRequestArguments(**extras) + if extras and isinstance(extras, dict) + else extras + ) self.stream = stream self.encode_audio_kwargs = encode_kwargs or {} - def __call__( + def __call__( # noqa: C901 self, columns: dict[GenerativeDatasetColumnType, list[Any]] ) -> GenerationRequest: - arguments = {"json_body": {}, "files": {}} - stats = {} + body: dict[str, Any] = {} + arguments = GenerationRequestArguments(body=body, files={}) + input_metrics = UsageMetrics() + output_metrics = UsageMetrics() # Add model if self.model is not None: - arguments["json_body"]["model"] = self.model + body["model"] = self.model # Configure streaming if self.stream: - arguments["stream"] = True - arguments["json_body"].update( - {"stream": True, "stream_options": {"include_usage": True}} - ) + arguments.stream = True + body.update({"stream": True, "stream_options": {"include_usage": True}}) - # Apply extra arguments - if self.extra_args: - arguments = GenerationRequestArguments.model_combine_dict( - arguments, self.extra_args - ) + # Handle output tokens + if output_tokens := sum( + count for count in columns.get("output_tokens_count_column", []) if count + ): + output_metrics.text_tokens = output_tokens - # Handle stats tokens - if output_tokens := columns.get("output_tokens_count_column", []): - output_count = output_tokens[0] - stats["output_tokens"] = output_count - if prompt_tokens := columns.get("prompt_tokens_count_column", []): - stats["prompt_tokens"] = prompt_tokens[0] + # Handle prompt tokens (for audio duration tracking) + if prompt_tokens := sum( + count for count in columns.get("prompt_tokens_count_column", []) if count + ): + input_metrics.text_tokens = prompt_tokens + + # Apply extra arguments + if self.extras: + arguments.model_combine(self.extras) # Build audio input - if audio := columns.get("audio_column", []): - arguments["files"]["file"] = encode_audio_as_file( + if audio := [aud for aud in columns.get("audio_column", []) if aud]: + file_name, content, mime_type = encode_audio_as_file( audio[0], **self.encode_audio_kwargs ) + arguments.files = {"file": (file_name, content, mime_type)} else: raise ValueError("No audio column found for audio transcription request.") # Build prompt - if (prefix := columns.get("prefix_column", [])) or ( - text := columns.get("text_column", []) - ): - arguments["json_body"]["prompt"] = "".join(prefix) + "".join(text) + prefix = "".join(pre for pre in columns.get("prefix_column", []) if pre) + text = "".join(txt for txt in columns.get("text_column", []) if txt) + if prefix or text: + body["prompt"] = prefix + text return GenerationRequest( request_type="audio_transcriptions", - arguments=GenerationRequestArguments(**arguments), - stats=stats, + arguments=arguments, + input_metrics=input_metrics, + output_metrics=output_metrics, ) @@ -299,5 +315,4 @@ def __call__( ) -> GenerationRequest: result = super().__call__(columns) result.request_type = "audio_translations" - return result diff --git a/src/guidellm/data/preprocessors/mappers.py b/src/guidellm/data/preprocessors/mappers.py index 5e64b51c..cbfa9c20 100644 --- a/src/guidellm/data/preprocessors/mappers.py +++ b/src/guidellm/data/preprocessors/mappers.py @@ -5,11 +5,11 @@ from datasets import Dataset, IterableDataset -from guidellm.data.objects import GenerativeDatasetColumnType from guidellm.data.preprocessors.preprocessor import ( DataDependentPreprocessor, PreprocessorRegistry, ) +from guidellm.data.schemas import GenerativeDatasetColumnType __all__ = ["GenerativeColumnMapper"] diff --git a/src/guidellm/data/schemas.py b/src/guidellm/data/schemas.py new file mode 100644 index 00000000..c4421e07 --- /dev/null +++ b/src/guidellm/data/schemas.py @@ -0,0 +1,13 @@ +from typing import Literal + +__all__ = ["GenerativeDatasetColumnType"] + +GenerativeDatasetColumnType = Literal[ + "prompt_tokens_count_column", + "output_tokens_count_column", + "prefix_column", + "text_column", + "image_column", + "video_column", + "audio_column", +] diff --git a/src/guidellm/scheduler/__init__.py b/src/guidellm/scheduler/__init__.py index 64647424..2f5eb53f 100644 --- a/src/guidellm/scheduler/__init__.py +++ b/src/guidellm/scheduler/__init__.py @@ -12,21 +12,18 @@ UnserializableConstraintInitializer, ) from .environments import Environment, NonDistributedEnvironment -from .objects import ( +from .scheduler import Scheduler +from .schemas import ( BackendInterface, BackendT, - MeasuredRequestTimings, MultiTurnRequestT, - RequestSchedulerTimings, RequestT, ResponseT, - ScheduledRequestInfo, SchedulerMessagingPydanticRegistry, SchedulerState, SchedulerUpdateAction, SchedulerUpdateActionProgress, ) -from .scheduler import Scheduler from .strategies import ( AsyncConstantStrategy, AsyncPoissonStrategy, @@ -62,16 +59,13 @@ "MaxErrorsConstraint", "MaxGlobalErrorRateConstraint", "MaxNumberConstraint", - "MeasuredRequestTimings", "MultiTurnRequestT", "NoDelayRequestTimings", "NonDistributedEnvironment", "PoissonRateRequestTimings", "PydanticConstraintInitializer", - "RequestSchedulerTimings", "RequestT", "ResponseT", - "ScheduledRequestInfo", "ScheduledRequestTimings", "Scheduler", "SchedulerMessagingPydanticRegistry", diff --git a/src/guidellm/scheduler/constraints.py b/src/guidellm/scheduler/constraints.py index c724a74a..94419f38 100644 --- a/src/guidellm/scheduler/constraints.py +++ b/src/guidellm/scheduler/constraints.py @@ -16,12 +16,12 @@ from pydantic import Field, field_validator -from guidellm.scheduler.objects import ( - ScheduledRequestInfo, +from guidellm.scheduler.schemas import ( SchedulerState, SchedulerUpdateAction, SchedulerUpdateActionProgress, ) +from guidellm.schemas import RequestInfo from guidellm.settings import settings from guidellm.utils import InfoMixin, RegistryMixin, StandardBaseModel @@ -46,7 +46,7 @@ class Constraint(Protocol): """Protocol for constraint evaluation functions that control scheduler behavior.""" def __call__( - self, state: SchedulerState, request: ScheduledRequestInfo + self, state: SchedulerState, request: RequestInfo ) -> SchedulerUpdateAction: """ Evaluate constraint against scheduler state and request information. @@ -370,7 +370,7 @@ def create_constraint( def __call__( self, state: SchedulerState, # noqa: ARG002 - request: ScheduledRequestInfo, # noqa: ARG002 + request: RequestInfo, # noqa: ARG002 ) -> SchedulerUpdateAction: """ Raise error since unserializable constraints cannot be invoked. @@ -438,7 +438,7 @@ def create_constraint(self, **kwargs) -> Constraint: # noqa: ARG002 def __call__( self, state: SchedulerState, - request_info: ScheduledRequestInfo, # noqa: ARG002 + request_info: RequestInfo, # noqa: ARG002 ) -> SchedulerUpdateAction: """ Evaluate constraint against current scheduler state and request count. @@ -556,7 +556,7 @@ def create_constraint(self, **kwargs) -> Constraint: # noqa: ARG002 def __call__( self, state: SchedulerState, - request_info: ScheduledRequestInfo, # noqa: ARG002 + request_info: RequestInfo, # noqa: ARG002 ) -> SchedulerUpdateAction: """ Evaluate constraint against current scheduler state and elapsed time. @@ -670,7 +670,7 @@ def create_constraint(self, **kwargs) -> Constraint: # noqa: ARG002 def __call__( self, state: SchedulerState, - request_info: ScheduledRequestInfo, # noqa: ARG002 + request_info: RequestInfo, # noqa: ARG002 ) -> SchedulerUpdateAction: """ Evaluate constraint against current error count. @@ -787,7 +787,7 @@ def create_constraint(self, **kwargs) -> Constraint: # noqa: ARG002 return self.model_copy() # type: ignore[return-value] def __call__( - self, state: SchedulerState, request_info: ScheduledRequestInfo + self, state: SchedulerState, request_info: RequestInfo ) -> SchedulerUpdateAction: """ Evaluate constraint against sliding window error rate. @@ -928,7 +928,7 @@ def create_constraint(self, **kwargs) -> Constraint: # noqa: ARG002 def __call__( self, state: SchedulerState, - request_info: ScheduledRequestInfo, # noqa: ARG002 + request_info: RequestInfo, # noqa: ARG002 ) -> SchedulerUpdateAction: """ Evaluate constraint against global error rate. @@ -1007,7 +1007,7 @@ def info(self) -> dict[str, Any]: def __call__( self, state: SchedulerState, - request_info: ScheduledRequestInfo, # noqa: ARG002 + request_info: RequestInfo, # noqa: ARG002 ) -> SchedulerUpdateAction: create_exceeded = state.created_requests >= self.num_requests processed_exceeded = state.processed_requests >= self.num_requests diff --git a/src/guidellm/scheduler/environments.py b/src/guidellm/scheduler/environments.py index 6234f8f6..ed756a06 100644 --- a/src/guidellm/scheduler/environments.py +++ b/src/guidellm/scheduler/environments.py @@ -25,14 +25,14 @@ ) from guidellm.scheduler.constraints import Constraint -from guidellm.scheduler.objects import ( +from guidellm.scheduler.schemas import ( MultiTurnRequestT, RequestT, ResponseT, - ScheduledRequestInfo, SchedulerState, ) from guidellm.scheduler.strategies import SchedulingStrategy +from guidellm.schemas import RequestInfo from guidellm.settings import settings from guidellm.utils import InfoMixin @@ -93,7 +93,7 @@ async def update_run_iteration( self, response: ResponseT | None, request: RequestT, - request_info: ScheduledRequestInfo, + request_info: RequestInfo, state: SchedulerState, ): """ @@ -131,7 +131,7 @@ async def sync_run_end( tuple[ ResponseT, RequestT | MultiTurnRequestT[RequestT], - ScheduledRequestInfo, + RequestInfo, SchedulerState, ] ]: @@ -162,7 +162,7 @@ class NonDistributedEnvironment(Environment): from guidellm.scheduler import ( MaxNumberConstraint, NonDistributedEnvironment, - ScheduledRequestInfo, + RequestInfo, SchedulerState, SynchronousStrategy, ) @@ -182,7 +182,7 @@ class NonDistributedEnvironment(Environment): for req in local_req: state.processed_requests += 1 await env.update_run_iteration( - f"resp_{req}", req, ScheduledRequestInfo(), state + f"resp_{req}", req, RequestInfo(), state ) async for nonlocal_req in env.sync_run_end(): state.processed_requests += 1 @@ -224,7 +224,7 @@ async def update_run_iteration( self, response: ResponseT | None, request: RequestT, - request_info: ScheduledRequestInfo, + request_info: RequestInfo, state: SchedulerState, ): """ @@ -251,7 +251,7 @@ async def sync_run_end( tuple[ ResponseT, RequestT | MultiTurnRequestT[RequestT], - ScheduledRequestInfo, + RequestInfo, SchedulerState, ] ]: diff --git a/src/guidellm/scheduler/scheduler.py b/src/guidellm/scheduler/scheduler.py index d9bb7c23..e03d6161 100644 --- a/src/guidellm/scheduler/scheduler.py +++ b/src/guidellm/scheduler/scheduler.py @@ -18,16 +18,16 @@ ConstraintsInitializerFactory, ) from guidellm.scheduler.environments import Environment, NonDistributedEnvironment -from guidellm.scheduler.objects import ( +from guidellm.scheduler.schemas import ( BackendInterface, MultiTurnRequestT, RequestT, ResponseT, - ScheduledRequestInfo, SchedulerState, ) from guidellm.scheduler.strategies import SchedulingStrategy from guidellm.scheduler.worker_group import WorkerProcessGroup +from guidellm.schemas import RequestInfo from guidellm.utils.singleton import ThreadSafeSingletonMixin __all__ = ["Scheduler"] @@ -75,7 +75,7 @@ async def run( tuple[ ResponseT | None, RequestT, - ScheduledRequestInfo, + RequestInfo, SchedulerState, ] ]: @@ -146,6 +146,7 @@ async def run( ) yield response, request, request_info, state except Exception as err: # noqa: BLE001 + raise err await env.sync_run_error(err) finally: # Ensure all worker processes are cleaned up for error or completion diff --git a/src/guidellm/scheduler/objects.py b/src/guidellm/scheduler/schemas.py similarity index 61% rename from src/guidellm/scheduler/objects.py rename to src/guidellm/scheduler/schemas.py index b7f2efc3..eab50f14 100644 --- a/src/guidellm/scheduler/objects.py +++ b/src/guidellm/scheduler/schemas.py @@ -10,11 +10,9 @@ from __future__ import annotations import time -import uuid from collections.abc import AsyncIterator from typing import ( Any, - ClassVar, Generic, Literal, Protocol, @@ -22,11 +20,11 @@ Union, ) -from pydantic import Field, computed_field +from pydantic import Field from typing_extensions import TypeAliasType, TypedDict +from guidellm.schemas import RequestInfo from guidellm.utils import ( - PydanticClassRegistryMixin, RegistryMixin, StandardBaseModel, ) @@ -35,12 +33,9 @@ __all__ = [ "BackendInterface", "BackendT", - "MeasuredRequestTimings", "MultiTurnRequestT", - "RequestSchedulerTimings", "RequestT", "ResponseT", - "ScheduledRequestInfo", "SchedulerMessagingPydanticRegistry", "SchedulerState", "SchedulerUpdateAction", @@ -71,167 +66,6 @@ class SchedulerMessagingPydanticRegistry(RegistryMixin[RegistryObjT]): """ -@SchedulerMessagingPydanticRegistry.register() -class RequestSchedulerTimings(StandardBaseModel): - """ - Scheduler-level timing measurements for request lifecycle tracking. - All timestamps are expected to be in Unix time (seconds since epoch). - """ - - targeted_start: float | None = Field( - default=None, - description="When the request was initially targeted for execution", - ) - queued: float | None = Field( - default=None, - description="When the request was placed into the processing queue", - ) - dequeued: float | None = Field( - default=None, - description="When the request was removed from the queue for processing", - ) - scheduled_at: float | None = Field( - default=None, description="When the request was scheduled for processing" - ) - resolve_start: float | None = Field( - default=None, description="When backend resolution of the request began" - ) - resolve_end: float | None = Field( - default=None, description="When backend resolution of the request completed" - ) - finalized: float | None = Field( - default=None, - description="When the request was processed/acknowledged by the scheduler", - ) - - -@SchedulerMessagingPydanticRegistry.register() -class MeasuredRequestTimings(PydanticClassRegistryMixin["MeasuredRequestTimings"]): - """ - Base timing measurements for backend request processing. - All timestamps are expected to be in Unix time (seconds since epoch). - """ - - @classmethod - def __pydantic_schema_base_type__(cls) -> type[MeasuredRequestTimings]: - if cls.__name__ == "MeasuredRequestTimings": - return cls - - return MeasuredRequestTimings - - schema_discriminator: ClassVar[str] = "timings_type" - - timings_type: Literal["measured_request_timings"] = Field( - default="measured_request_timings", - description="Type identifier for the timing measurement", - ) - request_start: float | None = Field( - default=None, description="When the backend began processing the request" - ) - request_end: float | None = Field( - default=None, description="When the backend completed processing the request" - ) - - -@SchedulerMessagingPydanticRegistry.register() -class ScheduledRequestInfo(StandardBaseModel): - """ - Complete request information including status, timings, and metadata. - - Central data structure for tracking request lifecycle from creation through - completion, containing scheduling metadata, timing measurements, and processing - status. Used by scheduler components to coordinate request processing across - distributed worker processes. - - Example: - :: - from guidellm.scheduler.objects import ScheduledRequestInfo - - # Create request info with automatic ID generation - request_info = ScheduledRequestInfo() - request_info.status = "in_progress" - request_info.scheduler_timings.queued = time.time() - - # Check processing completion - if request_info.completed_at: - duration = request_info.completed_at - request_info.started_at - """ - - request_id: str = Field( - description="Unique identifier for the request", - default_factory=lambda: str(uuid.uuid4()), - ) - status: Literal[ - "queued", "pending", "in_progress", "completed", "errored", "cancelled" - ] = Field(description="Current processing status of the request", default="queued") - scheduler_node_id: int = Field( - description="ID/rank of the scheduler node handling the request", - default=-1, - ) - scheduler_process_id: int = Field( - description="ID/rank of the node's scheduler process handling the request", - default=-1, - ) - scheduler_start_time: float = Field( - description="Unix timestamp for the local time when scheduler processing began", - default=-1, - ) - - error: str | None = Field( - default=None, description="Error message if the request.status is 'errored'" - ) - scheduler_timings: RequestSchedulerTimings = Field( - default_factory=RequestSchedulerTimings, - description="Scheduler-level timing measurements for request lifecycle", - ) - request_timings: MeasuredRequestTimings | None = Field( - default=None, - description="Backend-specific timing measurements for request processing", - ) - - @computed_field # type: ignore[misc] - @property - def started_at(self) -> float | None: - """ - Get the effective request processing start time. - - :return: Unix timestamp when processing began, or None if not started. - """ - request_start = ( - self.request_timings.request_start if self.request_timings else None - ) - - return request_start or self.scheduler_timings.resolve_start - - @computed_field # type: ignore[misc] - @property - def completed_at(self) -> float | None: - """ - Get the effective request processing completion time. - - :return: Unix timestamp when processing completed, or None if not completed. - """ - request_end = self.request_timings.request_end if self.request_timings else None - - return request_end or self.scheduler_timings.resolve_end - - def model_copy(self, **kwargs) -> ScheduledRequestInfo: # type: ignore[override] # noqa: ARG002 - """ - Create a deep copy of the request info with copied timing objects. - - :return: New ScheduledRequestInfo instance with independent timing objects - """ - return super().model_copy( - update={ - "scheduler_timings": self.scheduler_timings.model_copy(), - "request_timings": ( - self.request_timings.model_copy() if self.request_timings else None - ), - }, - deep=False, - ) - - class BackendInterface(Protocol, Generic[RequestT, ResponseT]): """ Abstract interface for request processing backends. @@ -297,9 +131,9 @@ async def process_shutdown(self) -> None: async def resolve( self, request: RequestT, - request_info: ScheduledRequestInfo, + request_info: RequestInfo, history: list[tuple[RequestT, ResponseT]] | None = None, - ) -> AsyncIterator[tuple[ResponseT, ScheduledRequestInfo]]: + ) -> AsyncIterator[tuple[ResponseT, RequestInfo]]: """ Process a request and yield incremental response updates. diff --git a/src/guidellm/scheduler/strategies.py b/src/guidellm/scheduler/strategies.py index 8c791671..267001e5 100644 --- a/src/guidellm/scheduler/strategies.py +++ b/src/guidellm/scheduler/strategies.py @@ -17,7 +17,7 @@ from pydantic import Field, PrivateAttr -from guidellm.scheduler.objects import ScheduledRequestInfo +from guidellm.schemas import RequestInfo from guidellm.utils import InfoMixin, PydanticClassRegistryMixin, StandardBaseModel __all__ = [ @@ -83,7 +83,7 @@ def next_offset(self) -> float: """ @abstractmethod - def request_completed(self, request_info: ScheduledRequestInfo): + def request_completed(self, request_info: RequestInfo): """ Handle request completion and update internal timing state. @@ -129,7 +129,7 @@ def next_offset(self) -> float: return self.offset - def request_completed(self, request_info: ScheduledRequestInfo): + def request_completed(self, request_info: RequestInfo): """ Update timing state based on the completed request. @@ -197,7 +197,7 @@ def next_offset(self) -> float: return self.offset + startup_percent * self.startup_duration - def request_completed(self, request_info: ScheduledRequestInfo): + def request_completed(self, request_info: RequestInfo): """ Handle request completion (no action needed for throughput strategy). @@ -236,7 +236,7 @@ def next_offset(self) -> float: return self.offset + interval * num_requests - def request_completed(self, request_info: ScheduledRequestInfo): + def request_completed(self, request_info: RequestInfo): """ Handle request completion (no action needed for constant rate strategy). @@ -283,7 +283,7 @@ def next_offset(self) -> float: return self.offset - def request_completed(self, request_info: ScheduledRequestInfo): + def request_completed(self, request_info: RequestInfo): """ Handle request completion (no action needed for Poisson rate strategy). diff --git a/src/guidellm/scheduler/worker.py b/src/guidellm/scheduler/worker.py index 1832d25f..4b426058 100644 --- a/src/guidellm/scheduler/worker.py +++ b/src/guidellm/scheduler/worker.py @@ -29,15 +29,14 @@ ] = False -from guidellm.scheduler.objects import ( +from guidellm.scheduler.schemas import ( BackendInterface, MultiTurnRequestT, RequestT, ResponseT, - ScheduledRequestInfo, - SchedulerMessagingPydanticRegistry, ) from guidellm.scheduler.strategies import ScheduledRequestTimings +from guidellm.schemas import RequestInfo from guidellm.utils import ( InterProcessMessaging, wait_for_sync_barrier, @@ -77,7 +76,7 @@ def __init__( tuple[ ResponseT | None, RequestT | MultiTurnRequestT[RequestT], - ScheduledRequestInfo, + RequestInfo, ], ], backend: BackendInterface[RequestT, ResponseT], @@ -241,8 +240,7 @@ async def _processing_startup(self): # Get messaging system ready await self.messaging.start( - receive_stop_criteria=[self.requests_generated_event], - pydantic_models=list(SchedulerMessagingPydanticRegistry.registry.values()), + receive_stop_criteria=[self.requests_generated_event] ) self.messaging_started = True @@ -289,56 +287,59 @@ async def _cancel_requests_loop(self): while True: try: request: RequestT - request_info: ScheduledRequestInfo + request_info: RequestInfo request, request_info = await self.messaging.get( timeout=self.messaging.poll_interval ) except asyncio.TimeoutError: continue - request_info.scheduler_node_id = self.messaging.worker_index + request_info.scheduler_node_id = self.messaging.worker_index or -1 request_info.error = "Request was cancelled" - request_info.scheduler_timings.resolve_end = time.time() + request_info.timings.resolve_end = time.time() self._send_update("cancelled", None, request, request_info) async def _process_next_request(self): request: RequestT | MultiTurnRequestT[RequestT] | None = None - request_info: ScheduledRequestInfo | None = None + request_info: RequestInfo | None = None response: ResponseT | None = None try: # Pull request from the queue request, request_info = await self.messaging.get() + if request is None or request_info is None: + raise RuntimeError("Received invalid request or request info") + if isinstance(request, (list, tuple)): raise NotImplementedError("Multi-turn requests are not yet supported") # Calculate targeted start and set pending state for request - request_info.scheduler_node_id = self.messaging.worker_index - request_info.scheduler_timings.dequeued = time.time() + request_info.scheduler_node_id = self.messaging.worker_index or -1 + request_info.timings.dequeued = time.time() target_start = ( request_info.scheduler_start_time + self.request_timings.next_offset() ) - request_info.scheduler_timings.targeted_start = target_start + request_info.timings.targeted_start = target_start self._send_update("pending", response, request, request_info) # Schedule the request current_time = time.time() - request_info.scheduler_timings.scheduled_at = current_time + request_info.timings.scheduled_at = current_time if target_start > current_time: await asyncio.sleep(target_start - current_time) # Adapt delay so that scheduled at reflects the sleep time - request_info.scheduler_timings.scheduled_at = target_start + request_info.timings.scheduled_at = target_start # Process the request with the backend - request_info.scheduler_timings.resolve_start = time.time() + request_info.timings.resolve_start = time.time() self._send_update("in_progress", response, request, request_info) async for resp, info in self.backend.resolve(request, request_info, None): response = resp request_info = info # Complete the request - request_info.scheduler_timings.resolve_end = time.time() + request_info.timings.resolve_end = time.time() self._send_update("completed", response, request, request_info) response = request = request_info = None @@ -346,13 +347,13 @@ async def _process_next_request(self): # Handle cancellation if request is not None and request_info is not None: request_info.error = "Request was cancelled" - request_info.scheduler_timings.resolve_end = time.time() + request_info.timings.resolve_end = time.time() self._send_update("cancelled", response, request, request_info) raise except Exception as exc: # noqa: BLE001 if request is not None and request_info is not None: request_info.error = str(exc) - request_info.scheduler_timings.resolve_end = time.time() + request_info.timings.resolve_end = time.time() self._send_update("errored", response, request, request_info) def _send_update( @@ -362,7 +363,7 @@ def _send_update( ], response: ResponseT | None, request: RequestT | MultiTurnRequestT[RequestT], - request_info: ScheduledRequestInfo, + request_info: RequestInfo, ): prev_status = request_info.status diff --git a/src/guidellm/scheduler/worker_group.py b/src/guidellm/scheduler/worker_group.py index 278fb44d..0f3a1acb 100644 --- a/src/guidellm/scheduler/worker_group.py +++ b/src/guidellm/scheduler/worker_group.py @@ -23,18 +23,17 @@ from typing import Generic, NamedTuple from guidellm.scheduler.constraints import Constraint, RequestsExhaustedConstraint -from guidellm.scheduler.objects import ( +from guidellm.scheduler.schemas import ( BackendInterface, MultiTurnRequestT, RequestT, ResponseT, - ScheduledRequestInfo, - SchedulerMessagingPydanticRegistry, SchedulerState, SchedulerUpdateAction, ) from guidellm.scheduler.strategies import SchedulingStrategy from guidellm.scheduler.worker import WorkerProcess +from guidellm.schemas import RequestInfo from guidellm.settings import settings from guidellm.utils import ( InterProcessMessaging, @@ -130,12 +129,12 @@ def __init__( self.messaging: InterProcessMessaging[ tuple[ RequestT | MultiTurnRequestT[RequestT], - ScheduledRequestInfo, + RequestInfo, ], tuple[ ResponseT | None, RequestT | MultiTurnRequestT[RequestT], - ScheduledRequestInfo, + RequestInfo, SchedulerState, ], ] = None @@ -303,7 +302,6 @@ async def start(self, start_time: float): send_stopped_event=send_requests_stopped_event, send_stop_criteria=[stop_send_requests_event], receive_stop_criteria=[self.shutdown_event], - pydantic_models=list(SchedulerMessagingPydanticRegistry.registry.values()), ) if (wait_time := start_time - time.time()) > 0: @@ -320,7 +318,7 @@ async def request_updates( tuple[ ResponseT | None, RequestT, - ScheduledRequestInfo, + RequestInfo, SchedulerState, ] ]: @@ -485,7 +483,7 @@ def requests_generator( :return: Generator yielding (request, request_info) tuples """ - def _iter(): + def _iter() -> Iterator[RequestT | MultiTurnRequestT[RequestT]]: if requests is not None: yield from requests @@ -494,7 +492,7 @@ def _iter(): yield from cycle_requests count = 0 - request_info: ScheduledRequestInfo = None + request_info: RequestInfo = None for request in _iter(): count += 1 @@ -505,14 +503,14 @@ def _iter(): request_id = request.id else: request_id = str(uuid.uuid4()) - request_info: ScheduledRequestInfo = ScheduledRequestInfo( + request_info: RequestInfo = RequestInfo( request_id=request_id, status="queued", scheduler_process_id=0, scheduler_start_time=self.start_time, ) state_update = self._locked_update(request_info) - request_info.scheduler_timings.queued = time.time() + request_info.timings.queued = time.time() yield (request, request_info) @@ -532,12 +530,12 @@ def received_callback( update: tuple[ ResponseT | None, RequestT | MultiTurnRequestT, - ScheduledRequestInfo, + RequestInfo, ], ) -> tuple[ ResponseT | None, RequestT | MultiTurnRequestT, - ScheduledRequestInfo, + RequestInfo, SchedulerState, ]: """ @@ -585,7 +583,7 @@ def received_callback( def _locked_update( self, - info: ScheduledRequestInfo | None = None, + info: RequestInfo | None = None, **add_constraints: dict[str, Constraint], ) -> _StateUpdate: with self._update_lock: @@ -605,7 +603,7 @@ def _locked_update( state_copy.end_processing_time is not None, ) - def _update_state_request_counts(self, info: ScheduledRequestInfo): + def _update_state_request_counts(self, info: RequestInfo): if info.status == "queued": self._queued_requests.add(info.request_id) self._state.queued_requests = len(self._queued_requests) @@ -642,7 +640,7 @@ def _update_state_request_counts(self, info: ScheduledRequestInfo): else: raise ValueError(f"Unknown request_info status {info.status} for {info}") - def _update_with_constraints(self, info: ScheduledRequestInfo): + def _update_with_constraints(self, info: RequestInfo): actions: dict[str, SchedulerUpdateAction] = { name: const(self._state, info) for name, const in self.constraints.items() } diff --git a/src/guidellm/schemas/__init__.py b/src/guidellm/schemas/__init__.py new file mode 100644 index 00000000..d49cd952 --- /dev/null +++ b/src/guidellm/schemas/__init__.py @@ -0,0 +1,20 @@ +from .info import RequestInfo, RequestTimings +from .request import ( + GenerationRequest, + GenerationRequestArguments, + GenerativeRequestType, + UsageMetrics, +) +from .response import GenerationResponse +from .stats import GenerativeRequestStats + +__all__ = [ + "GenerationRequest", + "GenerationRequestArguments", + "GenerationResponse", + "GenerativeRequestStats", + "GenerativeRequestType", + "RequestInfo", + "RequestTimings", + "UsageMetrics", +] diff --git a/src/guidellm/schemas/info.py b/src/guidellm/schemas/info.py new file mode 100644 index 00000000..3d5e61b7 --- /dev/null +++ b/src/guidellm/schemas/info.py @@ -0,0 +1,132 @@ +""" +Core data structures and interfaces for the GuideLLM scheduler system. + +Provides type-safe abstractions for distributed request processing, timing +measurements, and backend interfaces for benchmarking operations. Central to +the scheduler architecture, enabling request lifecycle tracking, backend +coordination, and state management across distributed worker processes. +""" + +from __future__ import annotations + +import uuid +from typing import Literal + +from pydantic import Field, computed_field + +from guidellm.utils import StandardBaseDict, StandardBaseModel + +__all__ = ["RequestInfo", "RequestTimings"] + + +class RequestTimings(StandardBaseDict): + targeted_start: float | None = Field( + default=None, + description="When the request was initially targeted for execution", + ) + queued: float | None = Field( + default=None, + description="When the request was placed into the processing queue", + ) + dequeued: float | None = Field( + default=None, + description="When the request was removed from the queue for processing", + ) + scheduled_at: float | None = Field( + default=None, description="When the request was scheduled for processing" + ) + resolve_start: float | None = Field( + default=None, description="When backend resolution of the request began" + ) + request_start: float | None = Field( + default=None, description="When the backend began processing the request" + ) + first_iteration: float | None = Field( + default=None, + description="Unix timestamp when the first generation iteration began.", + ) + last_iteration: float | None = Field( + default=None, + description="Unix timestamp when the last generation iteration completed.", + ) + iterations: int | None = Field( + default=None, + description="Total number of streaming update iterations performed.", + ) + request_end: float | None = Field( + default=None, description="When the backend completed processing the request" + ) + resolve_end: float | None = Field( + default=None, description="When backend resolution of the request completed" + ) + finalized: float | None = Field( + default=None, + description="When the request was processed/acknowledged by the scheduler", + ) + + +class RequestInfo(StandardBaseModel): + request_id: str = Field( + description="Unique identifier for the request", + default_factory=lambda: str(uuid.uuid4()), + ) + status: Literal[ + "queued", "pending", "in_progress", "completed", "errored", "cancelled" + ] = Field(description="Current processing status of the request", default="queued") + scheduler_node_id: int = Field( + description="ID/rank of the scheduler node handling the request", + default=-1, + ) + scheduler_process_id: int = Field( + description="ID/rank of the node's scheduler process handling the request", + default=-1, + ) + scheduler_start_time: float = Field( + description="Unix timestamp for the local time when scheduler processing began", + default=-1, + ) + timings: RequestTimings = Field( + default_factory=RequestTimings, + description="Timing measurements for the request lifecycle", + ) + + error: str | None = Field( + default=None, description="Error message if the request.status is 'errored'" + ) + + @computed_field # type: ignore[misc] + @property + def started_at(self) -> float | None: + """ + Get the effective request processing start time. + + :return: Unix timestamp when processing began, or None if not started. + """ + request_start = self.timings.request_start if self.timings else None + + return request_start or self.timings.resolve_start + + @computed_field # type: ignore[misc] + @property + def completed_at(self) -> float | None: + """ + Get the effective request processing completion time. + + :return: Unix timestamp when processing completed, or None if not completed. + """ + request_end = self.timings.request_end if self.timings else None + + return request_end or self.timings.resolve_end + + def model_copy(self, **kwargs) -> RequestInfo: # type: ignore[override] # noqa: ARG002 + """ + Create a deep copy of the request info with copied timing objects. + + :return: New ScheduledRequestInfo instance with independent timing objects + """ + return super().model_copy( + update={ + "timings": self.timings.model_copy(), + }, + deep=False, + ) diff --git a/src/guidellm/schemas/request.py b/src/guidellm/schemas/request.py new file mode 100644 index 00000000..de1f838a --- /dev/null +++ b/src/guidellm/schemas/request.py @@ -0,0 +1,164 @@ +from __future__ import annotations + +import uuid +from typing import Any, Literal + +from pydantic import Field, computed_field + +from guidellm.utils import StandardBaseDict, StandardBaseModel + +__all__ = [ + "GenerationRequest", + "GenerationRequestArguments", + "GenerativeRequestType", + "UsageMetrics", +] + + +GenerativeRequestType = Literal[ + "text_completions", + "chat_completions", + "audio_transcriptions", + "audio_translations", +] + + +class GenerationRequestArguments(StandardBaseDict): + method: str | None = Field( + default=None, + description="The HTTP method to use for the request (e.g., 'POST', 'GET').", + ) + stream: bool | None = Field( + default=None, + description="Whether to stream the response, if applicable.", + ) + headers: dict[str, str] | None = Field( + default=None, + description="HTTP headers to include in the request, if applicable.", + ) + params: dict[str, Any] | None = Field( + default=None, + description="Query parameters to include in the request URL, if applicable.", + ) + body: dict[str, Any] | None = Field( + default=None, + description="Content to include in the main request body.", + ) + files: dict[str, Any] | None = Field( + default=None, + description="Files to include in the request, if applicable.", + ) + + def model_combine( + self, additional: GenerationRequestArguments | dict[str, Any] + ) -> GenerationRequestArguments: + additional_dict = ( + additional.model_dump() + if isinstance(additional, GenerationRequestArguments) + else additional + ) + + for overwrite in ("method", "stream"): + if (val := additional_dict.get(overwrite)) is not None: + setattr(self, overwrite, val) + + for combine in ("headers", "params", "json_body", "files"): + if (val := additional_dict.get(combine)) is not None: + setattr(self, combine, {**getattr(self, combine, {}), **val}) + + return self + + +class UsageMetrics(StandardBaseDict): + # Text stats + text_tokens: int | None = Field( + default=None, description="Number of text tokens processed/generated." + ) + text_words: int | None = Field( + default=None, description="Number of text words processed/generated." + ) + text_characters: int | None = Field( + default=None, description="Number of text characters processed/generated." + ) + text_bytes: int | None = Field( + default=None, description="Number of text bytes processed/generated." + ) + + # Vision image stats + image_tokens: int | None = Field( + default=None, description="Number of image tokens processed/generated." + ) + image_count: int | None = Field( + default=None, description="Number of images processed/generated." + ) + image_pixels: int | None = Field( + default=None, description="Number of image pixels processed/generated." + ) + image_bytes: int | None = Field( + default=None, description="Number of image bytes processed/generated." + ) + + # Vision video stats + video_tokens: int | None = Field( + default=None, description="Number of video tokens processed/generated." + ) + video_frames: int | None = Field( + default=None, description="Number of video frames processed/generated." + ) + video_seconds: float | None = Field( + default=None, description="Duration of video processed/generated in seconds." + ) + video_bytes: int | None = Field( + default=None, description="Number of video bytes processed/generated." + ) + + # Audio stats + audio_tokens: int | None = Field( + default=None, description="Number of audio tokens processed/generated." + ) + audio_samples: int | None = Field( + default=None, description="Number of audio samples processed/generated." + ) + audio_seconds: float | None = Field( + default=None, description="Duration of audio processed/generated in seconds." + ) + audio_bytes: int | None = Field( + default=None, description="Number of audio bytes processed/generated." + ) + + @computed_field # type: ignore[misc] + @property + def total_tokens(self) -> int | None: + return (self.text_tokens or 0) + (self.image_tokens or 0) + ( + self.video_tokens or 0 + ) + (self.audio_tokens or 0) or None + + +class GenerationRequest(StandardBaseModel): + """Request model for backend generation operations.""" + + request_id: str = Field( + default_factory=lambda: str(uuid.uuid4()), + description="Unique identifier for the request.", + ) + request_type: GenerativeRequestType | str = Field( + description=( + "Type of request. If url is not provided in arguments, " + "this will be used to determine the request url." + ), + ) + arguments: GenerationRequestArguments = Field( + description=( + "Payload for the request, structured as a dictionary of arguments to pass " + "to the respective backend method. For example, can contain " + "'json', 'headers', 'files', etc." + ) + ) + input_metrics: UsageMetrics = Field( + default_factory=UsageMetrics, + description="Input statistics including token counts and audio duration.", + ) + output_metrics: UsageMetrics = Field( + default_factory=UsageMetrics, + description="Output statistics including token counts and audio duration.", + ) diff --git a/src/guidellm/schemas/response.py b/src/guidellm/schemas/response.py new file mode 100644 index 00000000..779d5c88 --- /dev/null +++ b/src/guidellm/schemas/response.py @@ -0,0 +1,97 @@ +""" +Backend object models for request and response handling. + +Provides standardized models for generation requests, responses, and timing +information to ensure consistent data handling across different backend +implementations. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from pydantic import Field + +from guidellm.schemas.info import RequestInfo +from guidellm.schemas.request import GenerationRequest, UsageMetrics +from guidellm.utils import StandardBaseModel + +if TYPE_CHECKING: + from guidellm.schemas.stats import GenerativeRequestStats + +__all__ = ["GenerationResponse"] + + +class GenerationResponse(StandardBaseModel): + """Response model for backend generation operations.""" + + request_id: str = Field( + description="Unique identifier matching the original GenerationRequest." + ) + request_args: str | None = Field( + description="Arguments passed to the backend for this request." + ) + text: str | None = Field( + default=None, + description="The generated response text.", + ) + input_metrics: UsageMetrics = Field( + default_factory=UsageMetrics, + description="Token statistics from the input.", + ) + output_metrics: UsageMetrics = Field( + default_factory=UsageMetrics, + description="Token statistics from the generated output.", + ) + + def compile_stats( + self, + request: GenerationRequest, + info: RequestInfo, + prefer_response: bool = True, + ) -> GenerativeRequestStats: + """Compile and return request statistics. + + :param request: The original generation request. + :param info: Metadata and timing information for the request. + :return: A GenerativeRequestStats object containing detailed statistics. + """ + if request.request_id != self.request_id: + raise ValueError("Mismatched request IDs between request and response.") + + if info.request_id != self.request_id: + raise ValueError("Mismatched request IDs between info and response.") + + if info.status != "completed": + # clear out request output metrics if the request failed since those are not valid + request.output_metrics = UsageMetrics() + + base_input = request.input_metrics if prefer_response else self.input_metrics + override_input = ( + self.input_metrics if prefer_response else request.input_metrics + ) + base_output = request.output_metrics if prefer_response else self.output_metrics + override_output = ( + self.output_metrics if prefer_response else request.output_metrics + ) + + input_metrics_dict = base_input.model_dump() + for key, value in override_input.model_dump().items(): + if value is not None: + input_metrics_dict[key] = value + output_metrics_dict = base_output.model_dump() + for key, value in override_output.model_dump().items(): + if value is not None: + output_metrics_dict[key] = value + + return GenerativeRequestStats( + request_id=self.request_id, + request_type=request.request_type, + request_args=str( + request.arguments.model_dump() if request.arguments else {} + ), + output=self.text, + info=info, + input_metrics=UsageMetrics(**input_metrics_dict), + output_metrics=UsageMetrics(**output_metrics_dict), + ) diff --git a/src/guidellm/schemas/stats.py b/src/guidellm/schemas/stats.py new file mode 100644 index 00000000..3ed5de6f --- /dev/null +++ b/src/guidellm/schemas/stats.py @@ -0,0 +1,213 @@ +""" +Benchmark data models and metrics for performance measurement and analysis. + +Provides comprehensive data structures for capturing, storing, and analyzing +benchmark results from scheduler executions. Includes timing measurements, +token statistics, and performance metrics for generative AI workloads. + +Classes: + BenchmarkSchedulerStats: Scheduler timing and performance statistics. + BenchmarkMetrics: Core benchmark metrics and distributions. + BenchmarkRequestStats: Individual request processing statistics. + Benchmark: Base benchmark result container with generic metrics. + GenerativeRequestStats: Request statistics for generative AI workloads. + GenerativeMetrics: Comprehensive metrics for generative benchmarks. + GenerativeBenchmark: Complete generative benchmark results and analysis. + GenerativeBenchmarksReport: Container for multiple benchmark results. + +Type Variables: + BenchmarkMetricsT: Generic benchmark metrics type. + BenchmarkRequestStatsT: Generic request statistics type. + BenchmarkT: Generic benchmark container type. +""" + +from __future__ import annotations + +from typing import Literal + +from pydantic import Field, computed_field + +from guidellm.schemas.info import RequestInfo +from guidellm.schemas.request import GenerativeRequestType, UsageMetrics +from guidellm.utils import StandardBaseDict + +__all__ = ["GenerativeRequestStats"] + + +class GenerativeRequestStats(StandardBaseDict): + """Request statistics for generative AI text generation workloads.""" + + type_: Literal["generative_request_stats"] = "generative_request_stats" + request_id: str = Field(description="Unique identifier for the request") + request_type: GenerativeRequestType | str = Field( + description="Type of generative request: text or chat completion" + ) + request_args: str | None = Field( + default=None, description="Arguments passed to the backend for this request" + ) + output: str | None = Field( + description="Generated text output, if request completed successfully" + ) + info: RequestInfo = Field( + description="Metadata and timing information for the request" + ) + input_metrics: UsageMetrics = Field( + description="Usage statistics for the input prompt" + ) + output_metrics: UsageMetrics = Field( + description="Usage statistics for the generated output" + ) + + # Request stats + @computed_field # type: ignore[misc] + @property + def request_latency(self) -> float | None: + """ + End-to-end request processing latency in seconds. + + :return: Duration from request start to completion, or None if unavailable. + """ + if not self.info.timings.request_end or not self.info.timings.request_start: + return None + + return self.info.timings.request_end - self.info.timings.request_start + + # Genral token stats + @computed_field # type: ignore[misc] + @property + def prompt_tokens(self) -> int | None: + """Number of tokens in the input prompt.""" + return self.input_metrics.text_tokens + + @computed_field # type: ignore[misc] + @property + def input_tokens(self) -> int | None: + """Number of tokens in the input prompt.""" + return self.input_metrics.total_tokens + + @computed_field # type: ignore[misc] + @property + def output_tokens(self) -> int | None: + """Number of tokens in the generated output.""" + return self.output_metrics.total_tokens + + @computed_field # type: ignore[misc] + @property + def total_tokens(self) -> int | None: + """ + Total token count including prompt and output tokens. + + :return: Sum of prompt and output tokens, or None if either is unavailable. + """ + input_tokens = self.input_metrics.total_tokens + output_tokens = self.output_metrics.total_tokens + + if input_tokens is None and output_tokens is None: + return None + + return (input_tokens or 0) + (output_tokens or 0) + + @computed_field # type: ignore[misc] + @property + def time_to_first_token_ms(self) -> float | None: + """ + Time to first token generation in milliseconds. + + :return: Latency from request start to first token, or None if unavailable. + """ + if ( + not self.info.timings.first_iteration + or not self.info.timings.request_start + or self.info.timings.first_iteration == self.info.timings.last_iteration + ): + return None + + return 1000 * ( + self.info.timings.first_iteration - self.info.timings.request_start + ) + + @computed_field # type: ignore[misc] + @property + def time_per_output_token_ms(self) -> float | None: + """ + Average time per output token in milliseconds. + + Includes time for first token and all subsequent tokens. + + :return: Average milliseconds per output token, or None if unavailable. + """ + if ( + not self.info.timings.request_start + or not self.info.timings.last_iteration + or not self.output_metrics.total_tokens + ): + return None + + return ( + 1000 + * (self.info.timings.last_iteration - self.info.timings.request_start) + / self.output_metrics.total_tokens + ) + + @computed_field # type: ignore[misc] + @property + def inter_token_latency_ms(self) -> float | None: + """ + Average inter-token latency in milliseconds. + + Measures time between token generations, excluding first token. + + :return: Average milliseconds between tokens, or None if unavailable. + """ + if ( + not self.info.timings.first_iteration + or not self.info.timings.last_iteration + or not self.output_metrics.total_tokens + or self.output_metrics.total_tokens <= 1 + ): + return None + + return ( + 1000 + * (self.info.timings.last_iteration - self.info.timings.first_iteration) + / (self.output_metrics.total_tokens - 1) + ) + + @computed_field # type: ignore[misc] + @property + def tokens_per_second(self) -> float | None: + """ + Overall token throughput including prompt and output tokens. + + :return: Total tokens per second, or None if unavailable. + """ + if not (latency := self.request_latency) or self.total_tokens is None: + return None + + return self.total_tokens / latency + + @computed_field # type: ignore[misc] + @property + def output_tokens_per_second(self) -> float | None: + """ + Output token generation throughput. + + :return: Output tokens per second, or None if unavailable. + """ + if not (latency := self.request_latency) or self.output_tokens is None: + return None + + return self.output_tokens / latency + + @computed_field # type: ignore[misc] + @property + def output_tokens_per_iteration(self) -> float | None: + """ + Average output tokens generated per iteration. + + :return: Output tokens per iteration, or None if unavailable. + """ + if self.output_tokens is None or not self.info.timings.iterations: + return None + + return self.output_tokens / self.info.timings.iterations diff --git a/src/guidellm/utils/statistics.py b/src/guidellm/utils/statistics.py index c820de9d..4dfa7500 100644 --- a/src/guidellm/utils/statistics.py +++ b/src/guidellm/utils/statistics.py @@ -255,6 +255,7 @@ def from_values( def from_request_times( requests: list[tuple[float, float]], distribution_type: Literal["concurrency", "rate"], + weights: list[float] | None = None, include_cdf: bool = False, epsilon: float = 1e-6, ) -> DistributionSummary: @@ -273,66 +274,85 @@ def from_request_times( :return: DistributionSummary with timing-based statistical metrics :raises ValueError: If distribution_type is not "concurrency" or "rate" """ + if not weights: + weights = [1.0] * len(requests) + + if len(requests) != len(weights): + raise ValueError( + "The length of requests and weights must be the same.", + ) + + # First convert to timing events based on type + events: list[tuple[float, float]] = [] + if distribution_type == "concurrency": - # convert to delta changes based on when requests were running - time_deltas: dict[float, int] = defaultdict(int) - for start, end in requests: - time_deltas[start] += 1 - time_deltas[end] -= 1 - - # convert to the events over time measuring concurrency changes - events = [] - active = 0 - - for time, delta in sorted(time_deltas.items()): - active += delta - events.append((time, active)) + # For concurrency, each request adds to concurrency at start + # and subtracts at end + for (start, end), weight in zip(requests, weights): + events.append((start, weight)) + events.append((end, -1 * weight)) elif distribution_type == "rate": - # convert to events for when requests finished - global_start = min(start for start, _ in requests) if requests else 0 - events = [(global_start, 1)] + [(end, 1) for _, end in requests] + # For rate, each request is added at the end time only + events.append((min(0, *(start for start, _ in requests)), 0.0)) + for (_, end), weight in zip(requests, weights): + events.append((end, weight)) else: raise ValueError( f"Invalid distribution_type '{distribution_type}'. " "Must be 'concurrency' or 'rate'." ) - # combine any events that are very close together - flattened_events: list[tuple[float, float]] = [] - for time, val in sorted(events): - last_time, last_val = ( - flattened_events[-1] if flattened_events else (None, None) - ) + # Combine any events within epsilon of each other for stability + sorted_events = sorted(events, key=lambda event: event[0]) + flattened_events: list[tuple[float, float]] = ( + [sorted_events.pop(0)] if sorted_events else [] + ) + last_time = flattened_events[0][0] if flattened_events else 0.0 - if ( - last_time is not None - and last_val is not None - and abs(last_time - time) <= epsilon - ): + for time, val in sorted_events: + if abs(time - last_time) <= epsilon: + last_val = flattened_events[-1][1] flattened_events[-1] = (last_time, last_val + val) else: + last_time = time flattened_events.append((time, val)) - # convert to value distribution function + # Convert events to value distribution function distribution: dict[float, float] = defaultdict(float) - for ind in range(len(flattened_events) - 1): - start_time, value = flattened_events[ind] - end_time, _ = flattened_events[ind + 1] - duration = end_time - start_time - - if distribution_type == "concurrency": - # weight the concurrency value by the duration + if distribution_type == "concurrency": + # For concurrency, convert to active concurrency over time + active = 0.0 + for ind in range(len(flattened_events)): + time, change = flattened_events[ind] + active += change + flattened_events[ind] = (time, active) + + # Then convert to distribution by weighting each concurrency + # by duration to next event (last event is 0 concurrency) + for ind in range(len(flattened_events) - 1): + time, value = flattened_events[ind] + next_time = flattened_events[ind + 1][0] + duration = next_time - time distribution[value] += duration - elif distribution_type == "rate": - # weight the rate value by the duration - rate = value / duration + elif distribution_type == "rate": + # For rate, convert to distribution by converting each value + # to a rate (value/duration) weighted by duration from previous + # (first event is 0 rate) + for ind in range(1, len(flattened_events)): + time, value = flattened_events[ind] + prev_time = flattened_events[ind - 1][0] + duration = time - prev_time + rate = value / duration if duration > 0 else 0.0 distribution[rate] += duration - - distribution_list: list[tuple[float, float]] = sorted(distribution.items()) + else: + raise ValueError( + f"Invalid distribution_type '{distribution_type}'. " + "Must be 'concurrency' or 'rate'." + ) return DistributionSummary.from_distribution_function( - distribution=distribution_list, + distribution=sorted(distribution.items()), include_cdf=include_cdf, ) @@ -562,6 +582,7 @@ def from_request_times( request_types: list[Literal["successful", "incomplete", "error"]], requests: list[tuple[float, float]], distribution_type: Literal["concurrency", "rate"], + weights: list[float] | None = None, include_cdf: bool = False, epsilon: float = 1e-6, ) -> StatusDistributionSummary: @@ -602,65 +623,78 @@ def from_request_times( f"Got {len(request_types)} and {len(requests)} instead.", ) - _, successful_requests = ( + if weights is None: + weights = [1.0] * len(requests) + + if len(requests) != len(weights): + raise ValueError( + "The length of requests and weights must be the same." + f"Got {len(requests)} and {len(weights)} instead.", + ) + + _, successful_requests, successful_weights = ( zip(*successful) if ( successful := list( filter( lambda val: val[0] == "successful", - zip(request_types, requests), + zip(request_types, requests, weights), ) ) ) - else ([], []) + else ([], [], []) ) - _, incomplete_requests = ( + _, incomplete_requests, incomplete_weights = ( zip(*incomplete) if ( incomplete := list( filter( lambda val: val[0] == "incomplete", - zip(request_types, requests), + zip(request_types, requests, weights), ) ) ) - else ([], []) + else ([], [], []) ) - _, errored_requests = ( + _, errored_requests, errored_weights = ( zip(*errored) if ( errored := list( filter( lambda val: val[0] == "error", - zip(request_types, requests), + zip(request_types, requests, weights), ) ) ) - else ([], []) + else ([], [], []) ) return StatusDistributionSummary( total=DistributionSummary.from_request_times( requests, distribution_type=distribution_type, + weights=weights, include_cdf=include_cdf, epsilon=epsilon, ), successful=DistributionSummary.from_request_times( successful_requests, # type: ignore[arg-type] distribution_type=distribution_type, + weights=successful_weights, # type: ignore[arg-type] include_cdf=include_cdf, epsilon=epsilon, ), incomplete=DistributionSummary.from_request_times( incomplete_requests, # type: ignore[arg-type] distribution_type=distribution_type, + weights=incomplete_weights, # type: ignore[arg-type] include_cdf=include_cdf, epsilon=epsilon, ), errored=DistributionSummary.from_request_times( errored_requests, # type: ignore[arg-type] distribution_type=distribution_type, + weights=errored_weights, # type: ignore[arg-type] include_cdf=include_cdf, epsilon=epsilon, ), diff --git a/src/guidellm/utils/text.py b/src/guidellm/utils/text.py index 8385ec7b..1138c08f 100644 --- a/src/guidellm/utils/text.py +++ b/src/guidellm/utils/text.py @@ -13,7 +13,6 @@ import gzip import re import textwrap -from importlib.resources import as_file, files # type: ignore[attr-defined] from pathlib import Path from typing import Any @@ -21,7 +20,6 @@ import httpx from loguru import logger -from guidellm import data as package_data from guidellm.settings import settings from guidellm.utils.console import Colors @@ -238,15 +236,6 @@ def load_text(data: str | Path, encoding: str | None = None) -> str: response.raise_for_status() return response.text - # check package data - if isinstance(data, str) and data.startswith("data:"): - resource_path = files(package_data).joinpath(data[5:]) - with ( - as_file(resource_path) as resource_file, - gzip.open(resource_file, "rt", encoding=encoding) as file, - ): - return file.read() - # check gzipped files if isinstance(data, str) and data.endswith(".gz"): with gzip.open(data, "rt", encoding=encoding) as file: diff --git a/tests/unit/backend/test_backend.py b/tests/unit/backend/test_backend.py index 49b65077..79c0d932 100644 --- a/tests/unit/backend/test_backend.py +++ b/tests/unit/backend/test_backend.py @@ -13,11 +13,11 @@ import pytest from guidellm.backends.backend import Backend, BackendType -from guidellm.backends.objects import ( +from guidellm.scheduler import BackendInterface, ScheduledRequestInfo +from guidellm.schemas.response import ( GenerationRequest, GenerationRequestTimings, ) -from guidellm.scheduler import BackendInterface, ScheduledRequestInfo from guidellm.utils import RegistryMixin diff --git a/tests/unit/backend/test_objects.py b/tests/unit/backend/test_objects.py index 34a6350c..1831c459 100644 --- a/tests/unit/backend/test_objects.py +++ b/tests/unit/backend/test_objects.py @@ -9,12 +9,12 @@ import pytest from pydantic import ValidationError -from guidellm.backends.objects import ( +from guidellm.scheduler import MeasuredRequestTimings +from guidellm.schemas.response import ( GenerationRequest, GenerationRequestTimings, GenerationResponse, ) -from guidellm.scheduler import MeasuredRequestTimings from guidellm.utils import StandardBaseModel diff --git a/tests/unit/backend/test_openai_backend.py b/tests/unit/backend/test_openai_backend.py index 7c7f528d..a83c411a 100644 --- a/tests/unit/backend/test_openai_backend.py +++ b/tests/unit/backend/test_openai_backend.py @@ -15,13 +15,13 @@ from PIL import Image from guidellm.backends.backend import Backend -from guidellm.backends.objects import ( +from guidellm.backends.openai import OpenAIHTTPBackend, UsageStats +from guidellm.scheduler import ScheduledRequestInfo +from guidellm.schemas.response import ( GenerationRequest, GenerationRequestTimings, GenerationResponse, ) -from guidellm.backends.openai import OpenAIHTTPBackend, UsageStats -from guidellm.scheduler import ScheduledRequestInfo def async_timeout(delay): diff --git a/tests/unit/mock_benchmark.py b/tests/unit/mock_benchmark.py index d846767d..7ce73a67 100644 --- a/tests/unit/mock_benchmark.py +++ b/tests/unit/mock_benchmark.py @@ -7,8 +7,8 @@ GenerativeMetrics, GenerativeRequestStats, ) -from guidellm.benchmark.objects import BenchmarkerDict, SchedulerDict from guidellm.benchmark.profile import SynchronousProfile +from guidellm.benchmark.schemas import BenchmarkerDict, SchedulerDict from guidellm.scheduler import ScheduledRequestInfo, SchedulerState, SynchronousStrategy from guidellm.utils import ( DistributionSummary, diff --git a/tests/unit/utils/test_encoding.py b/tests/unit/utils/test_encoding.py index cc4600cf..69587323 100644 --- a/tests/unit/utils/test_encoding.py +++ b/tests/unit/utils/test_encoding.py @@ -6,11 +6,11 @@ import pytest from pydantic import BaseModel, Field -from guidellm.backends.objects import ( +from guidellm.scheduler.schemas import RequestSchedulerTimings, ScheduledRequestInfo +from guidellm.schemas.response import ( GenerationRequest, GenerationResponse, ) -from guidellm.scheduler.objects import RequestSchedulerTimings, ScheduledRequestInfo from guidellm.utils.encoding import Encoder, MessageEncoding, Serializer From b2436641458defda47f17a2335828b6e7816e1b0 Mon Sep 17 00:00:00 2001 From: Samuel Monson Date: Fri, 10 Oct 2025 16:47:36 -0400 Subject: [PATCH 20/57] Move asyncio timeout to common location Signed-off-by: Samuel Monson --- tests/unit/backends/test_backend.py | 14 +------ tests/unit/backends/test_openai_backend.py | 14 +------ tests/unit/scheduler/test_scheduler.py | 15 +------- tests/unit/scheduler/test_worker.py | 13 +------ tests/unit/scheduler/test_worker_group.py | 13 +------ tests/unit/testing_utils.py | 44 ++++++++++++++++++++++ tests/unit/utils/test_messaging.py | 15 +------- tests/unit/utils/test_synchronous.py | 15 +------- 8 files changed, 51 insertions(+), 92 deletions(-) create mode 100644 tests/unit/testing_utils.py diff --git a/tests/unit/backends/test_backend.py b/tests/unit/backends/test_backend.py index ebd0da87..d5a4b955 100644 --- a/tests/unit/backends/test_backend.py +++ b/tests/unit/backends/test_backend.py @@ -4,9 +4,7 @@ from __future__ import annotations -import asyncio from collections.abc import AsyncIterator -from functools import wraps from typing import Any from unittest.mock import Mock, patch @@ -19,17 +17,7 @@ ) from guidellm.scheduler import BackendInterface, ScheduledRequestInfo from guidellm.utils import RegistryMixin - - -def async_timeout(delay): - def decorator(func): - @wraps(func) - async def new_func(*args, **kwargs): - return await asyncio.wait_for(func(*args, **kwargs), timeout=delay) - - return new_func - - return decorator +from tests.unit.testing_utils import async_timeout def test_backend_type(): diff --git a/tests/unit/backends/test_openai_backend.py b/tests/unit/backends/test_openai_backend.py index 2180b501..834cd0e9 100644 --- a/tests/unit/backends/test_openai_backend.py +++ b/tests/unit/backends/test_openai_backend.py @@ -4,9 +4,7 @@ from __future__ import annotations -import asyncio import base64 -from functools import wraps from pathlib import Path from unittest.mock import AsyncMock, Mock, patch @@ -22,17 +20,7 @@ ) from guidellm.backends.openai import OpenAIHTTPBackend, UsageStats from guidellm.scheduler import ScheduledRequestInfo - - -def async_timeout(delay): - def decorator(func): - @wraps(func) - async def new_func(*args, **kwargs): - return await asyncio.wait_for(func(*args, **kwargs), timeout=delay) - - return new_func - - return decorator +from tests.unit.testing_utils import async_timeout def test_usage_stats(): diff --git a/tests/unit/scheduler/test_scheduler.py b/tests/unit/scheduler/test_scheduler.py index 33efc27f..407dab6c 100644 --- a/tests/unit/scheduler/test_scheduler.py +++ b/tests/unit/scheduler/test_scheduler.py @@ -4,7 +4,6 @@ import inspect import random import uuid -from functools import wraps from typing import Any, Generic import pytest @@ -20,19 +19,7 @@ SynchronousStrategy, ) from guidellm.utils.singleton import ThreadSafeSingletonMixin - - -def async_timeout(delay: float): - """Decorator to add timeout to async test functions.""" - - def decorator(func): - @wraps(func) - async def new_func(*args, **kwargs): - return await asyncio.wait_for(func(*args, **kwargs), timeout=delay) - - return new_func - - return decorator +from tests.unit.testing_utils import async_timeout class MockRequest(BaseModel): diff --git a/tests/unit/scheduler/test_worker.py b/tests/unit/scheduler/test_worker.py index b62d66d5..b6624483 100644 --- a/tests/unit/scheduler/test_worker.py +++ b/tests/unit/scheduler/test_worker.py @@ -5,7 +5,6 @@ import random import time from dataclasses import dataclass -from functools import wraps from multiprocessing import Barrier, Event, Process from multiprocessing.synchronize import Barrier as ProcessingBarrier from multiprocessing.synchronize import Event as ProcessingEvent @@ -27,21 +26,11 @@ WorkerProcess, ) from guidellm.utils import InterProcessMessagingQueue +from tests.unit.testing_utils import async_timeout STANDARD_NUM_REQUESTS: int = 200 -def async_timeout(delay): - def decorator(func): - @wraps(func) - async def new_func(*args, **kwargs): - return await asyncio.wait_for(func(*args, **kwargs), timeout=delay) - - return new_func - - return decorator - - @dataclass class TimingsBounds: exact: float | None = None diff --git a/tests/unit/scheduler/test_worker_group.py b/tests/unit/scheduler/test_worker_group.py index 80bb6c23..2b8176e7 100644 --- a/tests/unit/scheduler/test_worker_group.py +++ b/tests/unit/scheduler/test_worker_group.py @@ -3,7 +3,6 @@ import asyncio import inspect import time -from functools import wraps from multiprocessing.context import BaseContext from multiprocessing.managers import BaseManager from multiprocessing.process import BaseProcess @@ -30,17 +29,7 @@ ) from guidellm.scheduler.worker_group import WorkerGroupState from guidellm.utils import InterProcessMessaging - - -def async_timeout(delay): - def decorator(func): - @wraps(func) - async def new_func(*args, **kwargs): - return await asyncio.wait_for(func(*args, **kwargs), timeout=delay) - - return new_func - - return decorator +from tests.unit.testing_utils import async_timeout class MockRequestTimings(MeasuredRequestTimings): diff --git a/tests/unit/testing_utils.py b/tests/unit/testing_utils.py new file mode 100644 index 00000000..11b563b5 --- /dev/null +++ b/tests/unit/testing_utils.py @@ -0,0 +1,44 @@ +"""Common test utilities for async testing.""" + +from __future__ import annotations + +import asyncio +from collections.abc import Awaitable, Callable +from functools import wraps +from typing import Any, TypeVar + +import pytest + +# Type variables for proper typing +F = TypeVar("F", bound=Callable[..., Awaitable[Any]]) + + +def async_timeout(delay: float = 10.0, hard_fail: bool = False) -> Callable[[F], F]: + """ + Decorator to add timeout to async test functions. + + Uses a longer default timeout (30s) to reduce intermittent failures + while still catching truly hanging tests. + + Args: + delay: Timeout in seconds (default: 30.0) + + Returns: + Decorated function with timeout applied + """ + + def decorator(func: F) -> F: + @wraps(func) + async def wrapper(*args: Any, **kwargs: Any) -> Any: + try: + return await asyncio.wait_for(func(*args, **kwargs), timeout=delay) + except asyncio.TimeoutError: + msg = f"Test {func.__name__} timed out after {delay} seconds" + if hard_fail: + pytest.fail(msg) + else: + pytest.xfail(msg) + + return wrapper # type: ignore[return-value] + + return decorator diff --git a/tests/unit/utils/test_messaging.py b/tests/unit/utils/test_messaging.py index d6b3283d..7b021aa6 100644 --- a/tests/unit/utils/test_messaging.py +++ b/tests/unit/utils/test_messaging.py @@ -3,7 +3,6 @@ import asyncio import multiprocessing import threading -from functools import wraps from typing import Any, TypeVar import culsans @@ -22,19 +21,7 @@ InterProcessMessagingQueue, ) from guidellm.utils.messaging import ReceiveMessageT, SendMessageT - - -def async_timeout(delay: float): - """Decorator to add timeout to async test functions.""" - - def decorator(func): - @wraps(func) - async def new_func(*args, **kwargs): - return await asyncio.wait_for(func(*args, **kwargs), timeout=delay) - - return new_func - - return decorator +from tests.unit.testing_utils import async_timeout class MockMessage(BaseModel): diff --git a/tests/unit/utils/test_synchronous.py b/tests/unit/utils/test_synchronous.py index 7acd5b4a..eebd6a52 100644 --- a/tests/unit/utils/test_synchronous.py +++ b/tests/unit/utils/test_synchronous.py @@ -3,7 +3,6 @@ import asyncio import multiprocessing import threading -from functools import wraps from multiprocessing.synchronize import Barrier as ProcessingBarrier from multiprocessing.synchronize import Event as ProcessingEvent from typing import get_args @@ -16,19 +15,7 @@ wait_for_sync_event, wait_for_sync_objects, ) - - -def async_timeout(delay: float): - """Decorator to add timeout to async functions.""" - - def decorator(func): - @wraps(func) - async def new_func(*args, **kwargs): - return await asyncio.wait_for(func(*args, **kwargs), timeout=delay) - - return new_func - - return decorator +from tests.unit.testing_utils import async_timeout def test_sync_object_types_alias(): From cfcbd13342b3e33a72e2a8f8427a3ba7b47ea3f4 Mon Sep 17 00:00:00 2001 From: Samuel Monson Date: Fri, 10 Oct 2025 16:48:21 -0400 Subject: [PATCH 21/57] Fix duplicate timeout in openai backend tests Signed-off-by: Samuel Monson --- tests/unit/backends/test_openai_backend.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/tests/unit/backends/test_openai_backend.py b/tests/unit/backends/test_openai_backend.py index 834cd0e9..724075e8 100644 --- a/tests/unit/backends/test_openai_backend.py +++ b/tests/unit/backends/test_openai_backend.py @@ -218,7 +218,6 @@ def test_header_building(self): @pytest.mark.smoke @pytest.mark.asyncio @async_timeout(10.0) - @async_timeout(5.0) async def test_info(self): """Test info method.""" backend = OpenAIHTTPBackend( @@ -238,7 +237,6 @@ async def test_info(self): @pytest.mark.smoke @pytest.mark.asyncio @async_timeout(10.0) - @async_timeout(5.0) async def test_process_startup(self): """Test process startup.""" backend = OpenAIHTTPBackend(target="http://test") @@ -255,7 +253,6 @@ async def test_process_startup(self): @pytest.mark.smoke @pytest.mark.asyncio @async_timeout(10.0) - @async_timeout(5.0) async def test_process_startup_already_started(self): """Test process startup when already started.""" backend = OpenAIHTTPBackend(target="http://test") @@ -267,7 +264,6 @@ async def test_process_startup_already_started(self): @pytest.mark.smoke @pytest.mark.asyncio @async_timeout(10.0) - @async_timeout(5.0) async def test_process_shutdown(self): """Test process shutdown.""" backend = OpenAIHTTPBackend(target="http://test") @@ -284,7 +280,6 @@ async def test_process_shutdown(self): @pytest.mark.smoke @pytest.mark.asyncio @async_timeout(10.0) - @async_timeout(5.0) async def test_process_shutdown_not_started(self): """Test process shutdown when not started.""" backend = OpenAIHTTPBackend(target="http://test") @@ -295,7 +290,6 @@ async def test_process_shutdown_not_started(self): @pytest.mark.sanity @pytest.mark.asyncio @async_timeout(10.0) - @async_timeout(5.0) async def test_check_in_process(self): """Test _check_in_process method.""" backend = OpenAIHTTPBackend(target="http://test") @@ -313,7 +307,6 @@ async def test_check_in_process(self): @pytest.mark.sanity @pytest.mark.asyncio @async_timeout(10.0) - @async_timeout(5.0) async def test_available_models(self): """Test available_models method.""" backend = OpenAIHTTPBackend(target="http://test") @@ -334,7 +327,6 @@ async def test_available_models(self): @pytest.mark.sanity @pytest.mark.asyncio @async_timeout(10.0) - @async_timeout(5.0) async def test_default_model(self): """Test default_model method.""" # Test when model is already set @@ -358,7 +350,6 @@ async def test_default_model(self): @pytest.mark.regression @pytest.mark.asyncio @async_timeout(10.0) - @async_timeout(10.0) async def test_validate_with_model(self): """Test validate method when model is set.""" backend = OpenAIHTTPBackend(target="http://test", model="test-model") From 9ca2dba919f03afd2ade929ac1f3d81b36135f67 Mon Sep 17 00:00:00 2001 From: Jared O'Connell Date: Fri, 10 Oct 2025 15:23:19 -0400 Subject: [PATCH 22/57] Force time zone in tests Signed-off-by: Jared O'Connell --- tests/unit/utils/test_functions.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/unit/utils/test_functions.py b/tests/unit/utils/test_functions.py index 3b353759..3e542ca8 100644 --- a/tests/unit/utils/test_functions.py +++ b/tests/unit/utils/test_functions.py @@ -1,5 +1,6 @@ from __future__ import annotations +import time from datetime import datetime import pytest @@ -180,6 +181,17 @@ def test_single_value(self): assert result == 3.0 +@pytest.fixture(autouse=True) +def force_us_eastern_timezone(monkeypatch): + """ + Forces the timezone to US/Eastern for the duration of a test. + This ensures that timestamp formatting is consistent across all environments. + + ## WRITTEN BY AI ## + """ + monkeypatch.setenv("TZ", "America/New_York") + time.tzset() # Propagates the change to the underlying C library + class TestSafeFormatTimestamp: """Test suite for safe_format_timestamp function.""" From 8d20525c4cf4117553961a949b8166c7dc99a347 Mon Sep 17 00:00:00 2001 From: Samuel Monson Date: Fri, 10 Oct 2025 17:16:16 -0400 Subject: [PATCH 23/57] Fix function doc Signed-off-by: Samuel Monson --- tests/unit/testing_utils.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/unit/testing_utils.py b/tests/unit/testing_utils.py index 11b563b5..c6b8c513 100644 --- a/tests/unit/testing_utils.py +++ b/tests/unit/testing_utils.py @@ -17,11 +17,8 @@ def async_timeout(delay: float = 10.0, hard_fail: bool = False) -> Callable[[F], """ Decorator to add timeout to async test functions. - Uses a longer default timeout (30s) to reduce intermittent failures - while still catching truly hanging tests. - Args: - delay: Timeout in seconds (default: 30.0) + delay: Timeout in seconds (default: 10.0) Returns: Decorated function with timeout applied From 3c646d4ceaee24356f569da765c6695a1a6a1bd7 Mon Sep 17 00:00:00 2001 From: Mark Kurtz Date: Mon, 13 Oct 2025 13:02:44 -0400 Subject: [PATCH 24/57] runnable state for multi modal refactor --- src/guidellm/__main__.py | 45 +++++------ src/guidellm/benchmark/progress.py | 78 ++++++++++--------- src/guidellm/benchmark/scenario.py | 11 +-- src/guidellm/benchmark/schemas.py | 69 ++++++++++------ src/guidellm/benchmark/types.py | 2 - src/guidellm/data/deserializers/file.py | 3 +- .../data/deserializers/huggingface.py | 3 +- src/guidellm/data/deserializers/memory.py | 3 +- src/guidellm/data/deserializers/synthetic.py | 6 +- src/guidellm/data/loaders.py | 2 +- src/guidellm/utils/registry.py | 2 +- src/guidellm/utils/statistics.py | 19 ++--- .../unit/data/deserializers/test_synthetic.py | 4 +- 13 files changed, 130 insertions(+), 117 deletions(-) diff --git a/src/guidellm/__main__.py b/src/guidellm/__main__.py index 7e37fb70..97f3e436 100644 --- a/src/guidellm/__main__.py +++ b/src/guidellm/__main__.py @@ -43,7 +43,7 @@ benchmark_generative_text, reimport_benchmarks_report, ) -from guidellm.benchmark.scenario import GenerativeTextScenario, get_builtin_scenarios +from guidellm.benchmark.scenario import GenerativeTextScenario from guidellm.mock_server import MockServer, MockServerConfig from guidellm.preprocess.dataset import ShortPromptStrategy, process_dataset from guidellm.scheduler import StrategyType @@ -123,25 +123,25 @@ def benchmark(): help="Run a benchmark against a generative model using the specified arguments.", context_settings={"auto_envvar_prefix": "GUIDELLM"}, ) -@click.option( - "--scenario", - type=cli_tools.Union( - click.Path( - exists=True, - readable=True, - file_okay=True, - dir_okay=False, - path_type=Path, - ), - click.Choice(get_builtin_scenarios()), - ), - default=None, - help=( - "The name of a builtin scenario or path to a config file. " - "Missing values from the config will use defaults. " - "Options specified on the commandline will override the scenario." - ), -) +# @click.option( +# "--scenario", +# type=cli_tools.Union( +# click.Path( +# exists=True, +# readable=True, +# file_okay=True, +# dir_okay=False, +# path_type=Path, +# ), +# click.Choice(get_builtin_scenarios()), +# ), +# default=None, +# help=( +# "The name of a builtin scenario or path to a config file. " +# "Missing values from the config will use defaults. " +# "Options specified on the commandline will override the scenario." +# ), +# ) @click.option( "--target", type=str, @@ -347,11 +347,6 @@ def benchmark(): help="Set this flag to display stats for the processes running the benchmarks", ) # Aggregators configuration -@click.option( - "--output-extras", - callback=cli_tools.parse_json, - help="A JSON string of extra data to save with the output benchmarks", -) @click.option( "--warmup", "--warmup-percent", # legacy alias diff --git a/src/guidellm/benchmark/progress.py b/src/guidellm/benchmark/progress.py index f9e29abb..5a88d696 100644 --- a/src/guidellm/benchmark/progress.py +++ b/src/guidellm/benchmark/progress.py @@ -808,7 +808,7 @@ def start(self, strategy: SchedulingStrategy): def update( self, - aggregator_update: EstimatedBenchmarkState, + estimated_state: EstimatedBenchmarkState, scheduler_state: SchedulerState, ): self.progress = ( @@ -816,64 +816,66 @@ def update( if scheduler_state.remaining_fraction is not None else 0.0 ) - status: Literal["in_warmup", "in_progress", "in_cooldown"] | None = ( - "in_progress" # Need to handle requests_in_* isn't in aggregator_update - ) - if aggregator_update.get("requests_in_warmup"): - status = "in_warmup" - elif aggregator_update.get("requests_in_cooldown"): - status = "in_cooldown" self._update_processing_states( - benchmark_status=status, + benchmark_status=estimated_state.get_metric( + group=EstimatedBenchmarkState.benchmark_state_group, + key="status", + default=None, + ), start_time=scheduler_state.start_time, successful_requests=scheduler_state.successful_requests, cancelled_requests=scheduler_state.cancelled_requests, errored_requests=scheduler_state.errored_requests, ) self._update_request_stats( - request_concurrency=aggregator_update.get_metric( - key="requests", type_="avg", prefix="completed" + request_concurrency=estimated_state.get_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="concurrency_requests", ), - requests_per_second=aggregator_update.get_metric( - key="requests", - type_="rate", - prefix="completed", + requests_per_second=estimated_state.get_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="completed_requests_per_second", ), - request_latency=aggregator_update.get_metric( - key="request_latency", type_="avg", prefix="completed" + request_latency=estimated_state.get_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="completed_request_latency", ), ) self._update_token_stats( - output_tokens=aggregator_update.get_metric( - key="output_tokens", type_="avg", prefix="completed" + output_tokens=estimated_state.get_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="completed_output_tokens_total", ), - output_tokens_rate=aggregator_update.get_metric( - key="output_tokens", type_="rate" + output_tokens_rate=estimated_state.get_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="completed_output_tokens", ), - prompt_tokens=aggregator_update.get_metric( - key="prompt_tokens", type_="avg", prefix="completed" + prompt_tokens=estimated_state.get_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="completed_input_tokens_total", ), - total_tokens_rate=aggregator_update.get_metric( - key="total_tokens", type_="rate" + total_tokens_rate=estimated_state.get_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="completed_total_tokens", ), - time_to_first_token=( - aggregator_update.get_metric(key="time_to_first_token", type_="avg") + time_to_first_token=estimated_state.get_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="completed_time_to_first_token", ), - inter_token_latency=( - aggregator_update.get_metric(key="inter_token_latency", type_="avg") + inter_token_latency=estimated_state.get_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="completed_inter_token_latency", ), ) - if aggregator_update.get("updated_scheduler_stats"): + if estimated_state.get("updated_scheduler_stats"): self._update_system_stats( - request_targeted_start_delay=( - aggregator_update.get_metric( - key="request_targeted_start_delay", type_="avg", default=0.0 - ) + request_targeted_start_delay=estimated_state.get_metric( + group=EstimatedBenchmarkState.scheduler_state_group, + key="request_targeted_start_delay", ), - queued_time=( - aggregator_update.get_metric( - key="queued_time", type_="avg", default=0.0 - ) + queued_time=estimated_state.get_metric( + group=EstimatedBenchmarkState.scheduler_state_group, + key="queued_time", ), scheduler_overheads_time=0.0, # Need to add up metrics here ) diff --git a/src/guidellm/benchmark/scenario.py b/src/guidellm/benchmark/scenario.py index 73a9a050..59cdef27 100644 --- a/src/guidellm/benchmark/scenario.py +++ b/src/guidellm/benchmark/scenario.py @@ -9,11 +9,11 @@ import yaml from loguru import logger -from pydantic import BeforeValidator, Field, PositiveFloat, PositiveInt, SkipValidation +from pydantic import BeforeValidator, Field, PositiveFloat, PositiveInt from guidellm.backends import Backend, BackendType from guidellm.benchmark.profile import Profile, ProfileType -from guidellm.benchmark.types import AggregatorInputT, DataInputT, ProcessorInputT +from guidellm.benchmark.types import ProcessorInputT from guidellm.scheduler import StrategyType from guidellm.utils import StandardBaseModel @@ -108,11 +108,7 @@ class Config: # types like PreTrainedTokenizerBase arbitrary_types_allowed = True - data: Annotated[ - DataInputT, - # BUG: See https://github.com/pydantic/pydantic/issues/9541 - SkipValidation, - ] + data: Any profile: StrategyType | ProfileType | Profile rate: Annotated[list[PositiveFloat] | None, BeforeValidator(parse_float_list)] = ( None @@ -128,7 +124,6 @@ class Config: data_args: dict[str, Any] | None = None data_sampler: Literal["random"] | None = None # Aggregators configuration - add_aggregators: AggregatorInputT | None = None warmup: Annotated[float | None, Field(gt=0, le=1)] = None cooldown: Annotated[float | None, Field(gt=0, le=1)] = None request_samples: PositiveInt | None = 20 diff --git a/src/guidellm/benchmark/schemas.py b/src/guidellm/benchmark/schemas.py index 1b11aae6..41cb832f 100644 --- a/src/guidellm/benchmark/schemas.py +++ b/src/guidellm/benchmark/schemas.py @@ -36,7 +36,6 @@ from pydantic import Field, computed_field from guidellm.benchmark.profile import Profile -from guidellm.benchmark.schemas import BenchmarkerDict, SchedulerDict from guidellm.scheduler import ( BackendInterface, Environment, @@ -309,8 +308,8 @@ def get_request_metrics_sample( float, ]: ... - @abstractmethod @classmethod + @abstractmethod def update_estimate( cls, args: BenchmarkArgs, @@ -321,8 +320,8 @@ def update_estimate( scheduler_state: SchedulerState, ): ... - @abstractmethod @classmethod + @abstractmethod def compile( cls, args: BenchmarkArgs, @@ -537,7 +536,7 @@ def compile( ) -> GenerativeMetricsSummary: total_values = [ input_val + output_val - for input_val, output_val in zip(input_values, output_values) + for input_val, output_val in zip(input_values, output_values, strict=False) ] return GenerativeMetricsSummary( @@ -828,7 +827,7 @@ def update_estimate( # Always track concurrency state.add_time_averaged_metric( group=EstimatedBenchmarkState.benchmark_metrics_group, - key="concurrency", + key="concurrency_requests", value=scheduler_state.processing_requests, ) @@ -847,7 +846,7 @@ def update_estimate( for prefix in (request_info.status, "total"): requests_count = ( scheduler_state.successful_requests - if prefix == "successful" + if prefix == "completed" else scheduler_state.errored_requests if prefix == "errored" else scheduler_state.cancelled_requests @@ -914,11 +913,16 @@ def update_estimate( value=(response.output_metrics.total_tokens if response else None) or request.output_metrics.total_tokens, ) - - # General stats output_tokens = ( response.output_metrics.total_tokens if response else None ) or request.output_metrics.total_tokens + state.add_avg_rate_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="total_tokens", + value=output_tokens, + ) + + # General stats state.add_avg_metric( group=EstimatedBenchmarkState.benchmark_metrics_group, key=f"{prefix}_time_to_first_token", @@ -1162,6 +1166,13 @@ def update_estimate( request_info: RequestInfo, scheduler_state: SchedulerState, ): + if ( + request_info.status == "cancelled" + and request_info.timings.resolve_start is None + ): + # Cancelled requests that never started should be ignored + return + # Update child metric groups BenchmarkSchedulerStats.update_estimate(state, request_info) GenerativeMetrics.update_estimate( @@ -1176,30 +1187,38 @@ def update_estimate( state["samples_errored"] = [] state["requests_incomplete"] = [] state["samples_incomplete"] = [] - - if request_info.status not in {"completed", "errored", "cancelled"}: - # Must be fully resolved to be added - return - - if ( - request_info.status == "cancelled" - and request_info.timings.resolve_start is None - ): - # Cancelled requests that never started should not be added - return - - state.set_metric(group=cls.group_name, key="updated", value=True) - if state.set_metric( - group=cls.group_name, + in_warmup = state.set_metric( + group=EstimatedBenchmarkState.benchmark_state_group, key="in_warmup", value=args.is_in_warmup(request_info, scheduler_state), - ) or state.set_metric( - group=cls.group_name, + ) + in_cooldown = state.set_metric( + group=EstimatedBenchmarkState.benchmark_state_group, key="in_cooldown", value=args.is_in_cooldown(request_info, scheduler_state), + ) + state[f"{EstimatedBenchmarkState.benchmark_state_group}_status"] = ( + "in_cooldown" + if in_cooldown + else "in_warmup" + if in_warmup + else "in_progress" + ) + + if ( + request_info.status not in {"completed", "errored", "cancelled"} + or in_warmup + or in_cooldown ): + # Must be fully resolved to be added return + state.set_metric( + group=EstimatedBenchmarkState.benchmark_state_group, + key="updated", + value=True, + ) + if response is None: response = GenerationResponse( request_id=request.request_id, request_args=str(request.arguments) diff --git a/src/guidellm/benchmark/types.py b/src/guidellm/benchmark/types.py index 2e861678..94df4b8e 100644 --- a/src/guidellm/benchmark/types.py +++ b/src/guidellm/benchmark/types.py @@ -1,10 +1,8 @@ from __future__ import annotations -from collections.abc import Iterable from pathlib import Path from typing import Any -from datasets import Dataset, DatasetDict, IterableDataset, IterableDatasetDict from transformers import PreTrainedTokenizerBase # type: ignore[import] from typing_extensions import TypeAliasType diff --git a/src/guidellm/data/deserializers/file.py b/src/guidellm/data/deserializers/file.py index 54b18edb..d57403db 100644 --- a/src/guidellm/data/deserializers/file.py +++ b/src/guidellm/data/deserializers/file.py @@ -1,7 +1,8 @@ from __future__ import annotations +from collections.abc import Callable from pathlib import Path -from typing import Any, Callable +from typing import Any import pandas as pd from datasets import Dataset, load_dataset diff --git a/src/guidellm/data/deserializers/huggingface.py b/src/guidellm/data/deserializers/huggingface.py index 69f7d506..e356043a 100644 --- a/src/guidellm/data/deserializers/huggingface.py +++ b/src/guidellm/data/deserializers/huggingface.py @@ -1,7 +1,8 @@ from __future__ import annotations +from collections.abc import Callable from pathlib import Path -from typing import Any, Callable +from typing import Any from datasets import ( Dataset, diff --git a/src/guidellm/data/deserializers/memory.py b/src/guidellm/data/deserializers/memory.py index ddca64a9..6f8888ec 100644 --- a/src/guidellm/data/deserializers/memory.py +++ b/src/guidellm/data/deserializers/memory.py @@ -3,8 +3,9 @@ import contextlib import csv import json +from collections.abc import Callable from io import StringIO -from typing import Any, Callable, cast +from typing import Any, cast from datasets import Dataset from transformers import PreTrainedTokenizerBase diff --git a/src/guidellm/data/deserializers/synthetic.py b/src/guidellm/data/deserializers/synthetic.py index c2078f1a..92e8fc14 100644 --- a/src/guidellm/data/deserializers/synthetic.py +++ b/src/guidellm/data/deserializers/synthetic.py @@ -1,10 +1,10 @@ from __future__ import annotations import math -from collections.abc import Iterator +from collections.abc import Callable, Iterator from pathlib import Path from random import Random -from typing import Any, Callable +from typing import Any import yaml from datasets import Features, IterableDataset, Value @@ -209,7 +209,7 @@ def _create_prefix_iter(self, faker: Faker, rand: Random) -> Iterator[str]: # Create prefix list maintaining the correct distribution prefixes = [] - for bucket, weight in zip(self.config.prefix_buckets, unnorm_weights): + for bucket, weight in zip(self.config.prefix_buckets, unnorm_weights, strict=False): bucket_prefixes = [ self._create_prompt(bucket.prefix_tokens, faker) for _ in range(bucket.prefix_count) diff --git a/src/guidellm/data/loaders.py b/src/guidellm/data/loaders.py index f397ad51..0d83d726 100644 --- a/src/guidellm/data/loaders.py +++ b/src/guidellm/data/loaders.py @@ -39,7 +39,7 @@ def __init__( ) self.datasets = [] - for datum, data_kwargs in zip(data, data_args): + for datum, data_kwargs in zip(data, data_args, strict=False): self.datasets.append( DatasetDeserializerFactory.deserialize( data=datum, diff --git a/src/guidellm/utils/registry.py b/src/guidellm/utils/registry.py index f341dfdd..1a1a213f 100644 --- a/src/guidellm/utils/registry.py +++ b/src/guidellm/utils/registry.py @@ -11,7 +11,7 @@ from __future__ import annotations from collections.abc import Callable -from typing import Any, ClassVar, Generic, TypeVar, cast +from typing import ClassVar, Generic, TypeVar, cast from guidellm.utils.auto_importer import AutoImporterMixin diff --git a/src/guidellm/utils/statistics.py b/src/guidellm/utils/statistics.py index 350d8311..0529cb0c 100644 --- a/src/guidellm/utils/statistics.py +++ b/src/guidellm/utils/statistics.py @@ -288,13 +288,14 @@ def from_request_times( if distribution_type == "concurrency": # For concurrency, each request adds to concurrency at start # and subtracts at end - for (start, end), weight in zip(requests, weights): + for (start, end), weight in zip(requests, weights, strict=False): events.append((start, weight)) events.append((end, -1 * weight)) elif distribution_type == "rate": # For rate, each request is added at the end time only - events.append((min(0, *(start for start, _ in requests)), 0.0)) - for (_, end), weight in zip(requests, weights): + global_start = min(start for start, _ in requests) if requests else 0.0 + events.append((global_start, 0.0)) + for (_, end), weight in zip(requests, weights, strict=False): events.append((end, weight)) else: raise ValueError( @@ -633,36 +634,36 @@ def from_request_times( ) _, successful_requests, successful_weights = ( - zip(*successful) + zip(*successful, strict=False) if ( successful := list( filter( lambda val: val[0] == "successful", - zip(request_types, requests, weights), + zip(request_types, requests, weights, strict=False), ) ) ) else ([], [], []) ) _, incomplete_requests, incomplete_weights = ( - zip(*incomplete) + zip(*incomplete, strict=False) if ( incomplete := list( filter( lambda val: val[0] == "incomplete", - zip(request_types, requests, weights), + zip(request_types, requests, weights, strict=False), ) ) ) else ([], [], []) ) _, errored_requests, errored_weights = ( - zip(*errored) + zip(*errored, strict=False) if ( errored := list( filter( lambda val: val[0] == "error", - zip(request_types, requests, weights), + zip(request_types, requests, weights, strict=False), ) ) ) diff --git a/tests/unit/data/deserializers/test_synthetic.py b/tests/unit/data/deserializers/test_synthetic.py index 58b76aee..de95227a 100644 --- a/tests/unit/data/deserializers/test_synthetic.py +++ b/tests/unit/data/deserializers/test_synthetic.py @@ -362,7 +362,7 @@ def test_random_seeding_consistency(self, simple_config, mock_tokenizer): items1 = [] items2 = [] - for i, (item1, item2) in enumerate(zip(generator1, generator2)): + for i, (item1, item2) in enumerate(zip(generator1, generator2, strict=False)): items1.append(item1) items2.append(item2) if i >= 2: # Only get 3 items @@ -370,7 +370,7 @@ def test_random_seeding_consistency(self, simple_config, mock_tokenizer): # With same seed and deterministic mocks, results should be identical assert len(items1) == len(items2) - for item1, item2 in zip(items1, items2): + for item1, item2 in zip(items1, items2, strict=False): assert item1["prompt_tokens_count"] == item2["prompt_tokens_count"] assert item1["output_tokens_count"] == item2["output_tokens_count"] From 2b0fef8c193e4f1dcee5ede9ea050195ed6bfe12 Mon Sep 17 00:00:00 2001 From: Mark Kurtz Date: Mon, 13 Oct 2025 14:15:58 -0400 Subject: [PATCH 25/57] Update src/guidellm/data/deserializers/synthetic.py Co-authored-by: Samuel Monson Signed-off-by: Mark Kurtz --- src/guidellm/data/deserializers/synthetic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/guidellm/data/deserializers/synthetic.py b/src/guidellm/data/deserializers/synthetic.py index 92e8fc14..b518bf73 100644 --- a/src/guidellm/data/deserializers/synthetic.py +++ b/src/guidellm/data/deserializers/synthetic.py @@ -100,7 +100,7 @@ class SyntheticTextDatasetConfig(StandardBaseModel): @model_validator(mode="after") def check_prefix_options(self) -> SyntheticTextDatasetConfig: prefix_count = self.__pydantic_extra__.get("prefix_count", None) # type: ignore[attr-defined] - prefix_tokens = self.__pydantic_extra__.get("prefix_count", None) # type: ignore[attr-defined] + prefix_tokens = self.__pydantic_extra__.get("prefix_tokens", None) # type: ignore[attr-defined] if prefix_count is not None or prefix_tokens is not None: if self.prefix_buckets: raise ValueError( From 24f2ca38f4851e52df50672a7119bfb78dad7181 Mon Sep 17 00:00:00 2001 From: Mark Kurtz Date: Mon, 13 Oct 2025 14:53:36 -0400 Subject: [PATCH 26/57] Update src/guidellm/scheduler/worker_group.py Co-authored-by: Samuel Monson Signed-off-by: Mark Kurtz --- src/guidellm/scheduler/worker_group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/guidellm/scheduler/worker_group.py b/src/guidellm/scheduler/worker_group.py index 1e9db124..41c41f21 100644 --- a/src/guidellm/scheduler/worker_group.py +++ b/src/guidellm/scheduler/worker_group.py @@ -161,7 +161,7 @@ async def create_processes(self): self.backend.requests_limit or math.inf, ) ) != math.inf: - max_conc = requests_limit # type: ignore[assignment] + max_conc = int(requests_limit) else: # If concurrency not specified, use settings max_conc = settings.max_concurrency From ef36af11195317fd8684389e18a0ba14e5204c7d Mon Sep 17 00:00:00 2001 From: Mark Kurtz Date: Mon, 13 Oct 2025 14:59:17 -0400 Subject: [PATCH 27/57] Fixes from review --- pyproject.toml | 6 +- src/guidellm/backends/response_handlers.py | 136 +++++++++---------- src/guidellm/benchmark/entrypoints.py | 19 +-- src/guidellm/benchmark/types.py | 8 +- src/guidellm/data/deserializers/synthetic.py | 4 +- src/guidellm/data/preprocessors/mappers.py | 9 +- src/guidellm/scheduler/worker.py | 12 +- src/guidellm/settings.py | 4 +- src/guidellm/utils/__init__.py | 11 +- src/guidellm/utils/encoding.py | 38 ++---- src/guidellm/utils/imports.py | 9 ++ 11 files changed, 109 insertions(+), 147 deletions(-) create mode 100644 src/guidellm/utils/imports.py diff --git a/pyproject.toml b/pyproject.toml index 51a2a695..8fe6d950 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,10 +78,8 @@ dependencies = [ [project.optional-dependencies] perf = ["orjson", "msgpack", "msgspec", "uvloop"] -recommended = [ - "tiktoken>=0.11.0", # For OpenAI tokenizer - "blobfile>=3.1.0", # For OpenAI tokenizer -] +openai = ["tiktoken>=0.11.0", "blobfile>=3.1.0"] +recommended = ["guidellm[perf,openai]"] dev = [ # build "build>=1.0.0", diff --git a/src/guidellm/backends/response_handlers.py b/src/guidellm/backends/response_handlers.py index 37952428..44c949e6 100644 --- a/src/guidellm/backends/response_handlers.py +++ b/src/guidellm/backends/response_handlers.py @@ -10,16 +10,10 @@ from __future__ import annotations -import json -from typing import Any, Protocol, cast +from typing import Any, Protocol from guidellm.schemas import GenerationRequest, GenerationResponse, UsageMetrics -from guidellm.utils import RegistryMixin - -try: - import orjson -except ImportError: - orjson = None # type: ignore[assignment] +from guidellm.utils import RegistryMixin, json __all__ = [ "AudioResponseHandler", @@ -115,8 +109,7 @@ def compile_non_streaming( :param response: Complete API response containing choices and usage data :return: Standardized GenerationResponse with extracted text and metrics """ - choices = cast("list[dict]", response.get("choices", [])) - usage = cast("dict[str, int | dict[str, int]]", response.get("usage", {})) + choices, usage = self.extract_choices_and_usage(response) input_metrics, output_metrics = self.extract_metrics(usage) return GenerationResponse( @@ -139,26 +132,17 @@ def add_streaming_line(self, line: str) -> int | None: :param line: Raw SSE line from the streaming response :return: 1 if text content was extracted, 0 if line ignored, None if done """ - if line == "data: [DONE]": - return None + if not (data := self.extract_line_data(line)): + return None if data is None else 0 - if not line or not (line := line.strip()) or not line.startswith("data:"): - return 0 - - line = line[len("data:") :].strip() - data = cast( - "dict[str, Any]", - json.loads(line) if orjson is None else orjson.loads(line), - ) updated = False + choices, usage = self.extract_choices_and_usage(data) - if (choices := cast("list[dict]", data.get("choices"))) and ( - text := choices[0].get("text") - ): + if text := choices[0].get("text"): self.streaming_texts.append(text) updated = True - if usage := cast("dict[str, int | dict[str, int]]", data.get("usage")): + if usage: self.streaming_usage = usage return 1 if updated else 0 @@ -182,6 +166,34 @@ def compile_streaming(self, request: GenerationRequest) -> GenerationResponse: output_metrics=output_metrics, ) + def extract_line_data(self, line: str) -> dict[str, Any] | None: + """ + Extract JSON data from a streaming response line. + + :param line: Raw line from the streaming response + :return: Parsed JSON data as a dictionary, or None if line is invalid + """ + if line == "data: [DONE]": + return None + + if not line or not (line := line.strip()) or not line.startswith("data:"): + return {} + + line = line[len("data:") :].strip() + + return json.loads(line) + + def extract_choices_and_usage( + self, response: dict + ) -> tuple[list[dict], dict[str, int | dict[str, int]]]: + """ + Extract choices and usage data from the API response. + + :param response: Complete API response containing choices and usage data + :return: Tuple of (choices list, usage dictionary) + """ + return response.get("choices", []), response.get("usage", {}) + def extract_metrics( self, usage: dict[str, int | dict[str, int]] | None ) -> tuple[UsageMetrics, UsageMetrics]: @@ -194,15 +206,14 @@ def extract_metrics( if not usage: return UsageMetrics(), UsageMetrics() - input_details = cast("dict[str, int]", usage.get("prompt_tokens_details", {})) - output_details = cast( - "dict[str, int]", usage.get("completion_tokens_details", {}) + input_details: dict[str, int] = usage.get("prompt_tokens_details", {}) or {} + output_details: dict[str, int] = ( + usage.get("completion_tokens_details", {}) or {} ) return UsageMetrics( text_tokens=( - input_details.get("prompt_tokens") - or cast("int", usage.get("prompt_tokens")) + input_details.get("prompt_tokens") or usage.get("prompt_tokens") ), image_tokens=input_details.get("image_tokens"), video_tokens=input_details.get("video_tokens"), @@ -211,7 +222,7 @@ def extract_metrics( ), UsageMetrics( text_tokens=( output_details.get("completion_tokens") - or cast("int", usage.get("completion_tokens")) + or usage.get("completion_tokens") ), image_tokens=output_details.get("image_tokens"), video_tokens=output_details.get("video_tokens"), @@ -243,8 +254,7 @@ def compile_non_streaming( :param response: Complete API response containing choices and usage data :return: Standardized GenerationResponse with extracted content and metrics """ - choices = cast("list[dict]", response.get("choices", [])) - usage = cast("dict[str, int | dict[str, int]]", response.get("usage", {})) + choices, usage = self.extract_choices_and_usage(response) input_metrics, output_metrics = self.extract_metrics(usage) return GenerationResponse( @@ -252,9 +262,7 @@ def compile_non_streaming( request_args=str( request.arguments.model_dump() if request.arguments else None ), - text=cast("dict", choices[0].get("message", {})).get("content", "") - if choices - else "", + text=(choices[0].get("message", {}).get("content", "") if choices else ""), input_metrics=input_metrics, output_metrics=output_metrics, ) @@ -269,27 +277,17 @@ def add_streaming_line(self, line: str) -> int | None: :param line: Raw SSE line from the streaming response :return: 1 if content was extracted, 0 if line ignored, None if done """ - if line == "data: [DONE]": - return None + if not (data := self.extract_line_data(line)): + return None if data is None else 0 - if not line or not (line := line.strip()) or not line.startswith("data:"): - return 0 - - line = line[len("data:") :].strip() - data = cast( - "dict[str, Any]", - json.loads(line) if orjson is None else orjson.loads(line), - ) updated = False + choices, usage = self.extract_choices_and_usage(data) - # Extract delta content for chat completion chunks - if choices := cast("list[dict]", data.get("choices")): - delta = choices[0].get("delta", {}) - if content := delta.get("content"): - self.streaming_texts.append(content) + if choices and (content := choices[0].get("delta", {}).get("content")): + self.streaming_texts.append(content) updated = True - if usage := cast("dict[str, int | dict[str, int]]", data.get("usage")): + if usage: self.streaming_usage = usage return 1 if updated else 0 @@ -355,10 +353,10 @@ def compile_non_streaming( :param response: Complete API response containing text and usage data :return: Standardized GenerationResponse with extracted text and metrics """ - usage = cast("dict[str, int]", response.get("usage", {})) - input_details = cast("dict[str, int]", usage.get("input_token_details", {})) - output_details = cast("dict[str, int]", usage.get("output_token_details", {})) - text = response.get("text", "") + usage: dict[str, int | dict[str, int]] = response.get("usage", {}) + input_details: dict[str, int] = usage.get("input_token_details", {}) or {} + output_details: dict[str, int] = usage.get("output_token_details", {}) or {} + text: str = response.get("text", "") return GenerationResponse( request_id=request.request_id, @@ -396,17 +394,16 @@ def add_streaming_line(self, line: str) -> int | None: if not line or not (line := line.strip()) or not line.startswith("{"): return 0 - data = cast( - "dict[str, Any]", - json.loads(line) if orjson is None else orjson.loads(line), - ) + data: dict[str, Any] = json.loads(line) + text: str + usage: dict[str, int | dict[str, int]] updated = False if text := data.get("text"): self.streaming_texts.append(text) updated = True - if usage := cast("dict[str, int | dict[str, int]]", data.get("usage")): + if usage := data.get("usage"): self.streaming_usage = usage return 1 if updated else 0 @@ -445,22 +442,15 @@ def extract_metrics( if not usage: return UsageMetrics(), UsageMetrics() - input_details = cast("dict[str, int]", usage.get("input_token_details", {})) - output_details = cast("dict[str, int]", usage.get("output_token_details", {})) + input_details: dict[str, int] = usage.get("input_token_details", {}) or {} + output_details: dict[str, int] = usage.get("output_token_details", {}) or {} return UsageMetrics( - text_tokens=( - input_details.get("text_tokens") - or cast("int", usage.get("input_tokens")) - ), + text_tokens=(input_details.get("text_tokens") or usage.get("input_tokens")), audio_tokens=( - input_details.get("audio_tokens") - or cast("int", usage.get("audio_tokens")) - ), - audio_seconds=( - input_details.get("seconds") or cast("int", usage.get("seconds")) + input_details.get("audio_tokens") or usage.get("audio_tokens") ), + audio_seconds=(input_details.get("seconds") or usage.get("seconds")), ), UsageMetrics( - text_tokens=output_details.get("text_tokens") - or cast("int", usage.get("output_tokens")), + text_tokens=output_details.get("text_tokens") or usage.get("output_tokens"), ) diff --git a/src/guidellm/benchmark/entrypoints.py b/src/guidellm/benchmark/entrypoints.py index 422355aa..f711dbfb 100644 --- a/src/guidellm/benchmark/entrypoints.py +++ b/src/guidellm/benchmark/entrypoints.py @@ -5,15 +5,14 @@ from typing import Any, Literal from torch.utils.data import Sampler -from transformers import PreTrainedTokenizerBase -from typing_extensions import TypeAliasType from guidellm.backends import Backend, BackendType from guidellm.benchmark.benchmarker import Benchmarker from guidellm.benchmark.output import GenerativeBenchmarkerOutput from guidellm.benchmark.profile import Profile, ProfileType -from guidellm.benchmark.progress import BenchmarkerProgress, BenchmarkerProgressGroup +from guidellm.benchmark.progress import BenchmarkerProgressGroup from guidellm.benchmark.schemas import GenerativeBenchmark, GenerativeBenchmarksReport +from guidellm.benchmark.types import OutputFormatT, ProcessorInputT, ProgressInputT from guidellm.data import ( DataLoader, DatasetPreprocessor, @@ -40,20 +39,6 @@ _CURRENT_WORKING_DIR = Path.cwd() -OutputFormatT = TypeAliasType( - "OutputFormatT", - tuple[str, ...] - | list[str] - | dict[str, str | dict[str, Any] | GenerativeBenchmarkerOutput] - | None, -) - -ProcessorInputT = TypeAliasType("ProcessorInputT", str | Path | PreTrainedTokenizerBase) - -ProgressInputT = TypeAliasType( - "ProgressInputT", tuple[str, ...] | list[str] | list[BenchmarkerProgress] -) - # Helper Functions diff --git a/src/guidellm/benchmark/types.py b/src/guidellm/benchmark/types.py index 94df4b8e..8fa2dbfb 100644 --- a/src/guidellm/benchmark/types.py +++ b/src/guidellm/benchmark/types.py @@ -9,13 +9,7 @@ from guidellm.benchmark.output import GenerativeBenchmarkerOutput from guidellm.benchmark.progress import BenchmarkerProgress -__all__ = [ - "AggregatorInputT", - "DataInputT", - "OutputFormatT", - "ProcessorInputT", - "ProgressInputT", -] +__all__ = ["OutputFormatT", "ProcessorInputT", "ProgressInputT"] OutputFormatT = TypeAliasType( diff --git a/src/guidellm/data/deserializers/synthetic.py b/src/guidellm/data/deserializers/synthetic.py index b518bf73..d9e415c6 100644 --- a/src/guidellm/data/deserializers/synthetic.py +++ b/src/guidellm/data/deserializers/synthetic.py @@ -209,7 +209,9 @@ def _create_prefix_iter(self, faker: Faker, rand: Random) -> Iterator[str]: # Create prefix list maintaining the correct distribution prefixes = [] - for bucket, weight in zip(self.config.prefix_buckets, unnorm_weights, strict=False): + for bucket, weight in zip( + self.config.prefix_buckets, unnorm_weights, strict=False + ): bucket_prefixes = [ self._create_prompt(bucket.prefix_tokens, faker) for _ in range(bucket.prefix_count) diff --git a/src/guidellm/data/preprocessors/mappers.py b/src/guidellm/data/preprocessors/mappers.py index cbfa9c20..0783103b 100644 --- a/src/guidellm/data/preprocessors/mappers.py +++ b/src/guidellm/data/preprocessors/mappers.py @@ -120,9 +120,16 @@ def datasets_mappings( for index, dataset in enumerate(datasets) } + # Parse out user mappings that were passed in and validate them + # Must be in the format of: + # {: []} + # where can be a single string or list of strings + # and each string can be any of: + # - a column name (assumes the first dataset was intended) + # - . where is the dataset index + # - . where is the dataset name for column_type, names in input_mappings.items(): mappings[column_type] = [] - for name in names if isinstance(names, list) else [names]: if "." in name: dataset, column_name = name.split(".", 1) diff --git a/src/guidellm/scheduler/worker.py b/src/guidellm/scheduler/worker.py index 4b426058..45716b78 100644 --- a/src/guidellm/scheduler/worker.py +++ b/src/guidellm/scheduler/worker.py @@ -232,18 +232,18 @@ async def _processing_startup(self): self.backend_started = True await self.backend.validate() - # Wait for all processes to be ready - await wait_for_sync_barrier( - self.startup_barrier, - poll_interval=self.messaging.poll_interval, - ) - # Get messaging system ready await self.messaging.start( receive_stop_criteria=[self.requests_generated_event] ) self.messaging_started = True + # Wait for all processes to be ready + await wait_for_sync_barrier( + self.startup_barrier, + poll_interval=self.messaging.poll_interval, + ) + self.startup_completed = True async def _processing_shutdown(self): diff --git a/src/guidellm/settings.py b/src/guidellm/settings.py index 222d85f9..20d9ff96 100644 --- a/src/guidellm/settings.py +++ b/src/guidellm/settings.py @@ -46,7 +46,7 @@ class LoggingSettings(BaseModel): disabled: bool = False clear_loggers: bool = True - console_log_level: str = "DEBUG" + console_log_level: str = "WARNING" log_file: str | None = None log_file_level: str | None = None @@ -145,7 +145,7 @@ class Settings(BaseSettings): mp_max_pending_buffer_percent: float = 0.5 mp_max_worker_buffer_percent: float = 0.2 max_concurrency: int = 512 - max_worker_processes: int = 2 + max_worker_processes: int = 10 scheduler_start_delay_non_distributed: float = 1.0 constraint_error_window_size: float = 30 constraint_error_min_processed: float = 30 diff --git a/src/guidellm/utils/__init__.py b/src/guidellm/utils/__init__.py index 702b2a9d..89312771 100644 --- a/src/guidellm/utils/__init__.py +++ b/src/guidellm/utils/__init__.py @@ -17,13 +17,9 @@ safe_getattr, safe_multiply, ) -from .hf_datasets import ( - SUPPORTED_TYPES, - save_dataset_to_file, -) -from .hf_transformers import ( - check_load_processor, -) +from .hf_datasets import SUPPORTED_TYPES, save_dataset_to_file +from .hf_transformers import check_load_processor +from .imports import json from .messaging import ( InterProcessMessaging, InterProcessMessagingManagerQueue, @@ -113,6 +109,7 @@ "format_value_display", "get_literal_vals", "is_punctuation", + "json", "load_text", "recursive_key_update", "safe_add", diff --git a/src/guidellm/utils/encoding.py b/src/guidellm/utils/encoding.py index 2931e98a..7ececef5 100644 --- a/src/guidellm/utils/encoding.py +++ b/src/guidellm/utils/encoding.py @@ -10,7 +10,6 @@ from __future__ import annotations -import json from collections.abc import Mapping from typing import Any, ClassVar, Generic, Literal, TypeVar, cast @@ -24,11 +23,11 @@ HAS_MSGPACK = False try: - from msgspec.msgpack import ( # type: ignore[import-not-found] # Optional dependency - Decoder as MsgspecDecoder, + from msgspec.msgpack import ( + Decoder as MsgspecDecoder, # type: ignore[import-not-found] # Optional dependency ) - from msgspec.msgpack import ( # type: ignore[import-not-found] # Optional dependency - Encoder as MsgspecEncoder, + from msgspec.msgpack import ( + Encoder as MsgspecEncoder, # type: ignore[import-not-found] # Optional dependency ) HAS_MSGSPEC = True @@ -36,16 +35,11 @@ MsgspecDecoder = MsgspecEncoder = None HAS_MSGSPEC = False -try: - import orjson # type: ignore[import-not-found] # Optional dependency - - HAS_ORJSON = True -except ImportError: - orjson = None - HAS_ORJSON = False from pydantic import BaseModel +from guidellm.utils.imports import json + __all__ = [ "Encoder", "EncodingTypesAlias", @@ -510,7 +504,7 @@ def to_sequence(self, obj: Any) -> str | Any: ): payload_type = "collection_mapping" keys = ",".join(str(key) for key in obj) - payload = keys.encode() + b"|" if HAS_ORJSON else keys + "|" + payload = keys.encode() + b"|" for item in obj.values(): is_pydantic = isinstance(item, BaseModel) payload = self.pack_next_sequence( @@ -601,15 +595,7 @@ def to_sequence_pydantic(self, obj: BaseModel) -> str | bytes: class_module: str = obj.__class__.__module__ json_data = obj.__pydantic_serializer__.to_json(obj) - return ( - (class_name.encode() + b"|" + class_module.encode() + b"|" + json_data) - if HAS_ORJSON - else ( - class_name + "|" + class_module + "|" + json_data.decode() - if isinstance(json_data, bytes) - else json_data - ) - ) + return class_name.encode() + b"|" + class_module.encode() + b"|" + json_data def from_sequence_pydantic(self, data: str | bytes) -> BaseModel: """ @@ -643,7 +629,7 @@ def to_sequence_python(self, obj: Any) -> str | bytes: :param obj: Python object to serialize :return: JSON string or bytes representation """ - return orjson.dumps(obj) if HAS_ORJSON else json.dumps(obj) + return json.dumps(obj) def from_sequence_python(self, data: str | bytes) -> Any: """ @@ -651,13 +637,7 @@ def from_sequence_python(self, data: str | bytes) -> Any: :param data: JSON string or bytes to deserialize :return: Reconstructed Python object - :raises ImportError: If orjson is required but not available """ - if isinstance(data, bytes): - if not HAS_ORJSON: - raise ImportError("orjson is not available, cannot deserialize bytes") - return orjson.loads(data) - return json.loads(data) def pack_next_sequence( # noqa: C901, PLR0912 diff --git a/src/guidellm/utils/imports.py b/src/guidellm/utils/imports.py new file mode 100644 index 00000000..9a6b82d1 --- /dev/null +++ b/src/guidellm/utils/imports.py @@ -0,0 +1,9 @@ +from __future__ import annotations + +try: + import orjson as json +except ImportError: + import json + + +__all__ = ["json"] From fcc1114a9c79e61b1c0ebe0bbae546499fea1545 Mon Sep 17 00:00:00 2001 From: Benjamin Blue Date: Mon, 13 Oct 2025 17:12:36 -0400 Subject: [PATCH 28/57] Features/add tooltip to line chart (#392) ## Summary The Metrics summary section now includes the data point dots on the line and a tooltip including the strategy label, e.g.: synchronous, constant@32.32, concurrent@5 ## Test Plan - Unit test for backend change - No tests written to check on tooltip addition, seems overflow complex to try and hover the dots on the chart but I didn't look into it. ## Related Issues - Resolves #277 --- - [x] "I certify that all code in this PR is my own." --------- Signed-off-by: dalthecow --- src/guidellm/presentation/data_models.py | 24 ++++++++++++++-- .../MetricLine/MetricLine.component.tsx | 28 +++++++++++++++++-- .../MetricsSummary.component.tsx | 2 +- src/ui/lib/store/benchmarksWindowData.ts | 10 +++++++ .../benchmarks/benchmarks.interfaces.ts | 1 + .../slices/benchmarks/benchmarks.selectors.ts | 25 ++++++++++++++--- tests/unit/presentation/test_data_models.py | 10 ++++++- 7 files changed, 89 insertions(+), 11 deletions(-) diff --git a/src/guidellm/presentation/data_models.py b/src/guidellm/presentation/data_models.py index ff2863b4..2401b3ef 100644 --- a/src/guidellm/presentation/data_models.py +++ b/src/guidellm/presentation/data_models.py @@ -5,6 +5,8 @@ from pydantic import BaseModel, computed_field +from guidellm.scheduler.strategy import SchedulingStrategy + if TYPE_CHECKING: from guidellm.benchmark import GenerativeBenchmark @@ -212,12 +214,30 @@ class BenchmarkDatum(BaseModel): ttft: TabularDistributionSummary throughput: TabularDistributionSummary time_per_request: TabularDistributionSummary + strategy_display_str: str + + @classmethod + def get_strategy_display_str(cls, strategy: SchedulingStrategy): + strategy_type = strategy if isinstance(strategy, str) else strategy.type_ + strategy_instance = ( + strategy if isinstance(strategy, SchedulingStrategy) else None + ) + + if strategy_type == "concurrent": + rate = f"@{strategy.streams}" if strategy_instance else "@##" # type: ignore[attr-defined] + elif strategy_type in ("constant", "poisson"): + rate = f"@{strategy.rate:.2f}" if strategy_instance else "@#.##" # type: ignore[attr-defined] + else: + rate = "" + return f"{strategy_type}{rate}" @classmethod def from_benchmark(cls, bm: "GenerativeBenchmark"): + rps = bm.metrics.requests_per_second.successful.mean return cls( - requests_per_second=bm.metrics.requests_per_second.successful.mean, - tpot=TabularDistributionSummary.from_distribution_summary( + strategy_display_str=cls.get_strategy_display_str(bm.args.strategy), + requests_per_second=rps, + itl=TabularDistributionSummary.from_distribution_summary( bm.metrics.inter_token_latency_ms.successful ), ttft=TabularDistributionSummary.from_distribution_summary( diff --git a/src/ui/lib/components/Charts/MetricLine/MetricLine.component.tsx b/src/ui/lib/components/Charts/MetricLine/MetricLine.component.tsx index 8b1b4df2..eb123593 100644 --- a/src/ui/lib/components/Charts/MetricLine/MetricLine.component.tsx +++ b/src/ui/lib/components/Charts/MetricLine/MetricLine.component.tsx @@ -1,5 +1,6 @@ -import { useTheme } from '@mui/material'; -import { ResponsiveLine } from '@nivo/line'; +import { Typography, useTheme } from '@mui/material'; +import { PointTooltipProps, ResponsiveLine } from '@nivo/line'; +import { BasicTooltip } from '@nivo/tooltip'; import React, { FC } from 'react'; import { useColor } from '@/lib/hooks/useColor'; @@ -49,11 +50,30 @@ export const Component: FC = ({ reverse: false, }; } + type PointTooltipPropsWithLabel = PointTooltipProps & { + point: { + data: { + label: string; + }; + }; + }; return ( ( + + {(point as PointTooltipPropsWithLabel).point.data.label} + + } + color={point.point.color} + enableChip={true} + /> + )} + pointSize={10} colors={[selectedColor]} margin={{ top: 20, right: 10, bottom: 20, left: 35.5 }} xScale={{ type: 'linear', min: minX }} @@ -92,7 +112,6 @@ export const Component: FC = ({ }} enableGridX={false} enableGridY={false} - pointSize={0} useMesh={true} layers={[ CustomAxes, @@ -115,6 +134,9 @@ export const Component: FC = ({ ), 'axes', 'lines', + 'points', + 'markers', + 'mesh', ]} theme={lineTheme} /> diff --git a/src/ui/lib/components/MetricsSummary/MetricsSummary.component.tsx b/src/ui/lib/components/MetricsSummary/MetricsSummary.component.tsx index 0d804f5c..9530d9e7 100644 --- a/src/ui/lib/components/MetricsSummary/MetricsSummary.component.tsx +++ b/src/ui/lib/components/MetricsSummary/MetricsSummary.component.tsx @@ -102,7 +102,7 @@ export const Component = () => { return ( <> - + diff --git a/src/ui/lib/store/benchmarksWindowData.ts b/src/ui/lib/store/benchmarksWindowData.ts index a589e8ed..b4af5063 100644 --- a/src/ui/lib/store/benchmarksWindowData.ts +++ b/src/ui/lib/store/benchmarksWindowData.ts @@ -1,5 +1,6 @@ export const benchmarksScript = `window.benchmarks = [ { + strategyDisplayStr: "synchronous", requestsPerSecond: 11.411616848282272, tpot: { mean: 8.758024845683707, @@ -171,6 +172,7 @@ export const benchmarksScript = `window.benchmarks = [ }, }, { + strategyDisplayStr: "constant@36.28", requestsPerSecond: 36.289181300710815, tpot: { mean: 588.0161376137819, @@ -342,6 +344,7 @@ export const benchmarksScript = `window.benchmarks = [ }, }, { + strategyDisplayStr: "constant@20.75", requestsPerSecond: 20.752070927855794, tpot: { mean: 116.28360712595156, @@ -513,6 +516,7 @@ export const benchmarksScript = `window.benchmarks = [ }, }, { + strategyDisplayStr: "constant@26.81", requestsPerSecond: 26.81917480361788, tpot: { mean: 299.7306064613554, @@ -684,6 +688,7 @@ export const benchmarksScript = `window.benchmarks = [ }, }, { + strategyDisplayStr: "constant@26.82", requestsPerSecond: 26.823988819498975, tpot: { mean: 683.8011571339198, @@ -855,6 +860,7 @@ export const benchmarksScript = `window.benchmarks = [ }, }, { + strategyDisplayStr: "constant@24.50", requestsPerSecond: 24.50047903792646, tpot: { mean: 742.9258901891964, @@ -1026,6 +1032,7 @@ export const benchmarksScript = `window.benchmarks = [ }, }, { + strategyDisplayStr: "constant@25.61", requestsPerSecond: 25.617829792196602, tpot: { mean: 663.3098317044122, @@ -1197,6 +1204,7 @@ export const benchmarksScript = `window.benchmarks = [ }, }, { + strategyDisplayStr: "constant@37.02", requestsPerSecond: 37.02892550982192, tpot: { mean: 606.4144710877113, @@ -1368,6 +1376,7 @@ export const benchmarksScript = `window.benchmarks = [ }, }, { + strategyDisplayStr: "constant@37.29", requestsPerSecond: 37.29183354201869, tpot: { mean: 603.3237551205925, @@ -1539,6 +1548,7 @@ export const benchmarksScript = `window.benchmarks = [ }, }, { + strategyDisplayStr: "throughput", requestsPerSecond: 37.45318312972309, tpot: { mean: 600.7204526769262, diff --git a/src/ui/lib/store/slices/benchmarks/benchmarks.interfaces.ts b/src/ui/lib/store/slices/benchmarks/benchmarks.interfaces.ts index 602ae17e..6c01d5e2 100644 --- a/src/ui/lib/store/slices/benchmarks/benchmarks.interfaces.ts +++ b/src/ui/lib/store/slices/benchmarks/benchmarks.interfaces.ts @@ -27,6 +27,7 @@ export interface BenchmarkMetrics { export interface Benchmark extends BenchmarkMetrics { requestsPerSecond: number; + strategyDisplayStr: string; } export type Benchmarks = Benchmark[]; diff --git a/src/ui/lib/store/slices/benchmarks/benchmarks.selectors.ts b/src/ui/lib/store/slices/benchmarks/benchmarks.selectors.ts index 53d54f40..d3da9bf9 100644 --- a/src/ui/lib/store/slices/benchmarks/benchmarks.selectors.ts +++ b/src/ui/lib/store/slices/benchmarks/benchmarks.selectors.ts @@ -11,6 +11,18 @@ import { selectSloState } from '../slo/slo.selectors'; export const selectBenchmarks = (state: RootState) => state.benchmarks.data; +const getUnitsByMetric = (metric: string) => { + switch (metric) { + case 'ttft': + case 'tpot': + return 'ms'; + case 'timePerRequest': + return 'sec'; + case 'throughput': + return 'tok/s'; + } +}; + export const selectMetricsSummaryLineData = createSelector( [selectBenchmarks, selectSloState], (benchmarks, sloState) => { @@ -18,8 +30,10 @@ export const selectMetricsSummaryLineData = createSelector( ?.slice() ?.sort((bm1, bm2) => (bm1.requestsPerSecond > bm2.requestsPerSecond ? 1 : -1)); const selectedPercentile = sloState.enforcedPercentile; - - const lineData: { [K in keyof BenchmarkMetrics]: Point[] } = { + interface PointWithLabel extends Point { + label: string; + } + const lineData: { [K in keyof BenchmarkMetrics]: PointWithLabel[] } = { ttft: [], tpot: [], timePerRequest: [], @@ -32,14 +46,17 @@ export const selectMetricsSummaryLineData = createSelector( 'throughput', ]; metrics.forEach((metric) => { - const data: Point[] = []; + const data: PointWithLabel[] = []; sortedByRPS?.forEach((benchmark) => { const percentile = benchmark[metric].percentileRows.find( (p) => p.percentile === selectedPercentile ); + const yValue = percentile?.value ?? 0; + const units = getUnitsByMetric(metric); data.push({ x: benchmark.requestsPerSecond, - y: percentile?.value ?? 0, + y: yValue, + label: `${benchmark.strategyDisplayStr} ${formatNumber(yValue)} ${units}`, }); }); diff --git a/tests/unit/presentation/test_data_models.py b/tests/unit/presentation/test_data_models.py index c1663c43..e879406d 100644 --- a/tests/unit/presentation/test_data_models.py +++ b/tests/unit/presentation/test_data_models.py @@ -1,6 +1,7 @@ import pytest -from guidellm.presentation.data_models import Bucket +from guidellm.presentation.data_models import BenchmarkDatum, Bucket +from tests.unit.mock_benchmark import mock_generative_benchmark @pytest.mark.smoke @@ -18,3 +19,10 @@ def test_bucket_from_data(): assert buckets[1].value == 8.0 assert buckets[1].count == 5 assert bucket_width == 1 + + +@pytest.mark.smoke +def test_from_benchmark_includes_strategy_display_str(): + mock_bm = mock_generative_benchmark() + bm = BenchmarkDatum.from_benchmark(mock_bm) + assert bm.strategy_display_str == "synchronous" From b0becd566631c111673d3b3552b0d6fbe183121c Mon Sep 17 00:00:00 2001 From: Mark Kurtz Date: Tue, 14 Oct 2025 15:13:12 -0400 Subject: [PATCH 29/57] Reenablement of flows and fixes --- src/guidellm/__main__.py | 10 +- src/guidellm/benchmark/__init__.py | 7 +- src/guidellm/benchmark/benchmarker.py | 62 +-- src/guidellm/benchmark/entrypoints.py | 48 +-- src/guidellm/benchmark/progress.py | 260 +---------- src/guidellm/benchmark/schemas.py | 8 +- src/guidellm/benchmark/types.py | 7 +- src/guidellm/data/loaders.py | 43 +- src/guidellm/data/preprocessors/formatters.py | 211 ++++++--- src/guidellm/data/utils/__init__.py | 16 +- src/guidellm/data/utils/functions.py | 405 ++++++++---------- src/guidellm/scheduler/worker_group.py | 128 +++--- src/guidellm/schemas/request.py | 3 - 13 files changed, 480 insertions(+), 728 deletions(-) diff --git a/src/guidellm/__main__.py b/src/guidellm/__main__.py index 97f3e436..680ac852 100644 --- a/src/guidellm/__main__.py +++ b/src/guidellm/__main__.py @@ -286,7 +286,7 @@ def benchmark(): ) @click.option( "--data-num-workers", - default=1, + default=None, type=int, help="The number of worker processes to use for data loading.", ) @@ -505,11 +505,9 @@ def run( output_formats=output_formats, # Updates configuration progress=( - [ - GenerativeConsoleBenchmarkerProgress( - display_scheduler_stats=display_scheduler_stats - ) - ] + GenerativeConsoleBenchmarkerProgress( + display_scheduler_stats=display_scheduler_stats + ) if not disable_progress else None ), diff --git a/src/guidellm/benchmark/__init__.py b/src/guidellm/benchmark/__init__.py index d987ebb3..4c7cc4a5 100644 --- a/src/guidellm/benchmark/__init__.py +++ b/src/guidellm/benchmark/__init__.py @@ -15,11 +15,7 @@ SynchronousProfile, ThroughputProfile, ) -from .progress import ( - BenchmarkerProgress, - BenchmarkerProgressGroup, - GenerativeConsoleBenchmarkerProgress, -) +from .progress import BenchmarkerProgress, GenerativeConsoleBenchmarkerProgress from .schemas import ( Benchmark, BenchmarkArgs, @@ -44,7 +40,6 @@ "Benchmarker", "BenchmarkerDict", "BenchmarkerProgress", - "BenchmarkerProgressGroup", "ConcurrentProfile", "EstimatedBenchmarkState", "GenerativeAudioMetricsSummary", diff --git a/src/guidellm/benchmark/benchmarker.py b/src/guidellm/benchmark/benchmarker.py index 2fa1c36e..ed9d789b 100644 --- a/src/guidellm/benchmark/benchmarker.py +++ b/src/guidellm/benchmark/benchmarker.py @@ -23,20 +23,19 @@ from typing import Generic from guidellm.benchmark.profile import Profile +from guidellm.benchmark.progress import BenchmarkerProgress from guidellm.benchmark.schemas import ( BenchmarkArgs, BenchmarkT, EstimatedBenchmarkState, ) +from guidellm.logger import logger from guidellm.scheduler import ( BackendInterface, Environment, - NonDistributedEnvironment, RequestT, ResponseT, Scheduler, - SchedulerState, - SchedulingStrategy, ) from guidellm.utils import ThreadSafeSingletonMixin @@ -65,19 +64,13 @@ async def run( requests: Iterable[RequestT | Iterable[RequestT | tuple[RequestT, float]]], backend: BackendInterface[RequestT, ResponseT], profile: Profile, - environment: Environment | None = None, + environment: Environment, + progress: BenchmarkerProgress[BenchmarkT] | None = None, sample_requests: int | None = 20, warmup: float | None = None, cooldown: float | None = None, prefer_response_metrics: bool = True, - ) -> AsyncIterator[ - tuple[ - EstimatedBenchmarkState | None, - BenchmarkT | None, - SchedulingStrategy, - SchedulerState | None, - ] - ]: + ) -> AsyncIterator[BenchmarkT]: """ Execute benchmark runs across multiple scheduling strategies. @@ -95,15 +88,17 @@ async def run( :raises Exception: If benchmark execution or compilation fails. """ with self.thread_lock: - if environment is None: - environment = NonDistributedEnvironment() + if progress: + await progress.on_initialize(profile) run_id = str(uuid.uuid4()) strategies_generator = profile.strategies_generator() strategy, constraints = next(strategies_generator) while strategy is not None: - yield None, None, strategy, None + if progress: + await progress.on_benchmark_start(strategy) + args = BenchmarkArgs( run_id=run_id, run_index=len(profile.completed_strategies), @@ -127,18 +122,23 @@ async def run( env=environment, **constraints or {}, ): - benchmark_class.update_estimate( - args, - estimated_state, - response, - request, - request_info, - scheduler_state, - ) - yield estimated_state, None, strategy, scheduler_state - - if scheduler_state is None: - raise RuntimeError("Scheduler state is None after execution.") + try: + benchmark_class.update_estimate( + args, + estimated_state, + response, + request, + request_info, + scheduler_state, + ) + if progress: + await progress.on_benchmark_update( + estimated_state, scheduler_state + ) + except Exception as err: + logger.error( + f"Error updating benchmark estimate/progress: {err}" + ) benchmark = benchmark_class.compile( args=args, @@ -151,10 +151,16 @@ async def run( strategy=strategy, constraints=constraints, ) - yield None, benchmark, strategy, None + if progress: + await progress.on_benchmark_complete(benchmark) + + yield benchmark try: strategy, constraints = strategies_generator.send(benchmark) except StopIteration: strategy = None constraints = None + + if progress: + await progress.on_finalize() diff --git a/src/guidellm/benchmark/entrypoints.py b/src/guidellm/benchmark/entrypoints.py index f711dbfb..18768216 100644 --- a/src/guidellm/benchmark/entrypoints.py +++ b/src/guidellm/benchmark/entrypoints.py @@ -10,9 +10,9 @@ from guidellm.benchmark.benchmarker import Benchmarker from guidellm.benchmark.output import GenerativeBenchmarkerOutput from guidellm.benchmark.profile import Profile, ProfileType -from guidellm.benchmark.progress import BenchmarkerProgressGroup +from guidellm.benchmark.progress import BenchmarkerProgress from guidellm.benchmark.schemas import GenerativeBenchmark, GenerativeBenchmarksReport -from guidellm.benchmark.types import OutputFormatT, ProcessorInputT, ProgressInputT +from guidellm.benchmark.types import OutputFormatT, ProcessorInputT from guidellm.data import ( DataLoader, DatasetPreprocessor, @@ -271,7 +271,6 @@ async def resolve_output_formats( return resolved -# @validate_call(config={"arbitrary_types_allowed": True}) async def benchmark_generative_text( # noqa: C901, PLR0915, PLR0912 # Required target: str, @@ -296,7 +295,7 @@ async def benchmark_generative_text( # noqa: C901, PLR0915, PLR0912 ) = "chat_completions", data_collator: Callable | Literal["generative"] | None = "generative", data_sampler: Sampler[int] | Literal["shuffle"] | None = None, - data_num_workers: int | None = 1, + data_num_workers: int | None = None, dataloader_kwargs: dict[str, Any] | None = None, random_seed: int = 42, # Output configuration @@ -308,7 +307,7 @@ async def benchmark_generative_text( # noqa: C901, PLR0915, PLR0912 | None ) = ("console", "json", "html", "csv"), # Updates configuration - progress: ProgressInputT | None = None, + progress: BenchmarkerProgress | None = None, print_updates: bool = False, # Benchmarker configuration benchmark_cls: type[GenerativeBenchmark] = GenerativeBenchmark, @@ -366,37 +365,26 @@ async def benchmark_generative_text( # noqa: C901, PLR0915, PLR0912 output_formats=output_formats, output_path=output_path, console=console ) - progress_group = BenchmarkerProgressGroup( - instances=progress or [], enabled=bool(progress) - ) report = GenerativeBenchmarksReport() console.print_update( title="Setup complete, starting benchmarks...", status="success" ) console.print("\n\n") - async for ( - _aggregator_update, - benchmark, - _strategy, - _scheduler_state, - ) in progress_group( - profile, - Benchmarker[ - GenerativeBenchmark, - GenerationRequest, - GenerationResponse, - ]().run( - benchmark_class=benchmark_cls, - requests=request_loader, - backend=backend, - profile=profile, - environment=NonDistributedEnvironment(), - sample_requests=sample_requests, - warmup=warmup, - cooldown=cooldown, - prefer_response_metrics=True, - ), + benchmarker: Benchmarker[ + GenerativeBenchmark, GenerationRequest, GenerationResponse + ] = Benchmarker() + async for benchmark in benchmarker.run( + benchmark_class=benchmark_cls, + requests=request_loader, + backend=backend, + profile=profile, + environment=NonDistributedEnvironment(), + progress=progress, + sample_requests=sample_requests, + warmup=warmup, + cooldown=cooldown, + prefer_response_metrics=True, ): if benchmark: report.benchmarks.append(benchmark) diff --git a/src/guidellm/benchmark/progress.py b/src/guidellm/benchmark/progress.py index 5a88d696..558def67 100644 --- a/src/guidellm/benchmark/progress.py +++ b/src/guidellm/benchmark/progress.py @@ -16,9 +16,7 @@ from __future__ import annotations -import asyncio from abc import ABC, abstractmethod -from collections.abc import AsyncIterable, AsyncIterator, Iterable from dataclasses import dataclass from datetime import datetime from typing import Any, Generic, Literal @@ -46,11 +44,7 @@ from guidellm.scheduler import SchedulerState, SchedulingStrategy, StrategyType from guidellm.utils import Colors, format_value_display -__all__ = [ - "BenchmarkerProgress", - "BenchmarkerProgressGroup", - "GenerativeConsoleBenchmarkerProgress", -] +__all__ = ["BenchmarkerProgress", "GenerativeConsoleBenchmarkerProgress"] class BenchmarkerProgress(Generic[BenchmarkT], ABC): @@ -62,106 +56,15 @@ class BenchmarkerProgress(Generic[BenchmarkT], ABC): enable/disable functionality for conditional progress tracking. """ - def __init__(self, enabled: bool = True): + def __init__(self): """ Initialize progress tracker. :param enabled: Whether to enable progress tracking and display. """ - self._enabled = enabled self.profile: Profile = None self.current_strategy: SchedulingStrategy = None - @property - def enabled(self) -> bool: - """ - :return: Whether progress tracking is currently enabled. - """ - return self._enabled - - @enabled.setter - def enabled(self, value: bool) -> None: - """ - :param value: True to enable progress tracking, False to disable. - :raises RuntimeError: If called after progress run has started. - """ - if self.profile is not None: - raise RuntimeError( - "Cannot change enabled state after __call__ for progress run" - ) - - self._enabled = value - - def __call__( - self, - profile: Profile, - agen: AsyncIterable[ - tuple[ - EstimatedBenchmarkState | None, - BenchmarkT | None, - SchedulingStrategy, - SchedulerState | None, - ] - ], - ) -> AsyncIterator[ - tuple[ - EstimatedBenchmarkState | None, - BenchmarkT | None, - SchedulingStrategy, - SchedulerState | None, - ] - ]: - """ - Track progress through benchmark execution pipeline. - - Wraps the provided async generator to monitor benchmark progress, - calling appropriate lifecycle hooks based on execution state. - - :param profile: Benchmark profile configuration. - :param agen: Async generator yielding benchmark execution updates. - :return: Async iterator forwarding original updates with progress tracking. - """ - - async def aiterator() -> AsyncIterator[ - tuple[ - EstimatedBenchmarkState | None, - BenchmarkT | None, - SchedulingStrategy, - SchedulerState | None, - ] - ]: - self.profile = profile - if self.enabled: - await self.on_initialize(profile) - - async for aggregator_update, benchmark, strategy, scheduler_state in agen: - if self.enabled: - await self.on_raw_update( - profile, - aggregator_update, - benchmark, - strategy, - scheduler_state, - ) - - if self.current_strategy != strategy: - self.current_strategy = strategy - await self.on_benchmark_start(strategy) - elif benchmark is not None: - await self.on_benchmark_complete(benchmark) - self.current_strategy = None - else: - await self.on_benchmark_update( - aggregator_update, scheduler_state - ) - - yield aggregator_update, benchmark, strategy, scheduler_state - - if self.enabled: - await self.on_finalize() - - return aiterator() - @abstractmethod async def on_initialize(self, profile: Profile): """ @@ -180,14 +83,12 @@ async def on_benchmark_start(self, strategy: SchedulingStrategy): @abstractmethod async def on_benchmark_update( - self, - aggregator_update: EstimatedBenchmarkState, - scheduler_state: SchedulerState, + self, estimated_state: EstimatedBenchmarkState, scheduler_state: SchedulerState ): """ Handle benchmark execution progress update. - :param aggregator_update: Current benchmark metrics and statistics. + :param estimated_state: Current benchmark metrics and statistics. :param scheduler_state: Current scheduler execution state. """ @@ -203,155 +104,6 @@ async def on_benchmark_complete(self, benchmark: BenchmarkT): async def on_finalize(self): """Finalize progress tracking and cleanup resources.""" - async def on_raw_update( - self, - profile: Profile, - aggregator_update: EstimatedBenchmarkState | None, - benchmark: BenchmarkT | None, - strategy: SchedulingStrategy, - scheduler_state: SchedulerState | None, - ): - """ - Handle raw benchmark execution update. - - Optional hook for accessing all execution state updates. Default - implementation does nothing. - - :param profile: Benchmark profile configuration. - :param aggregator_update: Current benchmark metrics and statistics. - :param benchmark: Completed benchmark if available. - :param strategy: Current scheduling strategy. - :param scheduler_state: Current scheduler execution state. - """ - - -class BenchmarkerProgressGroup(BenchmarkerProgress[BenchmarkT]): - """ - Composite progress handler that manages multiple progress instances. - - Distributes progress events to all contained progress instances, enabling - parallel progress tracking through multiple channels (e.g., console display - and file logging). - - :param instances: Collection of progress handlers to manage. - :param enabled: Whether the group is active. - """ - - def __init__( - self, - instances: ( - Iterable[BenchmarkerProgress[BenchmarkT]] - | list[BenchmarkerProgress[BenchmarkT]] - ), - enabled: bool = True, - ): - """ - Initialize progress group with handler instances. - - :param instances: Progress handler instances to coordinate. - :param enabled: Whether to enable the progress group. - """ - self.instances: list[BenchmarkerProgress[BenchmarkT]] = list(instances) - super().__init__(enabled=enabled) - - @property - def enabled(self) -> bool: - """Whether the progress group is currently enabled.""" - return self._enabled - - @enabled.setter - def enabled(self, value: bool): - """ - Set enabled state for group and all contained instances. - - :param value: New enabled state. - """ - self._enabled = value - for instance in self.instances: - instance.enabled = value - - async def on_initialize(self, profile: Profile): - """ - Initialize all progress handler instances. - - :param profile: Benchmark profile configuration. - """ - await asyncio.gather( - *[child.on_initialize(profile) for child in self.instances] - ) - - async def on_benchmark_start(self, strategy: SchedulingStrategy): - """ - Notify all handlers of benchmark strategy start. - - :param strategy: Scheduling strategy being executed. - """ - await asyncio.gather( - *[child.on_benchmark_start(strategy) for child in self.instances] - ) - - async def on_benchmark_update( - self, - aggregator_update: EstimatedBenchmarkState, - scheduler_state: SchedulerState, - ): - """ - Distribute benchmark updates to all handlers. - - :param aggregator_update: Current benchmark metrics and statistics. - :param scheduler_state: Current scheduler execution state. - """ - await asyncio.gather( - *[ - child.on_benchmark_update(aggregator_update, scheduler_state) - for child in self.instances - ] - ) - - async def on_benchmark_complete(self, benchmark: BenchmarkT): - """ - Notify all handlers of benchmark completion. - - :param benchmark: Completed benchmark results. - """ - await asyncio.gather( - *[child.on_benchmark_complete(benchmark) for child in self.instances] - ) - - async def on_finalize(self): - """Finalize all progress handler instances.""" - await asyncio.gather(*[child.on_finalize() for child in self.instances]) - - async def on_raw_update( - self, - profile: Profile, - aggregator_update: EstimatedBenchmarkState | None, - benchmark: BenchmarkT | None, - strategy: SchedulingStrategy, - scheduler_state: SchedulerState | None, - ): - """ - Distribute raw updates to all handlers. - - :param profile: Benchmark profile configuration. - :param aggregator_update: Current benchmark metrics and statistics. - :param benchmark: Completed benchmark if available. - :param strategy: Current scheduling strategy. - :param scheduler_state: Current scheduler execution state. - """ - await asyncio.gather( - *[ - child.on_raw_update( - profile, - aggregator_update, - benchmark, - strategy, - scheduler_state, - ) - for child in self.instances - ] - ) - class GenerativeConsoleBenchmarkerProgress( BenchmarkerProgress[GenerativeBenchmark], Live @@ -364,14 +116,14 @@ class GenerativeConsoleBenchmarkerProgress( bars in a structured console interface. """ - def __init__(self, enabled: bool = True, display_scheduler_stats: bool = False): + def __init__(self, display_scheduler_stats: bool = False): """ Initialize console progress display. :param enabled: Whether to enable progress tracking and display. :param display_scheduler_stats: Whether to display scheduler statistics. """ - BenchmarkerProgress.__init__(self, enabled=enabled) + BenchmarkerProgress.__init__(self) Live.__init__( self, refresh_per_second=4, diff --git a/src/guidellm/benchmark/schemas.py b/src/guidellm/benchmark/schemas.py index 41cb832f..62ae5b0e 100644 --- a/src/guidellm/benchmark/schemas.py +++ b/src/guidellm/benchmark/schemas.py @@ -157,6 +157,7 @@ def add_avg_rate_metric( if self.get(start_time_key) is None: if start_time is None: start_time = time.time() + self[start_time_key] = start_time else: self[start_time_key] = start_time or self[start_time_key] @@ -595,7 +596,6 @@ class GenerativeTextMetricsSummary(StandardBaseDict): tokens: GenerativeMetricsSummary = Field(description="") words: GenerativeMetricsSummary = Field(description="") characters: GenerativeMetricsSummary = Field(description="") - bytes: GenerativeMetricsSummary = Field(description="") @classmethod def compile( @@ -628,12 +628,6 @@ def compile( metrics.text_characters or 0 for metrics in output_metrics ], ), - bytes=GenerativeMetricsSummary.compile( - request_types=request_types, - request_times=request_times, - input_values=[metrics.text_bytes or 0 for metrics in input_metrics], - output_values=[metrics.text_bytes or 0 for metrics in output_metrics], - ), ) diff --git a/src/guidellm/benchmark/types.py b/src/guidellm/benchmark/types.py index 8fa2dbfb..983e3189 100644 --- a/src/guidellm/benchmark/types.py +++ b/src/guidellm/benchmark/types.py @@ -7,9 +7,8 @@ from typing_extensions import TypeAliasType from guidellm.benchmark.output import GenerativeBenchmarkerOutput -from guidellm.benchmark.progress import BenchmarkerProgress -__all__ = ["OutputFormatT", "ProcessorInputT", "ProgressInputT"] +__all__ = ["OutputFormatT", "ProcessorInputT"] OutputFormatT = TypeAliasType( @@ -21,7 +20,3 @@ ) ProcessorInputT = TypeAliasType("ProcessorInputT", str | Path | PreTrainedTokenizerBase) - -ProgressInputT = TypeAliasType( - "ProgressInputT", tuple[str, ...] | list[str] | list[BenchmarkerProgress] -) diff --git a/src/guidellm/data/loaders.py b/src/guidellm/data/loaders.py index 0d83d726..fcdea15d 100644 --- a/src/guidellm/data/loaders.py +++ b/src/guidellm/data/loaders.py @@ -12,6 +12,7 @@ from guidellm.data.deserializers import DatasetDeserializerFactory from guidellm.data.preprocessors import DataDependentPreprocessor, DatasetPreprocessor +from guidellm.logger import logger __all__ = ["DataLoader", "DatasetsIterator"] @@ -29,7 +30,7 @@ def __init__( if not data or not isinstance(data, list): raise ValueError(f"Data must be a non-empty list, got {data}.") - if data_args is None: + if not data_args: data_args = [{} for _ in data] if len(data) != len(data_args): @@ -61,15 +62,15 @@ def __init__( def __iter__(self): worker_info = torch.utils.data.get_worker_info() - modulus = worker_info.num_workers if worker_info is not None else 1 - index = worker_info.id if worker_info is not None else 0 + worker_modulus = worker_info.num_workers if worker_info is not None else 1 + worker_index = worker_info.id if worker_info is not None else 0 if self.precache is not None: for index, item in enumerate(self.precache): - if index == index % modulus: + if (index + worker_index) % worker_modulus == 0: yield item else: - yield from self.generator(modulus=modulus, offset=index) + yield from self.generator(modulus=worker_modulus, offset=worker_index) def generator( self, @@ -83,19 +84,25 @@ def generator( dataset_iters = [iter(dataset) for dataset in self.datasets] while max_items is None or gen_count < max_items: - row = {"items": [next(dataset_iter) for dataset_iter in dataset_iters]} - gen_count += 1 - - if ( - modulus is not None - and offset is not None - and (gen_count % modulus) != offset - ): - continue - - for preprocessor in self.preprocessors: - row = preprocessor(row) - yield row + try: + row = { + "items": [next(dataset_iter) for dataset_iter in dataset_iters] + } + gen_count += 1 + + if ( + modulus is not None + and offset is not None + and (gen_count % modulus) != offset + ): + continue + + for preprocessor in self.preprocessors: + row = preprocessor(row) + yield row + except Exception as err: + logger.error(f"Skipping data row due to error: {err}") + gen_count -= 1 if max_items is not None and gen_count < max_items: raise ValueError( diff --git a/src/guidellm/data/preprocessors/formatters.py b/src/guidellm/data/preprocessors/formatters.py index 76b0083b..ce0e46fc 100644 --- a/src/guidellm/data/preprocessors/formatters.py +++ b/src/guidellm/data/preprocessors/formatters.py @@ -7,12 +7,7 @@ PreprocessorRegistry, ) from guidellm.data.schemas import GenerativeDatasetColumnType -from guidellm.data.utils import ( - encode_audio_as_dict, - encode_audio_as_file, - encode_image, - encode_video, -) +from guidellm.data.utils import encode_audio, encode_image, encode_video, text_stats from guidellm.schemas import GenerationRequest, GenerationRequestArguments, UsageMetrics __all__ = [ @@ -45,30 +40,29 @@ def __init__( def __call__( self, columns: dict[GenerativeDatasetColumnType, list[Any]] ) -> GenerationRequest: - body: dict[str, Any] = {} - arguments: GenerationRequestArguments = GenerationRequestArguments(body=body) + arguments: GenerationRequestArguments = GenerationRequestArguments(body={}) input_metrics = UsageMetrics() output_metrics = UsageMetrics() # Add model if self.model is not None: - body["model"] = self.model + arguments.body["model"] = self.model # Configure streaming if self.stream: arguments.stream = True - body["stream"] = True + arguments.body["stream"] = True # Handle output tokens if output_tokens := sum( count for count in columns.get("output_tokens_count_column", []) if count ): output_metrics.text_tokens = output_tokens - body["max_tokens"] = output_tokens - body["stop"] = None - body["ignore_eos"] = True + arguments.body["max_tokens"] = output_tokens + arguments.body["stop"] = None + arguments.body["ignore_eos"] = True elif self.max_tokens is not None: - body["max_tokens"] = self.max_tokens + arguments.body["max_tokens"] = self.max_tokens # Handle prompt tokens if prompt_tokens := sum( @@ -84,7 +78,10 @@ def __call__( prefix = "".join(pre for pre in columns.get("prefix_column", []) if pre) text = "".join(txt for txt in columns.get("text_column", []) if txt) if prefix or text: - body["prompt"] = prefix + text + arguments.body["prompt"] = prefix + text + stats = text_stats(arguments.body["prompt"]) + input_metrics.text_characters = stats.get("num_chars") + input_metrics.text_words = stats.get("num_words") return GenerationRequest( request_type="text_completions", @@ -126,26 +123,27 @@ def __init__( def __call__( self, columns: dict[GenerativeDatasetColumnType, list[Any]] ) -> GenerationRequest: - body: dict[str, Any] = {} - arguments = GenerationRequestArguments(body=body) + arguments = GenerationRequestArguments(body={}) input_metrics = UsageMetrics() output_metrics = UsageMetrics() # Add model if self.model is not None: - body["model"] = self.model + arguments.body["model"] = self.model # Configure streaming if self.stream: arguments.stream = True - body.update({"stream": True, "stream_options": {"include_usage": True}}) + arguments.body.update( + {"stream": True, "stream_options": {"include_usage": True}} + ) # Handle output tokens if output_tokens := sum( count for count in columns.get("output_tokens_count_column", []) if count ): output_metrics.text_tokens = output_tokens - body.update( + arguments.body.update( { "max_completion_tokens": output_tokens, "stop": None, @@ -153,7 +151,7 @@ def __call__( } ) elif self.max_completion_tokens is not None: - body["max_completion_tokens"] = self.max_completion_tokens + arguments.body["max_completion_tokens"] = self.max_completion_tokens # Handle prompt tokens if prompt_tokens := sum( @@ -166,63 +164,120 @@ def __call__( arguments.model_combine(self.extras) # Build messages - body["messages"] = ( - [ - {"role": "system", "content": prefix} - for prefix in columns.get("prefix_column", []) - if prefix - ] - + [ + arguments.body["messages"] = [] + + for prefix in columns.get("prefix_column", []): + if not prefix: + continue + + stats = text_stats(prefix) + if (num_chars := stats.get("num_chars")) is not None: + input_metrics.text_characters = ( + input_metrics.text_characters or 0 + ) + num_chars + if (num_words := stats.get("num_words")) is not None: + input_metrics.text_words = (input_metrics.text_words or 0) + num_words + + arguments.body["messages"].append({"role": "system", "content": prefix}) + + for text in columns.get("text_column", []): + if not text: + continue + + stats = text_stats(text) + if (num_chars := stats.get("num_chars")) is not None: + input_metrics.text_characters = ( + input_metrics.text_characters or 0 + ) + num_chars + if (num_words := stats.get("num_words")) is not None: + input_metrics.text_words = (input_metrics.text_words or 0) + num_words + + arguments.body["messages"].append( {"role": "user", "content": [{"type": "text", "text": text}]} - for text in columns.get("text_column", []) - if text - ] - + [ + ) + + for image in columns.get("image_column", []): + if not image: + continue + + image_dict = encode_image(image, **self.encode_image_kwargs) + if (image_pixels := image_dict.get("image_pixels")) is not None: + input_metrics.image_pixels = ( + input_metrics.image_pixels or 0 + ) + image_pixels + if (image_bytes := image_dict.get("image_bytes")) is not None: + input_metrics.image_bytes = ( + input_metrics.image_bytes or 0 + ) + image_bytes + + arguments.body["messages"].append( { "role": "user", "content": [ - { - "type": "image_url", - "image_url": encode_image( - image, **self.encode_image_kwargs - ), - } + {"type": "image_url", "image_url": image_dict.get("image")} ], } - for image in columns.get("image_column", []) - if image - ] - + [ + ) + + for video in columns.get("video_column", []): + if not video: + continue + + video_dict = encode_video(video, **self.encode_video_kwargs) + if (video_frames := video_dict.get("video_frames")) is not None: + input_metrics.video_frames = ( + input_metrics.video_frames or 0 + ) + video_frames + if (video_seconds := video_dict.get("video_seconds")) is not None: + input_metrics.video_seconds = ( + input_metrics.video_seconds or 0.0 + ) + video_seconds + if (video_bytes := video_dict.get("video_bytes")) is not None: + input_metrics.video_bytes = ( + input_metrics.video_bytes or 0 + ) + video_bytes + + arguments.body["messages"].append( { "role": "user", "content": [ - { - "type": "video_url", - "video_url": encode_video( - video, **self.encode_video_kwargs - ), - } + {"type": "video_url", "video_url": video_dict.get("video")} ], } - for video in columns.get("video_column", []) - if video - ] - + [ + ) + + for audio in columns.get("audio_column", []): + if not audio: + continue + + audio_dict = encode_audio(audio, b64encode=True, **self.encode_audio_kwargs) + if (audio_samples := audio_dict.get("audio_samples")) is not None: + input_metrics.audio_samples = ( + input_metrics.audio_samples or 0 + ) + audio_samples + if (audio_seconds := audio_dict.get("audio_seconds")) is not None: + input_metrics.audio_seconds = ( + input_metrics.audio_seconds or 0.0 + ) + audio_seconds + if (audio_bytes := audio_dict.get("audio_bytes")) is not None: + input_metrics.audio_bytes = ( + input_metrics.audio_bytes or 0 + ) + audio_bytes + + arguments.body["messages"].append( { "role": "user", "content": [ { "type": "input_audio", - "input_audio": encode_audio_as_dict( - audio, **self.encode_audio_kwargs - ), + "input_audio": { + "data": audio_dict.get("audio"), + "format": audio_dict.get("format"), + }, } ], } - for audio in columns.get("audio_column", []) - if audio - ] - ) + ) return GenerationRequest( request_type="chat_completions", @@ -253,19 +308,18 @@ def __init__( def __call__( # noqa: C901 self, columns: dict[GenerativeDatasetColumnType, list[Any]] ) -> GenerationRequest: - body: dict[str, Any] = {} - arguments = GenerationRequestArguments(body=body, files={}) + arguments = GenerationRequestArguments(body={}, files={}) input_metrics = UsageMetrics() output_metrics = UsageMetrics() # Add model if self.model is not None: - body["model"] = self.model + arguments.body["model"] = self.model # Configure streaming if self.stream: arguments.stream = True - body.update({"stream": True, "stream_options": {"include_usage": True}}) + arguments.body["stream"] = True # Handle output tokens if output_tokens := sum( @@ -284,19 +338,36 @@ def __call__( # noqa: C901 arguments.model_combine(self.extras) # Build audio input - if audio := [aud for aud in columns.get("audio_column", []) if aud]: - file_name, content, mime_type = encode_audio_as_file( - audio[0], **self.encode_audio_kwargs + audio_columns = columns.get("audio_column", []) + if len(audio_columns) != 1: + raise ValueError( + f"GenerativeAudioTranscriptionRequestFormatter expects exactly " + f"one audio column, but got {len(audio_columns)}." + ) + + audio_dict = encode_audio( + audio_columns[0], b64encode=False, **self.encode_audio_kwargs + ) + input_metrics.audio_samples = audio_dict.get("audio_samples") + input_metrics.audio_seconds = audio_dict.get("audio_seconds") + input_metrics.audio_bytes = audio_dict.get("audio_bytes") + + arguments.files = { + "file": ( + audio_dict.get("file_name", "audio_input"), + audio_dict.get("audio"), + audio_dict.get("mimetype"), ) - arguments.files = {"file": (file_name, content, mime_type)} - else: - raise ValueError("No audio column found for audio transcription request.") + } # Build prompt prefix = "".join(pre for pre in columns.get("prefix_column", []) if pre) text = "".join(txt for txt in columns.get("text_column", []) if txt) if prefix or text: - body["prompt"] = prefix + text + arguments.body["prompt"] = prefix + text + stats = text_stats(arguments.body["prompt"]) + input_metrics.text_characters = stats.get("num_chars") + input_metrics.text_words = stats.get("num_words") return GenerationRequest( request_type="audio_transcriptions", diff --git a/src/guidellm/data/utils/__init__.py b/src/guidellm/data/utils/__init__.py index aac657f8..cd257898 100644 --- a/src/guidellm/data/utils/__init__.py +++ b/src/guidellm/data/utils/__init__.py @@ -1,34 +1,22 @@ from .dataset import DEFAULT_SPLITS, resolve_dataset_split from .functions import ( - download_audio, - download_image, - download_video, encode_audio, - encode_audio_as_dict, - encode_audio_as_file, encode_image, - encode_image_base64, encode_video, - encode_video_base64, get_file_format, is_url, resize_image, + text_stats, ) __all__ = [ "DEFAULT_SPLITS", - "download_audio", - "download_image", - "download_video", "encode_audio", - "encode_audio_as_dict", - "encode_audio_as_file", "encode_image", - "encode_image_base64", "encode_video", - "encode_video_base64", "get_file_format", "is_url", "resize_image", "resolve_dataset_split", + "text_stats", ] diff --git a/src/guidellm/data/utils/functions.py b/src/guidellm/data/utils/functions.py index 413b5a92..e11c5cb8 100644 --- a/src/guidellm/data/utils/functions.py +++ b/src/guidellm/data/utils/functions.py @@ -5,7 +5,6 @@ from pathlib import Path from typing import Any, Literal -import datasets import httpx import librosa import numpy as np @@ -15,19 +14,13 @@ from torch import Tensor __all__ = [ - "download_audio", - "download_image", - "download_video", "encode_audio", - "encode_audio_as_dict", - "encode_audio_as_file", "encode_image", - "encode_image_base64", "encode_video", - "encode_video_base64", "get_file_format", "is_url", "resize_image", + "text_stats", ] @@ -35,13 +28,30 @@ def is_url(text: Any) -> bool: return isinstance(text, str) and text.startswith(("http://", "https://")) +def text_stats( + text: str, +) -> dict[Literal["type", "text", "num_chars", "num_words"], str | int]: + """Compute basic text statistics.""" + num_chars = len(text) + num_words = len(text.split()) + + return { + "type": "text", + "text": text, + "num_chars": num_chars, + "num_words": num_words, + } + + def encode_image( - image: bytes | str | Path | np.ndarray | PILImage.Image | datasets.Image, + image: bytes | str | Path | np.ndarray | PILImage.Image, + width: int | None = None, + height: int | None = None, max_size: int | None = None, max_width: int | None = None, max_height: int | None = None, - encode_type: Literal["base64", "url"] | None = None, -) -> str: + encode_type: Literal["base64", "url"] | None = "base64", +) -> dict[Literal["type", "image", "image_pixels", "image_bytes"], str | int | None]: """ Input image types: - bytes: raw image bytes, decoded with Pillow @@ -64,71 +74,67 @@ def encode_image( - image url - "data:image/{type};base64, {data}" string """ - url = is_url(image) + if isinstance(image, str) and is_url(image): + if encode_type == "base64": + response = httpx.get(image) + response.raise_for_status() + return encode_image( + image=response.content, + max_size=max_size, + max_width=max_width, + max_height=max_height, + encode_type="base64", + ) - if ( - url - and (encode_type is None or encode_type == "url") - and (max_size is not None or max_width is not None or max_height is not None) - ): - raise ValueError("Cannot resize image when encode_type is 'url'") - elif url and (encode_type is None or encode_type == "url"): - return image - elif url and encode_type == "base64": - raise ValueError(f"Cannot convert non-url image to URL {image}") - - return encode_image_base64( - image=image, - max_size=max_size, - max_width=max_width, - max_height=max_height, - ) + if any([width, height, max_size, max_width, max_height]): + raise ValueError(f"Cannot resize image {image} when encode_type is 'url'") + return { + "type": "image_url", + "image": image, + "image_pixels": None, + "image_bytes": None, + } -def encode_image_base64( - image: bytes | str | Path | np.ndarray | PILImage.Image, - width: int | None = None, - height: int | None = None, - max_width: int | None = None, - max_height: int | None = None, - max_size: int | None = None, -) -> str: - if ( - isinstance(image, str) - and image.startswith("data:image/") - and ";base64," in image - ): - return image - - if is_url(image): - image = download_image(image) + decoded_image: PILImage.Image if isinstance(image, bytes): - image = PILImage.open(io.BytesIO(image)) - elif isinstance(image, (str, Path)): - image = PILImage.open(image) + decoded_image = PILImage.open(io.BytesIO(image)) + elif isinstance(image, str) and image.startswith("data:image/"): + _, encoded = image.split(",", 1) + image_data = base64.b64decode(encoded) + decoded_image = PILImage.open(io.BytesIO(image_data)) + elif isinstance(image, str | Path): + decoded_image = PILImage.open(image) elif isinstance(image, np.ndarray): - image = PILImage.fromarray(image) - elif not isinstance(image, PILImage.Image): - raise ValueError(f"Unsupported image type: {type(image)}") + decoded_image = PILImage.fromarray(image) + elif isinstance(image, PILImage.Image): + decoded_image = image + else: + raise ValueError(f"Unsupported image type: {type(image)} for {image}") - image = resize_image( - image, + output_image = resize_image( + decoded_image, width=width, height=height, max_width=max_width, max_height=max_height, max_size=max_size, ) - if image.mode != "RGB": - image = image.convert("RGB") + if output_image.mode != "RGB": + output_image = output_image.convert("RGB") buffer = io.BytesIO() - image.save(buffer, format="JPEG") + output_image.save(buffer, format="JPEG") image_bytes = buffer.getvalue() image_base64 = base64.b64encode(image_bytes).decode("utf-8") - return f"data:image/jpeg;base64,{image_base64}" + return { + "type": "image_base64", + "image": f"data:image/jpeg;base64,{image_base64}", + "image_pixels": output_image.width * output_image.height, + "image_bytes": len(image_bytes), + } def resize_image( @@ -176,16 +182,13 @@ def resize_image( return image -def download_image(url: str) -> bytes: - response = httpx.get(url) - response.raise_for_status() - return response.content - - def encode_video( video: bytes | str | Path, - encode_type: Literal["base64", "url"] | None = None, -) -> str: + encode_type: Literal["base64", "url"] | None = "base64", +) -> dict[ + Literal["type", "video", "video_frames", "video_seconds", "video_bytes"], + str | int | float | None, +]: """ Input video types: - bytes: raw video bytes @@ -202,97 +205,55 @@ def encode_video( - video url - "data:video/{type};base64, {data}" string """ - if ( - isinstance(video, str) - and is_url(video) - and (encode_type is None or encode_type == "url") - ): - return video - elif isinstance(video, str) and is_url(video) and encode_type == "base64": - raise ValueError(f"Cannot encode URL video {video}") - - return encode_video_base64(video=video) - - -def encode_video_base64(video: bytes | str | Path) -> str: - if ( - isinstance(video, str) - and video.startswith("data:video/") - and ";base64," in video - ): - return video - - video_format = "unknown" - if isinstance(video, str) and is_url(video): - video, video_format = download_video(video) - - if isinstance(video, (str, Path)): + if encode_type == "base64": + response = httpx.get(video) + response.raise_for_status() + return encode_video(video=response.content, encode_type="base64") + + return { + "type": "video_url", + "video": video, + "video_frames": None, + "video_seconds": None, + "video_bytes": None, + } + + if isinstance(video, str) and video.startswith("data:video/"): + data_str = video.split(",", 1)[1] + + return { + "type": "video_base64", + "video": video, + "video_frames": None, + "video_seconds": None, + "video_bytes": len(data_str) * 3 // 4, # base64 to bytes + } + + if isinstance(video, str | Path): path = Path(video) - video = path.read_bytes() + video_bytes = path.read_bytes() video_format = get_file_format(path) - elif not isinstance(video, bytes): - raise ValueError(f"Unsupported video type: {type(video)}") + elif isinstance(video, bytes): + video_bytes = video + video_format = "unknown" + else: + raise ValueError(f"Unsupported video type: {type(video)} for {video}") video_base64 = base64.b64encode(video).decode("utf-8") - return f"data:video/{video_format};base64,{video_base64}" - - -def download_video(url: str) -> tuple[bytes, str]: - response = httpx.get(url) - response.raise_for_status() - return response.content, get_file_format(url) - - -def encode_audio_as_dict( - audio: Any, - sample_rate: int = 16000, - encode_sample_rate: int = 16000, - max_duration: float | None = None, - mono: bool = True, - audio_format: str = "mp3", - bitrate: str = "64k", -) -> dict[Literal["data", "format"], Any]: - content, _, file_format = encode_audio( - audio=audio, - sample_rate=sample_rate, - encode_sample_rate=encode_sample_rate, - max_duration=max_duration, - mono=mono, - audio_format=audio_format, - bitrate=bitrate, - ) return { - "data": base64.b64encode(content).decode("utf-8"), - "format": file_format, + "type": "video_base64", + "video": f"data:video/{video_format};base64,{video_base64}", + "video_frames": None, + "video_seconds": None, + "video_bytes": len(video_bytes), } -def encode_audio_as_file( - audio: Any, - sample_rate: int = 16000, - encode_sample_rate: int = 16000, - max_duration: float | None = None, - mono: bool = True, - audio_format: str = "mp3", - bitrate: str = "64k", -) -> tuple[str, bytes, str]: - content, file_name, file_format = encode_audio( - audio=audio, - sample_rate=sample_rate, - encode_sample_rate=encode_sample_rate, - max_duration=max_duration, - mono=mono, - audio_format=audio_format, - bitrate=bitrate, - ) - - return file_name, content, f"audio/{file_format}" - - -def encode_audio( # noqa: PLR0912, PLR0911, C901 +def encode_audio( audio: Any, + b64encode: bool, sample_rate: int = 16000, file_name: str = "audio.wav", encode_sample_rate: int = 16000, @@ -300,38 +261,18 @@ def encode_audio( # noqa: PLR0912, PLR0911, C901 mono: bool = True, audio_format: str = "mp3", bitrate: str = "64k", -) -> tuple[bytes, str, str]: - audio_buffer: io.BytesIO = io.BytesIO() - - if hasattr(audio, "get_samples_played_in_range"): - # HF datasets Audio object - audio_samples = audio.get_samples_played_in_range( - start_seconds=0.0, - stop_seconds=None - if max_duration is None - else min(max_duration, audio.metadata.duration_seconds_from_header), - ) - return encode_audio( - audio=audio_samples.data.numpy(), - sample_rate=audio_samples.sample_rate, - encode_sample_rate=encode_sample_rate, - max_duration=max_duration, - mono=mono, - audio_format=audio_format, - bitrate=bitrate, - ) - - if isinstance(audio, Tensor): - return encode_audio( - audio=audio.numpy(), - sample_rate=sample_rate, - encode_sample_rate=encode_sample_rate, - max_duration=max_duration, - mono=mono, - audio_format=audio_format, - bitrate=bitrate, - ) - +) -> dict[ + Literal[ + "type", + "audio", + "format", + "mimetype", + "audio_samples", + "audio_seconds", + "audio_bytes", + ], + str | int | float | None, +]: if isinstance(audio, dict): sample_rate = audio.get("sample_rate", audio.get("sampling_rate", sample_rate)) if "data" not in audio and "url" not in audio: @@ -348,70 +289,65 @@ def encode_audio( # noqa: PLR0912, PLR0911, C901 bitrate=bitrate, ) - if isinstance(audio, str) and is_url(audio): - audio_bytes, file_name, _ = download_audio(audio) - return encode_audio( - audio=audio_bytes, - sample_rate=sample_rate, - encode_sample_rate=encode_sample_rate, - max_duration=max_duration, - mono=mono, - audio_format=audio_format, - bitrate=bitrate, - ) - - if isinstance(audio, (str, Path)): - if not Path(audio).exists(): - raise ValueError(f"Audio file does not exist: {audio}") - file_name = get_file_name(audio) - data, sample_rate = soundfile.read(str(audio), dtype="float32") + audio_numpy: np.ndarray - return encode_audio( - audio=data, - sample_rate=sample_rate, - encode_sample_rate=encode_sample_rate, - max_duration=max_duration, - mono=mono, - audio_format=audio_format, - bitrate=bitrate, + if hasattr(audio, "get_samples_played_in_range"): + # HF datasets Audio object + audio_samples = audio.get_samples_played_in_range( + start_seconds=0.0, + stop_seconds=( + None + if max_duration is None + else min(max_duration, audio.metadata.duration_seconds_from_header) + ), ) - - if isinstance(audio, bytes): - data, sample_rate = soundfile.read(io.BytesIO(audio), dtype="float32") - - return encode_audio( - audio=data, - sample_rate=sample_rate, - encode_sample_rate=encode_sample_rate, - max_duration=max_duration, - mono=mono, - audio_format=audio_format, - bitrate=bitrate, + audio_numpy = np.array(audio_samples.data) + elif isinstance(audio, Tensor): + audio_numpy = audio.numpy() + elif isinstance(audio, str | Path): + if is_url(audio): + response = httpx.get(audio) + response.raise_for_status() + audio_stream = response.content + file_name = get_file_name(audio) + else: + if not Path(audio).exists(): + raise ValueError(f"Audio file does not exist: {audio}") + file_name = get_file_name(audio) + audio_stream = Path(audio).read_bytes() + + audio_numpy, sample_rate = soundfile.read( + io.BytesIO(audio_stream), dtype="float32" ) - - if not isinstance(audio, np.ndarray): + elif isinstance(audio, bytes): + audio_numpy, sample_rate = soundfile.read(io.BytesIO(audio), dtype="float32") + elif isinstance(audio, np.ndarray): + audio_numpy = audio + else: raise ValueError(f"Unsupported audio type: {type(audio)}") if sample_rate != encode_sample_rate: - audio = librosa.resample( - audio.astype(np.float32), orig_sr=sample_rate, target_sr=encode_sample_rate + audio_numpy = librosa.resample( + audio_numpy.astype(np.float32), + orig_sr=sample_rate, + target_sr=encode_sample_rate, ) sample_rate = encode_sample_rate - audio = librosa.to_mono(audio) + audio_numpy = librosa.to_mono(audio_numpy) if ( max_duration is not None and max_duration > 0 - and (max_samples := int(max_duration * sample_rate)) < len(audio) + and (max_samples := int(max_duration * sample_rate)) < len(audio_numpy) ): - audio = audio[:max_samples] + audio_numpy = audio_numpy[max_samples:] audio_buffer = io.BytesIO() if audio_format.lower() == "mp3": wav = io.BytesIO() - soundfile.write(wav, audio, sample_rate, format="WAV", subtype="PCM_16") + soundfile.write(wav, audio_numpy, sample_rate, format="WAV", subtype="PCM_16") wav.seek(0) sound = AudioSegment.from_wav(wav) @@ -420,15 +356,22 @@ def encode_audio( # noqa: PLR0912, PLR0911, C901 soundfile.write(audio_buffer, audio, sample_rate, format=audio_format.upper()) audio_buffer.seek(0) - return audio_buffer.read(), file_name, audio_format.lower() - + decoded_audio = audio_buffer.read() -def download_audio(url: str) -> tuple[bytes, str, str]: - response = httpx.get(url) - response.raise_for_status() - content = response.content - - return content, get_file_name(url), get_file_format(url) + return { + "type": "audio_base64" if b64encode else "audio_file", + "audio": ( + base64.b64encode(decoded_audio).decode("utf-8") + if b64encode + else decoded_audio + ), + "file_name": file_name, + "format": audio_format, + "mimetype": f"audio/{audio_format}", + "audio_samples": len(audio_numpy), + "audio_seconds": len(audio_numpy) / sample_rate, + "audio_bytes": len(decoded_audio), + } def get_file_name(path: Path | str) -> str: diff --git a/src/guidellm/scheduler/worker_group.py b/src/guidellm/scheduler/worker_group.py index 41c41f21..21394668 100644 --- a/src/guidellm/scheduler/worker_group.py +++ b/src/guidellm/scheduler/worker_group.py @@ -22,6 +22,7 @@ from multiprocessing.synchronize import Barrier, Event from typing import Generic, NamedTuple +from guidellm.logger import logger from guidellm.scheduler.constraints import Constraint, RequestsExhaustedConstraint from guidellm.scheduler.schemas import ( BackendInterface, @@ -349,6 +350,7 @@ async def request_updates( """ while True: if self.error_event.is_set(): # type: ignore[union-attr] + logger.error("Error event set in WorkerProcessGroup") raise RuntimeError( "error_event is set in WorkerProcessGroup, " "indicating an error occurred in one of the worker processes." @@ -507,40 +509,47 @@ def _iter() -> Iterator[RequestT | MultiTurnRequestT[RequestT]]: while True: yield from cycle_requests - count = 0 - - for request in _iter(): - count += 1 - - if hasattr(request, "request_id"): - request_id = request.request_id - elif hasattr(request, "id"): - request_id = request.id - else: - request_id = str(uuid.uuid4()) - request_info: RequestInfo = RequestInfo( - request_id=request_id, - status="queued", - scheduler_process_id=0, - scheduler_start_time=self.start_time, + try: + count = 0 + request_iter = _iter() + for request in request_iter: + count += 1 + + if hasattr(request, "request_id"): + request_id = request.request_id + elif hasattr(request, "id"): + request_id = request.id + else: + request_id = str(uuid.uuid4()) + request_info: RequestInfo = RequestInfo( + request_id=request_id, + status="queued", + scheduler_process_id=0, + scheduler_start_time=self.start_time, + ) + state_update = self._locked_update(request_info) + request_info.timings.queued = time.time() + + yield (request, request_info) + + if state_update.stop_queueing: + self.stop_send_requests_event.set() + return + + # Reached the end, inject a RequestsExhaustedConstraint to record + self._locked_update( + info=None, + requests_exhausted={ + "requests_exhausted": RequestsExhaustedConstraint( + num_requests=count + ) + }, ) - state_update = self._locked_update(request_info) - request_info.timings.queued = time.time() - - yield (request, request_info) - - if state_update.stop_queueing: - self.stop_send_requests_event.set() - return - - # Reached the end, inject a RequestsExhaustedConstraint to record - self._locked_update( - info=None, - requests_exhausted={ - "requests_exhausted": RequestsExhaustedConstraint(num_requests=count) - }, - ) - self.stop_send_requests_event.set() + self.stop_send_requests_event.set() + except Exception as err: + logger.error(f"Error generating requests: {err}") + self.error_event.set() + raise err def received_callback( self, @@ -565,31 +574,40 @@ def received_callback( :param update: Tuple containing response, request, and request info :return: Updated tuple with injected scheduler state """ - response, request, request_info = update - state_update = self._locked_update(info=request_info) + try: + response, request, request_info = update + state_update = self._locked_update(info=request_info) - # Check if we need to tell workers to stop pulling new requests - # based on no more requests sent and all requests removed from queue - if ( - state_update.state.queued_requests == 0 - and self.stop_send_requests_event.is_set() - and not self.requests_generated_event.is_set() - ): - self.requests_generated_event.set() + # Check if we need to tell workers to stop pulling new requests + # based on no more requests sent and all requests removed from queue + if ( + state_update.state.queued_requests == 0 + and self.stop_send_requests_event.is_set() + and not self.requests_generated_event.is_set() + ): + self.requests_generated_event.set() - # Check if we need to tell workers to stop processing requests (constraints) - if state_update.stop_processing and not self.constraint_reached_event.is_set(): - self.constraint_reached_event.set() + # Check if we need to tell workers to stop processing requests (constraints) + if ( + state_update.stop_processing + and not self.constraint_reached_event.is_set() + ): + self.constraint_reached_event.set() - # Check if all requests have been processed and can shutdown - if ( - state_update.state.processed_requests == state_update.state.created_requests - and self.stop_send_requests_event.is_set() - and self.requests_generated_event.is_set() - and self.constraint_reached_event.is_set() - and not self.shutdown_event.is_set() - ): - self.shutdown_event.set() + # Check if all requests have been processed and can shutdown + if ( + state_update.state.processed_requests + == state_update.state.created_requests + and self.stop_send_requests_event.is_set() + and self.requests_generated_event.is_set() + and self.constraint_reached_event.is_set() + and not self.shutdown_event.is_set() + ): + self.shutdown_event.set() + except Exception as err: + logger.error(f"Error processing received update: {err}") + self.error_event.set() + raise err return ( response, diff --git a/src/guidellm/schemas/request.py b/src/guidellm/schemas/request.py index 3538ce0a..9e9189fc 100644 --- a/src/guidellm/schemas/request.py +++ b/src/guidellm/schemas/request.py @@ -114,9 +114,6 @@ class UsageMetrics(StandardBaseDict): text_characters: int | None = Field( default=None, description="Number of text characters processed/generated." ) - text_bytes: int | None = Field( - default=None, description="Number of text bytes processed/generated." - ) # Vision image stats image_tokens: int | None = Field( From 6adf7931864677213491d7a523913f962f6927d9 Mon Sep 17 00:00:00 2001 From: Mark Kurtz Date: Tue, 14 Oct 2025 15:21:18 -0400 Subject: [PATCH 30/57] Update src/guidellm/backends/openai.py Co-authored-by: Jared O'Connell <46976761+jaredoconnell@users.noreply.github.com> Signed-off-by: Mark Kurtz --- src/guidellm/backends/openai.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/guidellm/backends/openai.py b/src/guidellm/backends/openai.py index 9fac16b5..1e74fc6e 100644 --- a/src/guidellm/backends/openai.py +++ b/src/guidellm/backends/openai.py @@ -228,7 +228,7 @@ async def resolve( raise RuntimeError("Backend not started up for process.") if history is not None: - raise NotImplementedError("Multi-turn requests not yet supported") + raise NotImplementedError("Multi-turn requests not yet supported") response_handler = self._resolve_response_handler( request_type=request.request_type From 687b52fd2ff96ee643220bbb4cc9aea12edd5646 Mon Sep 17 00:00:00 2001 From: Mark Kurtz Date: Tue, 14 Oct 2025 15:35:29 -0400 Subject: [PATCH 31/57] Updates from review for multi modal data --- src/guidellm/backends/response_handlers.py | 67 +++++++++++----------- src/guidellm/benchmark/entrypoints.py | 2 +- 2 files changed, 34 insertions(+), 35 deletions(-) diff --git a/src/guidellm/backends/response_handlers.py b/src/guidellm/backends/response_handlers.py index 44c949e6..b7bd06ad 100644 --- a/src/guidellm/backends/response_handlers.py +++ b/src/guidellm/backends/response_handlers.py @@ -1,11 +1,10 @@ """ Response handlers for processing API responses from different generation backends. -This module provides a pluggable system for handling responses from various language -model backends, supporting both streaming and non-streaming responses. Each handler -implements the GenerationResponseHandler protocol to parse API responses, extract -usage metrics, and convert them into standardized GenerationResponse objects for the -benchmark system. +Provides a pluggable system for handling responses from language model backends, +supporting both streaming and non-streaming responses. Each handler implements the +GenerationResponseHandler protocol to parse API responses, extract usage metrics, +and convert them into standardized GenerationResponse objects. """ from __future__ import annotations @@ -26,11 +25,11 @@ class GenerationResponseHandler(Protocol): """ - Protocol defining the interface for handling generation API responses. + Protocol for handling generation API responses. - Response handlers implement this protocol to process both streaming and - non-streaming responses from different backend APIs, converting them into - standardized GenerationResponse objects with consistent metrics extraction. + Defines the interface for processing both streaming and non-streaming responses + from backend APIs, converting them into standardized GenerationResponse objects + with consistent metrics extraction. """ def compile_non_streaming( @@ -39,7 +38,7 @@ def compile_non_streaming( """ Process a complete non-streaming API response. - :param request: The original generation request + :param request: Original generation request :param response: Raw API response data from the backend :return: Standardized GenerationResponse with extracted metrics """ @@ -58,7 +57,7 @@ def compile_streaming(self, request: GenerationRequest) -> GenerationResponse: """ Compile accumulated streaming data into a final response. - :param request: The original generation request + :param request: Original generation request :return: Standardized GenerationResponse with extracted metrics """ ... @@ -68,9 +67,9 @@ class GenerationResponseHandlerFactory(RegistryMixin[type[GenerationResponseHand """ Factory for registering and creating response handlers by backend type. - Provides a registry-based system for associating handler classes with specific - backend API types, enabling automatic selection of the appropriate handler - for processing responses from different generation services. + Registry-based system for associating handler classes with specific backend API + types, enabling automatic selection of the appropriate handler for processing + responses from different generation services. """ @@ -79,9 +78,9 @@ class TextCompletionsResponseHandler(GenerationResponseHandler): """ Response handler for OpenAI-style text completion endpoints. - Processes responses from text completion APIs that return generated text - in the 'choices' array with 'text' fields. Handles both streaming and - non-streaming responses, extracting usage metrics for input and output tokens. + Processes responses from text completion APIs that return generated text in the + 'choices' array with 'text' fields. Handles both streaming and non-streaming + responses, extracting usage metrics for input and output tokens. Example: :: @@ -105,7 +104,7 @@ def compile_non_streaming( """ Process a complete text completion response. - :param request: The original generation request + :param request: Original generation request :param response: Complete API response containing choices and usage data :return: Standardized GenerationResponse with extracted text and metrics """ @@ -151,7 +150,7 @@ def compile_streaming(self, request: GenerationRequest) -> GenerationResponse: """ Compile accumulated streaming text chunks into a final response. - :param request: The original generation request + :param request: Original generation request :return: Standardized GenerationResponse with concatenated text and metrics """ input_metrics, output_metrics = self.extract_metrics(self.streaming_usage) @@ -171,7 +170,7 @@ def extract_line_data(self, line: str) -> dict[str, Any] | None: Extract JSON data from a streaming response line. :param line: Raw line from the streaming response - :return: Parsed JSON data as a dictionary, or None if line is invalid + :return: Parsed JSON data as dictionary, or None if line indicates completion """ if line == "data: [DONE]": return None @@ -190,7 +189,7 @@ def extract_choices_and_usage( Extract choices and usage data from the API response. :param response: Complete API response containing choices and usage data - :return: Tuple of (choices list, usage dictionary) + :return: Tuple of choices list and usage dictionary """ return response.get("choices", []), response.get("usage", {}) @@ -201,7 +200,7 @@ def extract_metrics( Extract input and output usage metrics from API response usage data. :param usage: Usage data dictionary from API response - :return: Tuple of (input_metrics, output_metrics) as UsageMetrics objects + :return: Tuple of input_metrics and output_metrics as UsageMetrics objects """ if not usage: return UsageMetrics(), UsageMetrics() @@ -236,9 +235,9 @@ class ChatCompletionsResponseHandler(TextCompletionsResponseHandler): """ Response handler for OpenAI-style chat completion endpoints. - Extends TextCompletionsResponseHandler to handle chat completion responses - where generated text is nested within message objects in the choices array. - Processes both streaming and non-streaming chat completion responses. + Extends TextCompletionsResponseHandler to handle chat completion responses where + generated text is nested within message objects in the choices array. Processes + both streaming and non-streaming chat completion responses. """ def compile_non_streaming( @@ -247,10 +246,10 @@ def compile_non_streaming( """ Process a complete chat completion response. - Extracts content from the message object within choices, handling the - nested structure specific to chat completion endpoints. + Extracts content from the message object within choices, handling the nested + structure specific to chat completion endpoints. - :param request: The original generation request + :param request: Original generation request :param response: Complete API response containing choices and usage data :return: Standardized GenerationResponse with extracted content and metrics """ @@ -271,8 +270,8 @@ def add_streaming_line(self, line: str) -> int | None: """ Process a single line from a chat completion streaming response. - Handles the chat completion specific delta structure where content - is nested within delta objects in the streaming response chunks. + Handles the chat completion specific delta structure where content is nested + within delta objects in the streaming response chunks. :param line: Raw SSE line from the streaming response :return: 1 if content was extracted, 0 if line ignored, None if done @@ -296,7 +295,7 @@ def compile_streaming(self, request: GenerationRequest) -> GenerationResponse: """ Compile accumulated streaming chat completion content into a final response. - :param request: The original generation request + :param request: Original generation request :return: Standardized GenerationResponse with concatenated content and metrics """ input_metrics, output_metrics = self.extract_metrics(self.streaming_usage) @@ -349,7 +348,7 @@ def compile_non_streaming( Extracts transcribed or translated text and audio-specific usage metrics including processing duration and token counts for audio content. - :param request: The original generation request + :param request: Original generation request :param response: Complete API response containing text and usage data :return: Standardized GenerationResponse with extracted text and metrics """ @@ -412,7 +411,7 @@ def compile_streaming(self, request: GenerationRequest) -> GenerationResponse: """ Compile accumulated streaming audio text into a final response. - :param request: The original generation request + :param request: Original generation request :return: Standardized GenerationResponse with concatenated text and metrics """ input_metrics, output_metrics = self.extract_metrics(self.streaming_usage) @@ -437,7 +436,7 @@ def extract_metrics( in addition to standard text token counts. :param usage: Usage data dictionary from audio API response - :return: Tuple of (input_metrics, output_metrics) as UsageMetrics objects + :return: Tuple of input_metrics and output_metrics as UsageMetrics objects """ if not usage: return UsageMetrics(), UsageMetrics() diff --git a/src/guidellm/benchmark/entrypoints.py b/src/guidellm/benchmark/entrypoints.py index 18768216..61dfa680 100644 --- a/src/guidellm/benchmark/entrypoints.py +++ b/src/guidellm/benchmark/entrypoints.py @@ -412,7 +412,7 @@ async def reimport_benchmarks_report( ) -> tuple[GenerativeBenchmarksReport, dict[str, Any]]: """ The command-line entry point for re-importing and displaying an - existing benchmarks report. Can also specify + existing benchmarks report. Can also specify an output format. Assumes the file provided exists. """ console = Console() From b162fb35f2f167841a1c2f220066e8b3a52b8dac Mon Sep 17 00:00:00 2001 From: Jared O'Connell <46976761+jaredoconnell@users.noreply.github.com> Date: Tue, 14 Oct 2025 15:44:39 -0400 Subject: [PATCH 32/57] Revert "Features/add tooltip to line chart" (#409) Reverts vllm-project/guidellm#392 That PR was rebased without using the new data types in the refactor branch. Due to it breaking things, it makes most sense to revert it and later submit a new one for the feature. --- src/guidellm/presentation/data_models.py | 24 ++-------------- .../MetricLine/MetricLine.component.tsx | 28 ++----------------- .../MetricsSummary.component.tsx | 2 +- src/ui/lib/store/benchmarksWindowData.ts | 10 ------- .../benchmarks/benchmarks.interfaces.ts | 1 - .../slices/benchmarks/benchmarks.selectors.ts | 25 +++-------------- tests/unit/presentation/test_data_models.py | 10 +------ 7 files changed, 11 insertions(+), 89 deletions(-) diff --git a/src/guidellm/presentation/data_models.py b/src/guidellm/presentation/data_models.py index 2401b3ef..ff2863b4 100644 --- a/src/guidellm/presentation/data_models.py +++ b/src/guidellm/presentation/data_models.py @@ -5,8 +5,6 @@ from pydantic import BaseModel, computed_field -from guidellm.scheduler.strategy import SchedulingStrategy - if TYPE_CHECKING: from guidellm.benchmark import GenerativeBenchmark @@ -214,30 +212,12 @@ class BenchmarkDatum(BaseModel): ttft: TabularDistributionSummary throughput: TabularDistributionSummary time_per_request: TabularDistributionSummary - strategy_display_str: str - - @classmethod - def get_strategy_display_str(cls, strategy: SchedulingStrategy): - strategy_type = strategy if isinstance(strategy, str) else strategy.type_ - strategy_instance = ( - strategy if isinstance(strategy, SchedulingStrategy) else None - ) - - if strategy_type == "concurrent": - rate = f"@{strategy.streams}" if strategy_instance else "@##" # type: ignore[attr-defined] - elif strategy_type in ("constant", "poisson"): - rate = f"@{strategy.rate:.2f}" if strategy_instance else "@#.##" # type: ignore[attr-defined] - else: - rate = "" - return f"{strategy_type}{rate}" @classmethod def from_benchmark(cls, bm: "GenerativeBenchmark"): - rps = bm.metrics.requests_per_second.successful.mean return cls( - strategy_display_str=cls.get_strategy_display_str(bm.args.strategy), - requests_per_second=rps, - itl=TabularDistributionSummary.from_distribution_summary( + requests_per_second=bm.metrics.requests_per_second.successful.mean, + tpot=TabularDistributionSummary.from_distribution_summary( bm.metrics.inter_token_latency_ms.successful ), ttft=TabularDistributionSummary.from_distribution_summary( diff --git a/src/ui/lib/components/Charts/MetricLine/MetricLine.component.tsx b/src/ui/lib/components/Charts/MetricLine/MetricLine.component.tsx index eb123593..8b1b4df2 100644 --- a/src/ui/lib/components/Charts/MetricLine/MetricLine.component.tsx +++ b/src/ui/lib/components/Charts/MetricLine/MetricLine.component.tsx @@ -1,6 +1,5 @@ -import { Typography, useTheme } from '@mui/material'; -import { PointTooltipProps, ResponsiveLine } from '@nivo/line'; -import { BasicTooltip } from '@nivo/tooltip'; +import { useTheme } from '@mui/material'; +import { ResponsiveLine } from '@nivo/line'; import React, { FC } from 'react'; import { useColor } from '@/lib/hooks/useColor'; @@ -50,30 +49,11 @@ export const Component: FC = ({ reverse: false, }; } - type PointTooltipPropsWithLabel = PointTooltipProps & { - point: { - data: { - label: string; - }; - }; - }; return ( ( - - {(point as PointTooltipPropsWithLabel).point.data.label} - - } - color={point.point.color} - enableChip={true} - /> - )} - pointSize={10} colors={[selectedColor]} margin={{ top: 20, right: 10, bottom: 20, left: 35.5 }} xScale={{ type: 'linear', min: minX }} @@ -112,6 +92,7 @@ export const Component: FC = ({ }} enableGridX={false} enableGridY={false} + pointSize={0} useMesh={true} layers={[ CustomAxes, @@ -134,9 +115,6 @@ export const Component: FC = ({ ), 'axes', 'lines', - 'points', - 'markers', - 'mesh', ]} theme={lineTheme} /> diff --git a/src/ui/lib/components/MetricsSummary/MetricsSummary.component.tsx b/src/ui/lib/components/MetricsSummary/MetricsSummary.component.tsx index 9530d9e7..0d804f5c 100644 --- a/src/ui/lib/components/MetricsSummary/MetricsSummary.component.tsx +++ b/src/ui/lib/components/MetricsSummary/MetricsSummary.component.tsx @@ -102,7 +102,7 @@ export const Component = () => { return ( <> - + diff --git a/src/ui/lib/store/benchmarksWindowData.ts b/src/ui/lib/store/benchmarksWindowData.ts index b4af5063..a589e8ed 100644 --- a/src/ui/lib/store/benchmarksWindowData.ts +++ b/src/ui/lib/store/benchmarksWindowData.ts @@ -1,6 +1,5 @@ export const benchmarksScript = `window.benchmarks = [ { - strategyDisplayStr: "synchronous", requestsPerSecond: 11.411616848282272, tpot: { mean: 8.758024845683707, @@ -172,7 +171,6 @@ export const benchmarksScript = `window.benchmarks = [ }, }, { - strategyDisplayStr: "constant@36.28", requestsPerSecond: 36.289181300710815, tpot: { mean: 588.0161376137819, @@ -344,7 +342,6 @@ export const benchmarksScript = `window.benchmarks = [ }, }, { - strategyDisplayStr: "constant@20.75", requestsPerSecond: 20.752070927855794, tpot: { mean: 116.28360712595156, @@ -516,7 +513,6 @@ export const benchmarksScript = `window.benchmarks = [ }, }, { - strategyDisplayStr: "constant@26.81", requestsPerSecond: 26.81917480361788, tpot: { mean: 299.7306064613554, @@ -688,7 +684,6 @@ export const benchmarksScript = `window.benchmarks = [ }, }, { - strategyDisplayStr: "constant@26.82", requestsPerSecond: 26.823988819498975, tpot: { mean: 683.8011571339198, @@ -860,7 +855,6 @@ export const benchmarksScript = `window.benchmarks = [ }, }, { - strategyDisplayStr: "constant@24.50", requestsPerSecond: 24.50047903792646, tpot: { mean: 742.9258901891964, @@ -1032,7 +1026,6 @@ export const benchmarksScript = `window.benchmarks = [ }, }, { - strategyDisplayStr: "constant@25.61", requestsPerSecond: 25.617829792196602, tpot: { mean: 663.3098317044122, @@ -1204,7 +1197,6 @@ export const benchmarksScript = `window.benchmarks = [ }, }, { - strategyDisplayStr: "constant@37.02", requestsPerSecond: 37.02892550982192, tpot: { mean: 606.4144710877113, @@ -1376,7 +1368,6 @@ export const benchmarksScript = `window.benchmarks = [ }, }, { - strategyDisplayStr: "constant@37.29", requestsPerSecond: 37.29183354201869, tpot: { mean: 603.3237551205925, @@ -1548,7 +1539,6 @@ export const benchmarksScript = `window.benchmarks = [ }, }, { - strategyDisplayStr: "throughput", requestsPerSecond: 37.45318312972309, tpot: { mean: 600.7204526769262, diff --git a/src/ui/lib/store/slices/benchmarks/benchmarks.interfaces.ts b/src/ui/lib/store/slices/benchmarks/benchmarks.interfaces.ts index 6c01d5e2..602ae17e 100644 --- a/src/ui/lib/store/slices/benchmarks/benchmarks.interfaces.ts +++ b/src/ui/lib/store/slices/benchmarks/benchmarks.interfaces.ts @@ -27,7 +27,6 @@ export interface BenchmarkMetrics { export interface Benchmark extends BenchmarkMetrics { requestsPerSecond: number; - strategyDisplayStr: string; } export type Benchmarks = Benchmark[]; diff --git a/src/ui/lib/store/slices/benchmarks/benchmarks.selectors.ts b/src/ui/lib/store/slices/benchmarks/benchmarks.selectors.ts index d3da9bf9..53d54f40 100644 --- a/src/ui/lib/store/slices/benchmarks/benchmarks.selectors.ts +++ b/src/ui/lib/store/slices/benchmarks/benchmarks.selectors.ts @@ -11,18 +11,6 @@ import { selectSloState } from '../slo/slo.selectors'; export const selectBenchmarks = (state: RootState) => state.benchmarks.data; -const getUnitsByMetric = (metric: string) => { - switch (metric) { - case 'ttft': - case 'tpot': - return 'ms'; - case 'timePerRequest': - return 'sec'; - case 'throughput': - return 'tok/s'; - } -}; - export const selectMetricsSummaryLineData = createSelector( [selectBenchmarks, selectSloState], (benchmarks, sloState) => { @@ -30,10 +18,8 @@ export const selectMetricsSummaryLineData = createSelector( ?.slice() ?.sort((bm1, bm2) => (bm1.requestsPerSecond > bm2.requestsPerSecond ? 1 : -1)); const selectedPercentile = sloState.enforcedPercentile; - interface PointWithLabel extends Point { - label: string; - } - const lineData: { [K in keyof BenchmarkMetrics]: PointWithLabel[] } = { + + const lineData: { [K in keyof BenchmarkMetrics]: Point[] } = { ttft: [], tpot: [], timePerRequest: [], @@ -46,17 +32,14 @@ export const selectMetricsSummaryLineData = createSelector( 'throughput', ]; metrics.forEach((metric) => { - const data: PointWithLabel[] = []; + const data: Point[] = []; sortedByRPS?.forEach((benchmark) => { const percentile = benchmark[metric].percentileRows.find( (p) => p.percentile === selectedPercentile ); - const yValue = percentile?.value ?? 0; - const units = getUnitsByMetric(metric); data.push({ x: benchmark.requestsPerSecond, - y: yValue, - label: `${benchmark.strategyDisplayStr} ${formatNumber(yValue)} ${units}`, + y: percentile?.value ?? 0, }); }); diff --git a/tests/unit/presentation/test_data_models.py b/tests/unit/presentation/test_data_models.py index e879406d..c1663c43 100644 --- a/tests/unit/presentation/test_data_models.py +++ b/tests/unit/presentation/test_data_models.py @@ -1,7 +1,6 @@ import pytest -from guidellm.presentation.data_models import BenchmarkDatum, Bucket -from tests.unit.mock_benchmark import mock_generative_benchmark +from guidellm.presentation.data_models import Bucket @pytest.mark.smoke @@ -19,10 +18,3 @@ def test_bucket_from_data(): assert buckets[1].value == 8.0 assert buckets[1].count == 5 assert bucket_width == 1 - - -@pytest.mark.smoke -def test_from_benchmark_includes_strategy_display_str(): - mock_bm = mock_generative_benchmark() - bm = BenchmarkDatum.from_benchmark(mock_bm) - assert bm.strategy_display_str == "synchronous" From e95007d7f3bc11619c4d7e70dacc2f7434fd93dd Mon Sep 17 00:00:00 2001 From: Mark Kurtz Date: Wed, 15 Oct 2025 12:51:12 -0400 Subject: [PATCH 33/57] Fixes for constant rate benchmarking race condition, simplfications, and minor bug fixes for stats accumulation and data loading --- src/guidellm/benchmark/benchmarker.py | 4 +- src/guidellm/benchmark/schemas.py | 236 +++++--- src/guidellm/data/loaders.py | 2 +- src/guidellm/scheduler/__init__.py | 17 +- src/guidellm/scheduler/environments.py | 72 +-- src/guidellm/scheduler/scheduler.py | 49 +- src/guidellm/scheduler/schemas.py | 106 ++-- src/guidellm/scheduler/strategies.py | 717 +++++++++---------------- src/guidellm/scheduler/worker.py | 147 +++-- src/guidellm/scheduler/worker_group.py | 81 ++- 10 files changed, 672 insertions(+), 759 deletions(-) diff --git a/src/guidellm/benchmark/benchmarker.py b/src/guidellm/benchmark/benchmarker.py index ed9d789b..6a5a5627 100644 --- a/src/guidellm/benchmark/benchmarker.py +++ b/src/guidellm/benchmark/benchmarker.py @@ -109,16 +109,18 @@ async def run( ) estimated_state = EstimatedBenchmarkState() scheduler_state = None + scheduler: Scheduler[RequestT, ResponseT] = Scheduler() async for ( response, request, request_info, scheduler_state, - ) in Scheduler[RequestT, ResponseT]().run( + ) in scheduler.run( requests=requests, backend=backend, strategy=strategy, + startup_duration=warmup if warmup and warmup >= 1 else 0.0, env=environment, **constraints or {}, ): diff --git a/src/guidellm/benchmark/schemas.py b/src/guidellm/benchmark/schemas.py index 62ae5b0e..2f2d8f98 100644 --- a/src/guidellm/benchmark/schemas.py +++ b/src/guidellm/benchmark/schemas.py @@ -56,7 +56,6 @@ StatusBreakdown, StatusDistributionSummary, ) -from guidellm.utils.pydantic_utils import StandardBaseDict __all__ = [ "Benchmark", @@ -127,7 +126,7 @@ def add_avg_metric( self[total_key] = self.get(total_key, 0) + value self[count_key] = self.get(count_key, 0) + count - average = self[total_key] / self[count_key] + average = self[total_key] / self[count_key] if self[count_key] > 0 else 0.0 self.set_metric( group=group, key=key, @@ -193,16 +192,19 @@ def add_time_averaged_metric( time_avg_numerator_key = f"{group}_{key}_time_avg_numerator" time_avg_denominator_key = f"{group}_{key}_time_avg_denominator" last_recorded_time_key = f"{group}_{key}_last_recorded_time" + last_recorded_value_key = f"{group}_{key}_last_recorded_value" if last_recorded_time_key not in self: self[last_recorded_time_key] = recorded_time + self[last_recorded_value_key] = value self[time_avg_numerator_key] = value self[time_avg_denominator_key] = 0.0 else: time_delta = recorded_time - self[last_recorded_time_key] - self[time_avg_numerator_key] += value * time_delta + self[time_avg_numerator_key] += self[last_recorded_value_key] * time_delta self[time_avg_denominator_key] += time_delta self[last_recorded_time_key] = recorded_time + self[last_recorded_value_key] = value if self[time_avg_denominator_key] > 0: average = self[time_avg_numerator_key] / self[time_avg_denominator_key] @@ -776,6 +778,9 @@ class GenerativeMetrics(StandardBaseDict): request_latency: StatusDistributionSummary = Field( description="Distribution of request latencies for completed requests" ) + request_streaming_iterations_count: StatusDistributionSummary = Field( + description="Distribution of stream iterations for completed requests" + ) # General token stats prompt_token_count: StatusDistributionSummary = Field( @@ -796,9 +801,15 @@ class GenerativeMetrics(StandardBaseDict): inter_token_latency_ms: StatusDistributionSummary = Field( description="Distribution of inter-token latencies in milliseconds" ) + output_tokens_wo_first_per_iteration: StatusDistributionSummary = Field( + description="Distribution of output tokens (without first) generated per streaming iteration" + ) output_tokens_per_second: StatusDistributionSummary = Field( description="Distribution of output token generation rates" ) + output_tokens_per_iteration: StatusDistributionSummary = Field( + description="Distribution of output tokens generated per streaming iteration" + ) tokens_per_second: StatusDistributionSummary = Field( description="Distribution of total token throughput including prompt and output" ) @@ -818,13 +829,42 @@ def update_estimate( request_info: RequestInfo, scheduler_state: SchedulerState, ): - # Always track concurrency - state.add_time_averaged_metric( - group=EstimatedBenchmarkState.benchmark_metrics_group, - key="concurrency_requests", - value=scheduler_state.processing_requests, + benchmark_start_time = scheduler_state.start_time + request_start_time = ( + request_info.timings.request_start or request_info.timings.resolve_start + ) + request_end_time = ( + request_info.timings.request_end or request_info.timings.resolve_end + ) + event_occurence_time = ( + request_info.timings.queued + if request_info.status == "queued" + else ( + request_info.timings.dequeued + if request_info.status == "pending" + else request_start_time + if request_info.status == "in_progress" + else request_end_time + ) + ) + benchmark_duration = ( + event_occurence_time - benchmark_start_time + if event_occurence_time + else None + ) + request_duration = ( + request_end_time - request_start_time if request_end_time else None ) + # Always track concurrency + if event_occurence_time is not None: + state.add_time_averaged_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="concurrency_requests", + value=scheduler_state.processing_requests, + recorded_time=event_occurence_time, + ) + if request_info.status not in {"completed", "errored", "cancelled"}: return @@ -833,9 +873,6 @@ def update_estimate( key="updated", value=True, ) - start_time = scheduler_state.start_time - end_time = request_info.timings.request_end or request_info.timings.resolve_end - duration = end_time - start_time if end_time else None for prefix in (request_info.status, "total"): requests_count = ( @@ -847,8 +884,18 @@ def update_estimate( if prefix == "cancelled" else scheduler_state.processed_requests ) + input_tokens = ( + (response.input_metrics.total_tokens if response else None) + or request.input_metrics.total_tokens + or 0 + ) + output_tokens = ( + (response.output_metrics.total_tokens if response else None) + or request.output_metrics.total_tokens + or 0 + ) - # Request stats + # Request distribution stats state.set_metric( group=EstimatedBenchmarkState.benchmark_metrics_group, key=f"{prefix}_requests", @@ -857,96 +904,120 @@ def update_estimate( state.set_metric( group=EstimatedBenchmarkState.benchmark_metrics_group, key=f"{prefix}_requests_per_second", - value=requests_count / duration if duration else None, - ) - state.add_avg_metric( - group=EstimatedBenchmarkState.benchmark_metrics_group, - key=f"{prefix}_request_latency", value=( - request_info.timings.request_end or request_info.timings.resolve_end + requests_count / benchmark_duration if benchmark_duration else None ), - start_val=( - request_info.timings.request_start - or request_info.timings.resolve_start - ), - ) - - # Input/output token stats - state.add_avg_rate_metric( - group=EstimatedBenchmarkState.benchmark_metrics_group, - key="input_tokens", - value=(response.input_metrics.total_tokens if response else None) - or request.input_metrics.total_tokens, - ) - state.add_avg_rate_metric( - group=EstimatedBenchmarkState.benchmark_metrics_group, - key="input_text_tokens", - value=(response.input_metrics.text_tokens if response else None) - or request.input_metrics.text_tokens, ) - state.add_avg_rate_metric( + state.add_avg_metric( group=EstimatedBenchmarkState.benchmark_metrics_group, - key="input_images", - value=(response.input_metrics.image_count if response else None) - or request.input_metrics.image_count, + key=f"{prefix}_request_latency", + value=request_duration, ) - state.add_avg_rate_metric( + state.add_avg_metric( group=EstimatedBenchmarkState.benchmark_metrics_group, - key="input_video_frames", - value=(response.input_metrics.video_frames if response else None) - or request.input_metrics.video_frames, + key=f"{prefix}_request_streaming_iterations", + value=request_info.timings.iterations or 0, ) - state.add_avg_rate_metric( + + # Token iteration stats + state.add_avg_metric( group=EstimatedBenchmarkState.benchmark_metrics_group, - key="input_audio_seconds", - value=request.input_metrics.audio_seconds if request else None, + key="output_tokens_iterations", + value=output_tokens, + count=request_info.timings.iterations or 1, ) - state.add_avg_rate_metric( + state.add_avg_metric( group=EstimatedBenchmarkState.benchmark_metrics_group, - key="output_tokens", - value=(response.output_metrics.total_tokens if response else None) - or request.output_metrics.total_tokens, - ) - output_tokens = ( - response.output_metrics.total_tokens if response else None - ) or request.output_metrics.total_tokens - state.add_avg_rate_metric( - group=EstimatedBenchmarkState.benchmark_metrics_group, - key="total_tokens", - value=output_tokens, + key="output_tokens_wo_first_iterations", + value=output_tokens - 1 if output_tokens > 1 else 0, + count=request_info.timings.iterations or 1, ) - # General stats + # Token metrics stats state.add_avg_metric( group=EstimatedBenchmarkState.benchmark_metrics_group, key=f"{prefix}_time_to_first_token", value=request_info.timings.first_iteration, - start_val=request_info.timings.request_start - or request_info.timings.resolve_start, + start_val=request_start_time, ) state.add_avg_metric( group=EstimatedBenchmarkState.benchmark_metrics_group, key=f"{prefix}_inter_token_latency", value=request_info.timings.last_iteration, start_val=request_info.timings.first_iteration, - count=output_tokens - 1 - if output_tokens and output_tokens > 1 - else None, + count=(output_tokens or 1) - 1, ) state.add_avg_metric( group=EstimatedBenchmarkState.benchmark_metrics_group, key=f"{prefix}_time_per_output_token", - value=( - request_info.timings.request_end or request_info.timings.resolve_end - ), - start_val=( - request_info.timings.first_iteration - or request_info.timings.request_start - or request_info.timings.resolve_start - ), - count=output_tokens, + value=request_duration, + count=output_tokens or 0, ) + # Input/output throughput stats + if event_occurence_time is not None: + state.add_avg_rate_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="input_tokens", + value=input_tokens, + start_time=benchmark_start_time, + end_time=event_occurence_time, + ) + state.add_avg_rate_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="output_tokens", + value=output_tokens, + start_time=benchmark_start_time, + end_time=event_occurence_time, + ) + state.add_avg_rate_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="total_tokens", + value=input_tokens + output_tokens, + start_time=benchmark_start_time, + end_time=event_occurence_time, + ) + state.add_avg_rate_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="input_text_tokens", + value=( + (response.input_metrics.text_tokens if response else None) + or request.input_metrics.text_tokens + or 0 + ), + start_time=benchmark_start_time, + end_time=event_occurence_time, + ) + state.add_avg_rate_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="input_images", + value=( + (response.input_metrics.image_count if response else None) + or request.input_metrics.image_count + or 0 + ), + start_time=benchmark_start_time, + end_time=event_occurence_time, + ) + state.add_avg_rate_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="input_video_frames", + value=( + (response.input_metrics.video_frames if response else None) + or request.input_metrics.video_frames + or 0 + ), + start_time=benchmark_start_time, + end_time=event_occurence_time, + ) + state.add_avg_rate_metric( + group=EstimatedBenchmarkState.benchmark_metrics_group, + key="input_audio_seconds", + value=request.input_metrics.audio_seconds or 0, + start_time=benchmark_start_time, + end_time=event_occurence_time, + ) + @classmethod def compile( cls, @@ -987,6 +1058,10 @@ def compile( value_types=request_types, values=[req.request_latency or 0.0 for req in requests], ), + request_streaming_iterations_count=StatusDistributionSummary.from_values( + value_types=request_types, + values=[float(req.info.timings.iterations or 0) for req in requests], + ), # General token stats prompt_token_count=StatusDistributionSummary.from_values( value_types=request_types, @@ -1012,10 +1087,23 @@ def compile( value_types=request_types, values=[req.inter_token_latency_ms or 0.0 for req in requests], ), + output_tokens_wo_first_per_iteration=StatusDistributionSummary.from_values( + value_types=request_types, + values=[ + max(0.0, (req.output_metrics.total_tokens or 1.0) - 1.0) + for req in requests + ], + weights=[req.info.timings.iterations or 1 for req in requests], + ), output_tokens_per_second=StatusDistributionSummary.from_values( value_types=request_types, values=[req.output_tokens_per_second or 0.0 for req in requests], ), + output_tokens_per_iteration=StatusDistributionSummary.from_values( + value_types=request_types, + values=[req.output_tokens_per_iteration or 0.0 for req in requests], + weights=[req.info.timings.iterations or 1 for req in requests], + ), tokens_per_second=StatusDistributionSummary.from_values( value_types=request_types, values=[req.tokens_per_second or 0.0 for req in requests], diff --git a/src/guidellm/data/loaders.py b/src/guidellm/data/loaders.py index fcdea15d..fd46334d 100644 --- a/src/guidellm/data/loaders.py +++ b/src/guidellm/data/loaders.py @@ -65,7 +65,7 @@ def __iter__(self): worker_modulus = worker_info.num_workers if worker_info is not None else 1 worker_index = worker_info.id if worker_info is not None else 0 - if self.precache is not None: + if self.precache: for index, item in enumerate(self.precache): if (index + worker_index) % worker_modulus == 0: yield item diff --git a/src/guidellm/scheduler/__init__.py b/src/guidellm/scheduler/__init__.py index 2f5eb53f..9b74c44a 100644 --- a/src/guidellm/scheduler/__init__.py +++ b/src/guidellm/scheduler/__init__.py @@ -1,3 +1,15 @@ +""" +Scheduler subsystem for orchestrating benchmark workloads and managing worker processes. + +This module provides the core scheduling infrastructure for guidellm, including +strategies for controlling request timing patterns (synchronous, asynchronous, +constant rate, Poisson), constraints for limiting benchmark execution (duration, +error rates, request counts), and distributed execution through worker processes. +The scheduler coordinates between backend interfaces, manages benchmark state +transitions, and handles multi-turn request sequences with customizable timing +strategies and resource constraints. +""" + from .constraints import ( Constraint, ConstraintInitializer, @@ -28,11 +40,6 @@ AsyncConstantStrategy, AsyncPoissonStrategy, ConcurrentStrategy, - ConstantRateRequestTimings, - LastCompletionRequestTimings, - NoDelayRequestTimings, - PoissonRateRequestTimings, - ScheduledRequestTimings, SchedulingStrategy, StrategyT, StrategyType, diff --git a/src/guidellm/scheduler/environments.py b/src/guidellm/scheduler/environments.py index 69997e57..4f02d772 100644 --- a/src/guidellm/scheduler/environments.py +++ b/src/guidellm/scheduler/environments.py @@ -1,18 +1,19 @@ """ Environment abstractions for coordinating scheduler execution across distributed nodes. -Provides environment abstractions that handle synchronization, timing coordination, -error propagation, and lifecycle management for scheduler execution across single -or multiple nodes. The Environment protocol defines the interface for distributed +Provides abstractions that handle synchronization, timing coordination, error +propagation, and lifecycle management for scheduler execution across single or +multiple nodes. The Environment protocol defines the interface for distributed coordination while NonDistributedEnvironment provides a minimal implementation -for single-node execution. +for single-node execution. Environments manage the complete execution lifecycle +from parameter distribution through result aggregation. -Environment Execution Flow: -1. sync_run_params() - Distribute workload and synchronize parameters across nodes -2. sync_run_start() - Coordinate synchronized start time for all nodes -3. update_run_iteration() - Update state after each request (called per iteration) +Execution Flow: +1. sync_run_params() - Distribute workload and synchronize parameters +2. sync_run_start() - Coordinate synchronized start time +3. update_run_iteration() - Update state after each request iteration 4. sync_run_error() - Handle and propagate errors across nodes -5. sync_run_end() - Aggregate results and cleanup at completion +5. sync_run_end() - Aggregate results and finalize execution """ from __future__ import annotations @@ -39,12 +40,12 @@ class Environment(ABC, Generic[RequestT, ResponseT], InfoMixin): """ - Abstract base for coordinating scheduler execution across distributed nodes. + Abstract interface for coordinating scheduler execution across distributed nodes. - Defines the interface for managing distributed scheduler execution including + Defines the protocol for managing distributed scheduler execution including parameter synchronization, timing coordination, state updates, error propagation, - and result aggregation. Implementations handle the complexity of distributed - coordination while providing a unified interface for scheduler orchestration. + and result aggregation. Implementations handle distributed coordination complexity + while providing a unified interface for scheduler orchestration. """ @abstractmethod @@ -61,10 +62,6 @@ async def sync_run_params( """ Synchronize execution parameters across nodes and resolve local scope. - Coordinates parameter distribution and validation across active nodes. - In distributed environments, handles node assignment and workload partitioning. - In non-distributed environments, typically returns parameters unchanged. - :param requests: Complete set of requests to process across all nodes :param strategy: Scheduling strategy to apply during execution :param constraints: Runtime constraints to enforce during execution @@ -78,9 +75,6 @@ async def sync_run_start(self) -> float: """ Coordinate synchronized start time across all nodes. - Ensures all nodes begin processing simultaneously for accurate benchmarking - and consistent timing measurements across distributed execution. - :return: Unix timestamp when all nodes should begin processing :raises Exception: If startup synchronization fails across nodes """ @@ -97,11 +91,6 @@ async def update_run_iteration( """ Update environment state with completed request iteration results. - Called after each request processing to update execution progress and - synchronize any required state across nodes in distributed environments. - Generally, distributed is expected to store the iteration updates until - all nodes have processed and sync_run_end is called to retrieve them. - :param response: Response generated for the request, if successful :param request: The processed request :param request_info: Metadata about request processing including timings @@ -115,9 +104,6 @@ async def sync_run_error(self, err: list[Exception] | Exception): """ Handle and propagate errors across all active nodes. - Coordinates error handling when failures occur, ensuring all nodes are - notified for appropriate cleanup or shutdown procedures. - :param err: The exception(s) that occurred during execution """ ... @@ -136,10 +122,6 @@ async def sync_run_end( """ Finalize execution and aggregate results from all nodes. - Handles cleanup, result synchronization, and error propagation at execution - completion. Collects and yields results from worker nodes in distributed - environments. - :return: Iterator of (response, request, request_info, state) tuples from remote nodes in distributed environments, empty for non-distributed :raises Exception: Any errors that occurred during execution @@ -151,9 +133,9 @@ class NonDistributedEnvironment(Environment[RequestT, ResponseT]): """ Single-node scheduler execution environment with minimal coordination overhead. - Simplified environment for running schedulers on a single node without distributed - coordination requirements. Implements the Environment interface with no-op - synchronization for local testing, development, and single-machine benchmarking. + Implements the Environment interface with no-op synchronization for local testing, + development, and single-machine benchmarking. All synchronization methods return + immediately without distributed coordination logic. Example: :: @@ -165,29 +147,27 @@ class NonDistributedEnvironment(Environment[RequestT, ResponseT]): SynchronousStrategy, ) - - # Definitions + env = NonDistributedEnvironment() requests = [f"req_{ind}" for ind in range(5)] strategy = SynchronousStrategy() constraints = {"max_num": MaxNumberConstraint(max_num=5)} state = SchedulerState() - # Run environment local_req, local_strat, local_const = await env.sync_run_params( requests, strategy, constraints ) start_time = await env.sync_run_start() for req in local_req: state.processed_requests += 1 - await env.update_run_iteration( - f"resp_{req}", req, RequestInfo(), state - ) + await env.update_run_iteration(f"resp_{req}", req, RequestInfo(), state) async for nonlocal_req in env.sync_run_end(): state.processed_requests += 1 """ def __init__(self): - """Initialize with empty error storage for single-node execution.""" + """ + Initialize single-node environment with empty error storage. + """ self.run_errors: list[Exception] = [] async def sync_run_params( @@ -206,7 +186,7 @@ async def sync_run_params( :param requests: Requests to process locally :param strategy: Scheduling strategy to apply during execution :param constraints: Runtime constraints to enforce during execution - :return: Tuple containing the original (requests, strategy, constraints) + :return: Original (requests, strategy, constraints) tuple unchanged """ return requests, strategy, constraints @@ -214,7 +194,7 @@ async def sync_run_start(self) -> float: """ Return current time plus configured delay for single-node startup. - :return: Unix timestamp for when the run should start + :return: Unix timestamp when execution should begin """ return time.time() + settings.scheduler_start_delay_non_distributed @@ -229,7 +209,7 @@ async def update_run_iteration( No-op for single-node execution with no distributed state synchronization. :param response: Response generated for the request, if successful - :param request: The request that was processed + :param request: The processed request :param request_info: Metadata about request processing including timings :param state: Current scheduler state with metrics and progress """ @@ -256,7 +236,7 @@ async def sync_run_end( """ Finalize single-node execution and propagate any stored errors. - :return: Empty iterator since there are no remote nodes + :return: Empty iterator as there are no remote nodes :raises Exception: Any error stored during execution via sync_run_error """ if self.run_errors: diff --git a/src/guidellm/scheduler/scheduler.py b/src/guidellm/scheduler/scheduler.py index 0e19350b..ca5935fa 100644 --- a/src/guidellm/scheduler/scheduler.py +++ b/src/guidellm/scheduler/scheduler.py @@ -1,11 +1,10 @@ """ -Thread-safe singleton scheduler for distributed load generation workload coordination. +Thread-safe singleton scheduler for distributed benchmarking workload coordination. -Provides the core orchestration engine that coordinates request processing across -worker processes and distributed environments. Manages timing synchronization, -resource allocation, constraint enforcement, and result aggregation for -load generation operations. Integrates with backends, environments, and strategies -to enable scalable load testing across various scenarios including LLM inference. +Orchestrates request processing across worker processes with distributed timing +coordination, constraint enforcement, and result aggregation. Integrates with +backends, environments, and strategies to enable scalable load testing across +various scenarios including LLM inference benchmarking. """ from __future__ import annotations @@ -38,16 +37,14 @@ class Scheduler( Thread-safe singleton scheduler for distributed benchmarking workload coordination. Orchestrates request processing across worker processes with distributed timing - coordination, constraint enforcement, and result aggregation. Provides a unified - interface for executing benchmarking operations while abstracting the complexity - of multi-process coordination, environment synchronization, and resource management. - Implements singleton pattern to ensure consistent execution state across concurrent - benchmark operations. + coordination, constraint enforcement, and result aggregation. Abstracts the + complexity of multi-process coordination, environment synchronization, and + resource management while providing a unified interface for executing benchmarking + operations. Implements singleton pattern to ensure consistent execution state. Example: :: from guidellm.scheduler import Scheduler - from guidellm.backends import OpenAIBackend from guidellm.scheduler import NonDistributedEnvironment, SynchronousStrategy scheduler = Scheduler() @@ -58,7 +55,7 @@ class Scheduler( env=NonDistributedEnvironment(), max_requests=1000 ): - print(f"Processed: {request} with info: {info} and response: {response}") + print(f"Processed: {request}") """ async def run( @@ -66,6 +63,7 @@ async def run( requests: Iterable[RequestT | MultiTurnRequestT[RequestT]], backend: BackendInterface[RequestT, ResponseT], strategy: SchedulingStrategy, + startup_duration: float, env: Environment[RequestT, ResponseT] | None, **constraints: Any | dict[str, Any] | Constraint, ) -> AsyncIterator[ @@ -80,22 +78,23 @@ async def run( Execute distributed request processing with coordinated timing and constraints. Orchestrates the complete benchmarking workflow across worker processes with - environment synchronization, constraint enforcement, and error handling. - Manages resource lifecycle from initialization through cleanup while yielding - real-time processing updates for monitoring and aggregation. + environment synchronization, constraint enforcement, and error handling. Manages + resource lifecycle from initialization through cleanup while yielding real-time + processing updates for monitoring and aggregation. - :param requests: Request collection to process. Supports single requests or + :param requests: Request collection to process, supporting single requests or multi-turn sequences with optional inter-request delays :param backend: Backend interface for request processing and response generation :param strategy: Scheduling strategy controlling request timing and distribution + :param startup_duration: Duration in seconds for requests to ramp up :param env: Environment interface for distributed coordination and - synchronization + synchronization. Defaults to NonDistributedEnvironment if None :param constraints: Runtime constraints for execution control (max_requests, - max_duration, max_error_rate, etc.). Values can be primitives, dictionaries, - or constraint instances - :yields: Requests udpates as (response, request, request_info, scheduler_state) - tuples. Each request will generate three ordered updates: - queued, in_progress, completed | errored | cancelled. + max_duration, max_error_rate, etc.) as primitives, dictionaries, or + constraint instances + :yields: Request updates as (response, request, request_info, scheduler_state) + tuples. Each request generates three ordered updates: queued, in_progress, + completed | errored | cancelled :raises Exception: Worker process errors, environment synchronization failures, or constraint evaluation errors are propagated after cleanup """ @@ -122,10 +121,10 @@ async def run( # Setup the worker group, sync start with the environment worker_group = WorkerProcessGroup[RequestT, ResponseT]( requests=local_requests, - cycle_requests=local_requests, backend=backend, strategy=local_strategy, - constraints=local_constraints, + startup_duration=startup_duration, + **local_constraints, ) await worker_group.create_processes() local_start_time = await env.sync_run_start() diff --git a/src/guidellm/scheduler/schemas.py b/src/guidellm/scheduler/schemas.py index d53b55a1..21567c67 100644 --- a/src/guidellm/scheduler/schemas.py +++ b/src/guidellm/scheduler/schemas.py @@ -11,22 +11,13 @@ import time from collections.abc import AsyncIterator -from typing import ( - Any, - Generic, - Literal, - Protocol, - TypeVar, -) +from typing import Any, Generic, Literal, Protocol, TypeVar from pydantic import Field from typing_extensions import TypeAliasType, TypedDict from guidellm.schemas import RequestInfo -from guidellm.utils import ( - RegistryMixin, - StandardBaseModel, -) +from guidellm.utils import RegistryMixin, StandardBaseModel from guidellm.utils.registry import RegistryObjT __all__ = [ @@ -42,46 +33,45 @@ ] RequestT = TypeVar("RequestT") -"""Generic request object type for scheduler processing.""" +"Generic request object type for scheduler processing" ResponseT = TypeVar("ResponseT") -"""Generic response object type returned by backend processing.""" +"Generic response object type returned by backend processing" MultiTurnRequestT = TypeAliasType( "MultiTurnRequestT", list[RequestT | tuple[RequestT, float]] | tuple[RequestT | tuple[RequestT, float]], type_params=(RequestT,), ) -"""Multi-turn request structure supporting conversation history with optional delays.""" +"Multi-turn request structure supporting conversation history with optional delays" class SchedulerMessagingPydanticRegistry(RegistryMixin[RegistryObjT]): """ - Registry for enabling a generic interface to define the pydantic class types used - for inter-process messaging within the scheduler. + Registry for Pydantic types used in scheduler inter-process messaging. + + Enables generic interface for defining Pydantic class types used for + communication between distributed scheduler components and worker processes. """ class BackendInterface(Protocol, Generic[RequestT, ResponseT]): """ - Abstract interface for request processing backends. + Protocol defining the interface for request processing backends. - Defines the contract for backend implementations that process requests within - the scheduler system. Backends handle initialization, validation, processing, - and shutdown lifecycle management. Must ensure all properties are pickleable - before process_startup is invoked for multi-process environments. + Establishes the contract for backend implementations that process requests + within the scheduler system. Backends manage initialization, validation, + processing, and shutdown lifecycle. All properties must be pickleable before + process_startup is called for multi-process environments. Example: :: - from guidellm.scheduler.objects import BackendInterface - class CustomBackend(BackendInterface): @property def processes_limit(self) -> int: return 4 async def resolve(self, request, request_info, history=None): - # Process request and yield responses yield response, updated_request_info """ @@ -107,21 +97,21 @@ async def process_startup(self) -> None: """ Perform backend initialization and startup procedures. - :raises: Implementation-specific exceptions for startup failures. + :raises Exception: Implementation-specific exceptions for startup failures """ async def validate(self) -> None: """ Validate backend configuration and operational status. - :raises: Implementation-specific exceptions for validation failures. + :raises Exception: Implementation-specific exceptions for validation failures """ async def process_shutdown(self) -> None: """ Perform backend cleanup and shutdown procedures. - :raises: Implementation-specific exceptions for shutdown failures. + :raises Exception: Implementation-specific exceptions for shutdown failures """ async def resolve( @@ -135,23 +125,23 @@ async def resolve( :param request: The request object to process :param request_info: Scheduling metadata and timing information - :param history: Optional conversation history for multi-turn requests + :param history: Conversation history for multi-turn requests :yield: Tuples of (response, updated_request_info) for each response chunk - :raises: Implementation-specific exceptions for processing failures + :raises Exception: Implementation-specific exceptions for processing failures """ BackendT = TypeVar("BackendT", bound=BackendInterface) -"""Generic backend interface type for request processing.""" +"Generic backend interface type for request processing" class SchedulerUpdateActionProgress(TypedDict, total=False): """ - Progress information for a scheduler update action. + Progress tracking data for scheduler operations. - Optional progress tracking data that provides estimates for remaining work - in scheduler operations. Used by constraints and monitoring systems to - track execution progress and make termination decisions. + Provides estimates for remaining work in scheduler operations, including + fraction complete, request counts, and duration. Used by constraints and + monitoring systems to track execution progress and make termination decisions. """ remaining_fraction: float | None @@ -161,17 +151,14 @@ class SchedulerUpdateActionProgress(TypedDict, total=False): class SchedulerUpdateAction(StandardBaseModel): """ - Scheduler behavior control directives and actions. + Control directives for scheduler behavior and operations. Encapsulates control signals for scheduler operations including request queuing and processing directives. Used by constraints to communicate - termination conditions and progress information to scheduler components. + termination conditions and progress to scheduler components. Example: :: - from guidellm.scheduler.objects import SchedulerUpdateAction - - # Signal to stop queuing but continue processing action = SchedulerUpdateAction( request_queuing="stop", request_processing="continue", @@ -198,25 +185,18 @@ class SchedulerUpdateAction(StandardBaseModel): class SchedulerState(StandardBaseModel): """ - Scheduler operation state tracking and statistics. + Comprehensive state tracking for scheduler execution. - Comprehensive state container for tracking scheduler execution progress, - request counts, timing information, and constraint enforcement. Central - to scheduler coordination and provides real-time metrics for monitoring - and decision-making across distributed worker processes. + Tracks scheduler execution progress, request counts, timing information, + and constraint enforcement. Central to scheduler coordination, providing + real-time metrics for monitoring and decision-making across distributed + worker processes. Example: :: - from guidellm.scheduler.objects import SchedulerState - - # Initialize scheduler state state = SchedulerState(node_id=0, num_processes=4) - - # Track request processing state.created_requests += 1 state.queued_requests += 1 - - # Monitor completion progress completion_rate = state.processed_requests / state.created_requests """ @@ -234,41 +214,35 @@ class SchedulerState(StandardBaseModel): default=None, description="Unix timestamp when the scheduler stopped" ) end_queuing_time: float | None = Field( - default=None, description="When request queuing stopped, if applicable" + default=None, description="Unix timestamp when request queuing stopped" ) end_queuing_constraints: dict[str, SchedulerUpdateAction] = Field( default_factory=dict, description="Constraints that triggered queuing termination", ) end_processing_time: float | None = Field( - default=None, description="When request processing stopped, if applicable" + default=None, description="Unix timestamp when request processing stopped" ) end_processing_constraints: dict[str, SchedulerUpdateAction] = Field( default_factory=dict, - description="Constraints that triggered process ing termination", + description="Constraints that triggered processing termination", ) scheduler_constraints: dict[str, SchedulerUpdateAction] = Field( default_factory=dict, - description=( - "The latest state from all constraints applied during the scheduler run" - ), + description="Latest state from all constraints applied during scheduler run", ) remaining_fraction: float | None = Field( default=None, - description=( - "Estimated fraction for the remaining progress of the run, if known" - ), + description="Estimated fraction of remaining progress, if known", ) remaining_requests: float | None = Field( default=None, - description="Estimated number of requests remaining to be processed, if known", + description="Estimated number of remaining requests to process, if known", ) remaining_duration: float | None = Field( default=None, - description=( - "Estimated time remaining in seconds for the scheduler run, if known" - ), + description="Estimated remaining time in seconds for scheduler run, if known", ) created_requests: int = Field( @@ -279,13 +253,13 @@ class SchedulerState(StandardBaseModel): ) pending_requests: int = Field( default=0, - description="Total number of requests pending processing within a worker", + description="Number of requests pending processing within a worker", ) processing_requests: int = Field( default=0, description="Number of requests currently being processed" ) processed_requests: int = Field( - default=0, description="Total number of requests that completed processing" + default=0, description="Number of requests that completed processing" ) successful_requests: int = Field( default=0, description="Number of requests that completed successfully" diff --git a/src/guidellm/scheduler/strategies.py b/src/guidellm/scheduler/strategies.py index d3e31d43..5e13a26d 100644 --- a/src/guidellm/scheduler/strategies.py +++ b/src/guidellm/scheduler/strategies.py @@ -1,34 +1,32 @@ """ -Request scheduling strategies for controlling how benchmark requests are processed. +Request scheduling strategies for controlling benchmark request processing patterns. -This module provides timing implementations and concrete strategies that control request +Provides timing implementations and concrete strategies that control request concurrency, timing patterns, and throughput characteristics to simulate real-world -usage scenarios. The scheduling system separates timing logic from strategy constraints, -enabling flexible combination of timing behaviors with process and concurrency limits. +usage scenarios. Strategies define how requests are distributed across worker processes, +when they should be scheduled, and what constraints apply to concurrent processing. +The scheduling system separates timing logic from strategy constraints, enabling +flexible combination of timing behaviors with process and concurrency limits. """ from __future__ import annotations -import math +import asyncio import random import time -from abc import ABC, abstractmethod +from abc import abstractmethod +from multiprocessing import Lock, Value from typing import Annotated, ClassVar, Literal, TypeVar from pydantic import Field, PrivateAttr from guidellm.schemas import RequestInfo -from guidellm.utils import InfoMixin, PydanticClassRegistryMixin, StandardBaseModel +from guidellm.utils import InfoMixin, PydanticClassRegistryMixin __all__ = [ "AsyncConstantStrategy", "AsyncPoissonStrategy", "ConcurrentStrategy", - "ConstantRateRequestTimings", - "LastCompletionRequestTimings", - "NoDelayRequestTimings", - "PoissonRateRequestTimings", - "ScheduledRequestTimings", "SchedulingStrategy", "StrategyT", "StrategyType", @@ -43,308 +41,162 @@ ] -def _exponential_decay_tau(max_progress: float, convergence: float = 0.99) -> float: - """ - Calculate tau value for exponential decay to reach target progress level. - - :param max_progress: The max progress value to reach - :param convergence: The target convergence level for reaching max_progress - :return: The calculated tau value for the given max_progress and convergence - """ - return max_progress / (-math.log(1 - convergence)) - - -def _exponential_decay_fraction(progress: float, tau: float = 1.0) -> float: - """ - Calculate completion fraction based on exponential decay curve. - - :param progress: The current progress value (>=0) - :param tau: The scale factor for the exponential decay - :return: The fraction of completion based on exponential decay (0 -> 1) +class SchedulingStrategy(PydanticClassRegistryMixin["SchedulingStrategy"], InfoMixin): """ - return 1 - math.exp(-progress / tau) + Base class for scheduling strategies controlling request processing patterns. + Defines the interface for strategies that combine timing implementations with + process and concurrency constraints to enable various benchmark scenarios. + Strategies manage request timing, worker process coordination, and concurrency + limits across distributed execution environments. -class ScheduledRequestTimings(StandardBaseModel, ABC): - """ - Abstract base class for controlling when requests are scheduled. - - Defines the interface for timing implementations that determine request scheduling - behavior. Different implementations provide various patterns like synchronous, - constant-rate, or stochastic scheduling to simulate real-world scenarios. + :cvar schema_discriminator: Field name used for polymorphic deserialization """ - @abstractmethod - def next_offset(self) -> float: - """ - Calculate the time offset for the next request to be scheduled. - - :return: The offset in seconds from scheduler start time for next request - """ - - @abstractmethod - def request_completed(self, request_info: RequestInfo): - """ - Handle request completion and update internal timing state. - - :param request_info: Information about the completed request including - timing details and completion status - """ - + schema_discriminator: ClassVar[str] = "type_" -class LastCompletionRequestTimings(ScheduledRequestTimings): - """ - Timing implementation for synchronous and concurrent scheduling strategies. + @classmethod + def __pydantic_schema_base_type__(cls) -> type[SchedulingStrategy]: + if cls.__name__ == "SchedulingStrategy": + return cls - Schedules the next request immediately after the last request completes, enabling - sequential or limited concurrent processing with completion-based timing control. - """ + return SchedulingStrategy - offset: float = Field( - default=0.0, - description="Current time offset in seconds from scheduler start time", + type_: Literal["strategy"] = Field( + description="The type of scheduling strategy to schedule requests with", ) - startup_requests: int = Field( + worker_coount: int = Field( default=0, - description="Number of initial requests to schedule with equal spacing", - ge=0, - ) - startup_requests_delay: float = Field( - default=0.0, - description="Delay in seconds between startup requests", + description="Number of worker processes to use for this strategy", ge=0, ) - _requests_count: int = PrivateAttr(0) - - def next_offset(self) -> float: - """ - Get the current offset value and apply startup delay if applicable. - - :return: The current offset value in seconds from scheduler start time - """ - self._requests_count += 1 - - if self._requests_count <= self.startup_requests: - self.offset += self.startup_requests_delay - - return self.offset - - def request_completed(self, request_info: RequestInfo): - """ - Update timing state based on the completed request. - - :param request_info: Information about the completed request - """ - if ( - self._requests_count > self.startup_requests - and request_info.completed_at is not None - ): - # set the next sync offset to the time when the previous request completed - self.offset = request_info.completed_at - request_info.scheduler_start_time - - -class NoDelayRequestTimings(ScheduledRequestTimings): - """ - Timing implementation for throughput-maximizing scheduling strategies. - - Schedules requests with minimal delay to achieve maximum throughput, with optional - startup ramping to gradually increase request processing during initialization. - """ - - offset: float = Field( - default=0.0, - description="Base time offset in seconds from scheduler start time", + max_concurrency: int = Field( + default=0, + description="Maximum number of concurrent requests to allow", ge=0, ) startup_duration: float = Field( default=0.0, - description="Duration in seconds for gradual startup ramp", + description="Duration in seconds for startup request distribution", ge=0, ) - startup_target_requests: int = Field( - default=1, - description="Target number of requests to converge to during startup", - gt=0, - ) - startup_convergence: float = Field( - default=0.99, - description="Target convergence rate during startup phase", - ) - _start_time: float | None = PrivateAttr(None) - _requests_count: int = PrivateAttr(0) - - def next_offset(self) -> float: - """ - Calculate offset with optional startup adjustment. - - :return: Static offset plus any startup adjustment - """ - if self._start_time is None: - self._start_time = time.time() - - self._requests_count += 1 - elapsed = time.time() - self._start_time - if self.startup_duration > 0 and elapsed < self.startup_duration: - startup_percent = _exponential_decay_fraction( - self._requests_count, - _exponential_decay_tau( - self.startup_target_requests, self.startup_convergence - ), - ) - else: - startup_percent = 1.0 - - return self.offset + startup_percent * self.startup_duration + _processes_lock = PrivateAttr(None) + _processes_request_index = PrivateAttr(None) + _processes_start_time = PrivateAttr(None) + _cached_processes_start_time: float | None = PrivateAttr(None) - def request_completed(self, request_info: RequestInfo): + @property + def processes_limit(self) -> int | None: """ - Handle request completion (no action needed for throughput strategy). + Get the maximum number of worker processes supported by this strategy. - :param request_info: Information about the completed request (unused) + :return: Maximum number of worker processes, None if unlimited """ + return None - -class ConstantRateRequestTimings(ScheduledRequestTimings): - """ - Timing implementation for constant-rate scheduling strategies. - - Schedules requests at a fixed rate with evenly spaced intervals to provide - predictable timing behavior for steady-state load simulation. - """ - - rate: float = Field( - description="Target rate in requests per second", - gt=0, - ) - offset: float = Field( - default=0.0, - description="Base time offset in seconds from scheduler start time", - ge=0, - ) - _requests_count: int = PrivateAttr(0) - - def next_offset(self) -> float: + @property + def requests_limit(self) -> int | None: """ - Calculate the offset for the next request at a constant rate. + Get the maximum number of concurrent requests supported by this strategy. - :return: The offset in seconds for the next request + :return: Maximum number of concurrent requests, None if unlimited """ - num_requests = self._requests_count - self._requests_count += 1 - interval = 1.0 / self.rate - - return self.offset + interval * num_requests + return None - def request_completed(self, request_info: RequestInfo): + def init_processes_timings( + self, + worker_count: int, + max_concurrency: int, + startup_duration: float, + ): """ - Handle request completion (no action needed for constant rate strategy). + Initialize shared timing state for multi-process coordination. - :param request_info: Information about the completed request (unused) + :param worker_count: Number of worker processes to coordinate + :param max_concurrency: Maximum number of concurrent requests allowed + :param startup_duration: Duration in seconds for request startup ramping """ + self.worker_coount = worker_count + self.max_concurrency = max_concurrency + self.startup_duration = startup_duration + self._processes_request_index = Value("i", 0) + self._processes_lock = Lock() + self._processes_start_time = Value("d", -1.0) -class PoissonRateRequestTimings(ScheduledRequestTimings): - """ - Timing implementation for Poisson-distributed scheduling strategies. - - Schedules requests following a Poisson process with exponentially distributed - inter-arrival times to simulate realistic traffic patterns with random variance. - """ - - rate: float = Field( - description="Target average rate in requests per second", - gt=0, - ) - random_seed: int = Field( - default=42, - description="Seed for random number generator for reproducible behavior", - ) - offset: float = Field( - default=0.0, - description="Base time offset in seconds from scheduler start time", - ) - _requests_count: int = PrivateAttr(0) - _random: random.Random | None = PrivateAttr(None) - - def next_offset(self) -> float: + def init_processes_start(self, start_time: float): """ - Calculate the offset for the next request using Poisson distribution. + Set the synchronized start time for all worker processes. - :return: The cumulative offset in seconds for the next request + :param start_time: Unix timestamp when request processing should begin + :raises RuntimeError: If called before init_processes_timings """ - self._requests_count += 1 - - if self._random is None: - self._random = random.Random(self.random_seed) - else: - next_delay = self._random.expovariate(self.rate) - self.offset += next_delay + if self._processes_lock is None: + raise RuntimeError( + "SchedulingStrategy init_processes_start called before " + "init_processes_timings" + ) - return self.offset + with self._processes_lock: + self._processes_start_time.value = start_time - def request_completed(self, request_info: RequestInfo): + async def get_processes_start_time(self) -> float: """ - Handle request completion (no action needed for Poisson rate strategy). + Get the synchronized start time, waiting if not yet set. - :param request_info: Information about the completed request (unused) + :return: Unix timestamp when request processing began + :raises RuntimeError: If called before init_processes_timings """ + if self._processes_lock is None: + raise RuntimeError( + "SchedulingStrategy get_processes_start_time called before " + "init_processes_timings" + ) + while self._cached_processes_start_time is None: + with self._processes_lock: + if self._processes_start_time.value != -1.0: + self._cached_processes_start_time = self._processes_start_time.value + else: + await asyncio.sleep(0.01) # wait for start time to be set by main -class SchedulingStrategy(PydanticClassRegistryMixin["SchedulingStrategy"], InfoMixin): - """ - Abstract base class for scheduling strategies controlling request processing. - - Defines the interface for strategies that combine timing implementations with - process and concurrency constraints to enable various benchmark scenarios. - """ - - schema_discriminator: ClassVar[str] = "type_" - - @classmethod - def __pydantic_schema_base_type__(cls) -> type[SchedulingStrategy]: - if cls.__name__ == "SchedulingStrategy": - return cls - - return SchedulingStrategy - - type_: Literal["strategy"] = Field( - description="The type of scheduling strategy to schedule requests with", - ) + return self._cached_processes_start_time - @property - def processes_limit(self) -> int | None: + def next_request_index(self) -> int: """ - Get the maximum number of worker processes supported by this strategy. + Get the next sequential request index across all worker processes. - :return: Maximum number of worker processes, None if unlimited + :return: Globally unique request index for timing calculations + :raises RuntimeError: If called before init_processes_timings """ - return None + if self._processes_lock is None: + raise RuntimeError( + "SchedulingStrategy next_request_index called before " + "init_processes_timings" + ) - @property - def requests_limit(self) -> int | None: + with self._processes_lock: + self._processes_request_index.value += 1 + return self._processes_request_index.value + + @abstractmethod + async def next_request_time(self, offset: int) -> float: """ - Get the maximum number of concurrent requests supported by this strategy. + Calculate the scheduled start time for the next request. - :return: Maximum number of concurrent requests, None if unlimited + :param offset: Worker process offset for distributing request timing + :return: Unix timestamp when the request should be processed """ - return None - def create_request_timings( - self, local_rank: int, local_world_size: int, local_max_concurrency: int | float - ) -> ScheduledRequestTimings: + @abstractmethod + def request_completed(self, request_info: RequestInfo): """ - Create a timing instance to define scheduling behavior for a worker process. + Handle request completion and update internal timing state. - :param local_rank: The rank of the worker process within local world size - :param local_world_size: Total number of worker processes in local world - :param local_max_concurrency: Maximum concurrent requests for the worker - :return: A ScheduledRequestTimings instance for the worker process - :raises NotImplementedError: Must be implemented by subclasses + :param request_info: Information about the completed request including + timing details and completion status """ - raise NotImplementedError( - "create_worker_timings method must be implemented by subclasses." - ) StrategyT = TypeVar("StrategyT", bound=SchedulingStrategy) @@ -353,19 +205,18 @@ def create_request_timings( @SchedulingStrategy.register("synchronous") class SynchronousStrategy(SchedulingStrategy): """ - Sequential request processing strategy with single-process constraint. + Sequential request processing with strict single-request-at-a-time execution. Processes requests one at a time in strict sequential order, providing predictable timing behavior ideal for measuring maximum sequential throughput and ensuring - request isolation. + complete request isolation. Each request completes before the next begins. """ type_: Literal["synchronous"] = "synchronous" # type: ignore[assignment] + _process_last_request_time: float | None = PrivateAttr(None) def __str__(self) -> str: """ - Return string representation of the strategy. - :return: String identifier for synchronous strategy """ return "synchronous" @@ -373,52 +224,49 @@ def __str__(self) -> str: @property def processes_limit(self) -> int | None: """ - Get maximum number of worker processes for synchronous scheduling. - - :return: Always returns 1 to enforce single-process constraint + :return: Always 1 to enforce single-process constraint """ return 1 @property def requests_limit(self) -> int | None: """ - Get maximum number of concurrent requests for synchronous scheduling. - - :return: Always returns 1 to enforce single-request constraint + :return: Always 1 to enforce single-request constraint """ return 1 - def create_request_timings( - self, - local_rank: int, - local_world_size: int, - local_max_concurrency: int, # noqa: ARG002 - ) -> ScheduledRequestTimings: - """ - Create timing implementation for synchronous request scheduling. - - :param local_rank: The rank of the worker process (must be 0) - :param local_world_size: Total number of worker processes (must be 1) - :param local_max_concurrency: Maximum concurrent requests (unused) - :return: LastCompletionRequestTimings instance for sequential processing - :raises ValueError: If multiple workers or non-zero rank specified - """ - if local_world_size > 1 or local_rank != 0: - raise ValueError( - "SynchronousStrategy can only be used with a single worker process." - ) + async def next_request_time(self, offset: int) -> float: + """ + Calculate next request time based on previous completion. + + :param offset: Unused for synchronous strategy + :return: Time of last completion or start time if first request + """ + _ = offset # offset unused for synchronous strategy + + if self._process_last_request_time is not None: + return self._process_last_request_time + + return await self.get_processes_start_time() + + def request_completed(self, request_info: RequestInfo): + """ + Update timing state with completed request information. - return LastCompletionRequestTimings() + :param request_info: Completed request metadata including timing + """ + if request_info.completed_at is not None: + self._process_last_request_time = request_info.completed_at @SchedulingStrategy.register("concurrent") class ConcurrentStrategy(SchedulingStrategy): """ - Parallel request processing strategy with controlled concurrency limits. + Parallel request processing with fixed concurrency limits. Enables concurrent request processing up to a specified number of streams, - providing balanced throughput while maintaining predictable resource usage - and completion-based timing coordination. + providing balanced throughput while maintaining predictable resource usage. + Requests are distributed across streams with completion-based timing coordination. """ type_: Literal["concurrent"] = "concurrent" # type: ignore[assignment] @@ -426,16 +274,11 @@ class ConcurrentStrategy(SchedulingStrategy): description="Number of concurrent streams for scheduling requests", gt=0, ) - startup_duration: float = Field( - default=0.0, - description="Duration in seconds for distributing startup requests", - ge=0, - ) + + _process_last_request_time: float | None = PrivateAttr(None) def __str__(self) -> str: """ - Return string representation of the strategy. - :return: String identifier with stream count """ return f"concurrent@{self.streams}" @@ -443,8 +286,6 @@ def __str__(self) -> str: @property def processes_limit(self) -> int: """ - Get maximum number of worker processes for concurrent scheduling. - :return: Number of streams as maximum worker processes """ return self.streams @@ -452,72 +293,42 @@ def processes_limit(self) -> int: @property def requests_limit(self) -> int: """ - Get maximum number of concurrent requests for concurrent scheduling. - :return: Number of streams as maximum concurrent requests """ return self.streams - def create_request_timings( - self, - local_rank: int, - local_world_size: int, - local_max_concurrency: int, # noqa: ARG002 - ) -> LastCompletionRequestTimings: - """ - Create timing implementation for concurrent request scheduling. - - :param local_rank: The rank of the worker process (must be < streams) - :param local_world_size: Total worker processes (must not exceed streams) - :param local_max_concurrency: Maximum concurrent requests (unused) - :return: LastCompletionRequestTimings instance for stream-based processing - :raises ValueError: If worker configuration exceeds stream limits - """ - if local_world_size > self.streams: - raise ValueError( - "ConcurrentStrategy can only be used with up to " - f"{self.streams} worker processes." - ) + async def next_request_time(self, offset: int) -> float: + """ + Calculate next request time with stream-based distribution. - if local_rank >= self.streams: - raise ValueError( - f"Local rank {local_rank} exceeds the number of streams {self.streams}." - ) + :param offset: Worker process offset for distributing initial requests + :return: Time of last completion or staggered start time if first request + """ + if self._process_last_request_time is not None: + return self._process_last_request_time - if self.startup_duration > 0: - # Ensure equal global distribution of the start up for concurrent streams - # Ex: for 10 streams, 2 workers, and 8 seconds start up duration, - # the first worker should start at 0.0, 1.6, 3.2, 4.8, 6.4 - # and the second worker should start at 0.8, 2.4, 4.0, 5.6, 7.2 - delay_per_stream = self.startup_duration / self.streams - streams_per_worker = self.streams // local_world_size - - offset = local_rank * streams_per_worker * delay_per_stream - startup_requests = streams_per_worker + ( - 1 - if local_world_size > 1 and local_rank < self.streams % local_world_size - else 0 - ) - startup_requests_delay = delay_per_stream * local_world_size - else: - offset = 0.0 - startup_requests = 0 - startup_requests_delay = 0.0 + start_time = await self.get_processes_start_time() + + return start_time + (offset / self.worker_coount) + + def request_completed(self, request_info: RequestInfo): + """ + Update timing state with completed request information. - return LastCompletionRequestTimings( - offset=offset, - startup_requests=startup_requests, - startup_requests_delay=startup_requests_delay, - ) + :param request_info: Completed request metadata including timing + """ + if request_info.completed_at is not None: + self._process_last_request_time = request_info.completed_at @SchedulingStrategy.register("throughput") class ThroughputStrategy(SchedulingStrategy): """ - Maximum throughput strategy with optional concurrency limits. + Maximum throughput scheduling with optional concurrency limits. Schedules requests to maximize system throughput by allowing unlimited concurrent - processing with optional constraints and startup ramping for controlled ramp-up. + processing with optional constraints. Supports startup ramping to gradually + distribute initial requests for controlled system ramp-up. """ type_: Literal["throughput"] = "throughput" # type: ignore[assignment] @@ -526,16 +337,9 @@ class ThroughputStrategy(SchedulingStrategy): description="Maximum number of concurrent requests to schedule", gt=0, ) - startup_duration: float = Field( - default=0.0, - description="Duration in seconds for startup request distribution", - ge=0, - ) def __str__(self) -> str: """ - Return string representation of the strategy. - :return: String identifier for throughput strategy """ return "throughput" @@ -543,56 +347,57 @@ def __str__(self) -> str: @property def processes_limit(self) -> int | None: """ - Get maximum number of worker processes for throughput scheduling. - - :return: The max_concurrency value if set, otherwise None for unlimited + :return: Max concurrency if set, otherwise None for unlimited """ return self.max_concurrency @property def requests_limit(self) -> int | None: """ - Get maximum number of concurrent requests for throughput scheduling. - - :return: The max_concurrency value if set, otherwise None for unlimited + :return: Max concurrency if set, otherwise None for unlimited """ return self.max_concurrency - def create_request_timings( - self, local_rank: int, local_world_size: int, local_max_concurrency: int - ) -> ScheduledRequestTimings: + async def next_request_time(self, offset: int) -> float: """ - Create timing implementation for throughput request scheduling. + Calculate next request time with optional startup ramping. - :param local_rank: The rank of the worker process - :param local_world_size: Total number of worker processes - :param local_max_concurrency: Maximum concurrent requests for the worker - :return: NoDelayRequestTimings instance for immediate request scheduling + :param offset: Unused for throughput strategy + :return: Immediate start or ramped start time during startup period """ - if self.startup_duration > 0: - # Vary offset by up to 5% of the startup duration for a bit of variance - offset = 0.05 * self.startup_duration * (local_rank / local_world_size) - # Use local_max_concurrency as the target requests for startup convergence - startup_target_requests = local_max_concurrency - else: - offset = 0.0 - startup_target_requests = 1 + _ = offset # offset unused for throughput strategy + start_time = await self.get_processes_start_time() - return NoDelayRequestTimings( - startup_duration=self.startup_duration, - startup_target_requests=startup_target_requests, - offset=offset, - ) + if ( + self.startup_duration > 0 + and (time.time() - start_time) < self.startup_duration + and (current_index := self.next_request_index()) <= self.max_concurrency + ): + # linearly ramp start times to spread max_concurrency requests evenly + # over startup_duration + return start_time + self.startup_duration * ( + current_index / self.max_concurrency + ) + + return start_time + self.startup_duration + + def request_completed(self, request_info: RequestInfo): + """ + Handle request completion (no-op for throughput strategy). + + :param request_info: Completed request metadata (unused) + """ + _ = request_info # request_info unused for throughput strategy @SchedulingStrategy.register("constant") class AsyncConstantStrategy(ThroughputStrategy): """ - Asynchronous constant-rate scheduling strategy for predictable load patterns. + Constant-rate scheduling for predictable load patterns. Schedules requests at a fixed rate distributed evenly across worker processes, providing predictable timing behavior for steady-state load simulation and - consistent system performance measurement. + consistent system performance measurement. Requests arrive at uniform intervals. """ type_: Literal["constant"] = "constant" # type: ignore[assignment] @@ -600,53 +405,43 @@ class AsyncConstantStrategy(ThroughputStrategy): description="Rate for scheduling requests asynchronously in requests/second", gt=0, ) - startup_duration: float = Field( - default=0.0, - description="Duration in seconds for startup request distribution", - ge=0, - ) def __str__(self) -> str: """ - Return string representation of the strategy. - :return: String identifier with rate value """ return f"constant@{self.rate:.2f}" - def create_request_timings( - self, - local_rank: int, - local_world_size: int, - local_max_concurrency: int, # noqa: ARG002 - ) -> ScheduledRequestTimings: + async def next_request_time(self, offset: int) -> float: """ - Create timing implementation for constant-rate request scheduling. + Calculate next request time at fixed intervals. - :param local_rank: The rank of the worker process - :param local_world_size: Total number of worker processes for rate division - :param local_max_concurrency: Maximum concurrent requests for the worker - :return: ConstantRateRequestTimings instance with per-worker rate + :param offset: Unused for constant strategy + :return: Start time plus constant interval based on request index + """ + _ = offset # offset unused for throughput strategy + current_index = self.next_request_index() + start_time = await self.get_processes_start_time() + + return start_time + current_index / self.rate + + def request_completed(self, request_info: RequestInfo): """ - # Divide the rate evenly across all worker processes - worker_rate = self.rate / local_world_size - # Start each worker with an offset to interleave rates - worker_offset = (1 / self.rate) * local_rank + Handle request completion (no-op for constant strategy). - return ConstantRateRequestTimings( - rate=worker_rate, - offset=worker_offset, - ) + :param request_info: Completed request metadata (unused) + """ + _ = request_info # request_info unused for async constant strategy @SchedulingStrategy.register("poisson") class AsyncPoissonStrategy(ThroughputStrategy): """ - Asynchronous Poisson-distributed scheduling strategy for realistic load simulation. + Poisson-distributed scheduling for realistic load simulation. Schedules requests following a Poisson process with exponentially distributed inter-arrival times, providing realistic simulation of user behavior and network - traffic patterns with random variance around the target rate. + traffic patterns. Request arrivals have random variance around the target rate. """ type_: Literal["poisson"] = "poisson" # type: ignore[assignment] @@ -654,47 +449,71 @@ class AsyncPoissonStrategy(ThroughputStrategy): description="Rate for scheduling requests asynchronously in requests/second", gt=0, ) - startup_duration: float = Field( - default=0.0, - description="Duration in seconds for startup request distribution", - ge=0, - ) random_seed: int = Field( default=42, description="Random seed to use for Poisson distribution", ) + _random: random.Random | None = PrivateAttr(None) + _offset = PrivateAttr(None) + def __str__(self) -> str: """ - Return string representation of the strategy. - :return: String identifier with rate value """ return f"poisson@{self.rate:.2f}" - def create_request_timings( + def init_processes_timings( self, - local_rank: int, - local_world_size: int, - local_max_concurrency: int, # noqa: ARG002 - ) -> ScheduledRequestTimings: - """ - Create timing implementation for Poisson-distributed request scheduling. - - :param local_rank: The rank of the worker process for seed generation - :param local_world_size: Total number of worker processes for rate division - :param local_max_concurrency: Maximum concurrent requests for the worker - :return: PoissonRateRequestTimings instance with per-worker rate and unique seed - """ - # Divide the rate evenly across all worker processes - worker_rate = self.rate / local_world_size - # Use a different seed for each worker to ensure different sequences - worker_seed = self.random_seed + local_rank - # Start each worker with an offset to interleave rates - worker_offset = (1 / self.rate) * local_rank - - return PoissonRateRequestTimings( - rate=worker_rate, - random_seed=worker_seed, - offset=worker_offset, - ) + worker_count: int, + max_concurrency: int, + startup_duration: float, + ): + """ + Initialize Poisson-specific timing state. + + :param worker_count: Number of worker processes to coordinate + :param max_concurrency: Maximum number of concurrent requests allowed + :param startup_duration: Duration in seconds for request startup ramping + """ + super().init_processes_timings(worker_count, max_concurrency, startup_duration) + with self._processes_lock: + self._offset = Value("d", -1.0) + + def init_processes_start(self, start_time: float): + """ + Initialize the offset time for Poisson timing calculations. + + :param start_time: Unix timestamp when request processing should begin + """ + ThroughputStrategy.init_processes_start(self, start_time) + with self._processes_lock: + self._offset.value = start_time + + async def next_request_time(self, offset: int) -> float: + """ + Calculate next request time using exponential distribution. + + :param offset: Unused for Poisson strategy + :return: Next arrival time based on Poisson process + """ + _ = offset # offset unused for throughput strategy + _ = await self.get_processes_start_time() # ensure offset is initialized + + if self._random is None: + self._random = random.Random(self.random_seed) + + next_delay = self._random.expovariate(self.rate) + + with self._processes_lock: + self._offset.value += next_delay + + return self._offset.value + + def request_completed(self, request_info: RequestInfo): + """ + Handle request completion (no-op for Poisson strategy). + + :param request_info: Completed request metadata (unused) + """ + _ = request_info # request_info unused for async poisson strategy diff --git a/src/guidellm/scheduler/worker.py b/src/guidellm/scheduler/worker.py index 45716b78..a46455f9 100644 --- a/src/guidellm/scheduler/worker.py +++ b/src/guidellm/scheduler/worker.py @@ -1,10 +1,11 @@ """ -Individual worker process management for multi-process request execution. +Worker process implementation for distributed request execution and coordination. -Manages worker processes that handle request scheduling, backend processing, and -coordination in distributed benchmark environments. Workers consume requests from -queues, apply timing strategies, process requests through backends, and publish -status updates while maintaining synchronization across the process group. +Manages individual worker processes within the scheduler system, handling request +lifecycle from queue consumption through backend processing and status publication. +Workers coordinate with other processes through barriers and events, apply timing +strategies for request scheduling, maintain concurrency limits, and publish real-time +status updates throughout request processing. """ from __future__ import annotations @@ -19,13 +20,13 @@ import uvloop HAS_UVLOOP: Annotated[ - bool, "Flag indicating if uvloop is available for event loop optimization" + bool, "Flag indicating uvloop availability for event loop optimization" ] = True except ImportError: uvloop = None HAS_UVLOOP: Annotated[ - bool, "Flag indicating if uvloop is available for event loop optimization" + bool, "Flag indicating uvloop availability for event loop optimization" ] = False @@ -35,7 +36,7 @@ RequestT, ResponseT, ) -from guidellm.scheduler.strategies import ScheduledRequestTimings +from guidellm.scheduler.strategies import SchedulingStrategy from guidellm.schemas import RequestInfo from guidellm.utils import ( InterProcessMessaging, @@ -49,29 +50,34 @@ class WorkerProcess(Generic[RequestT, ResponseT]): """ - Individual worker process for distributed request execution and coordination. + Worker process for distributed request execution in the scheduler system. - Manages the complete request lifecycle from queue consumption through backend - processing and status publication. Coordinates with other workers through - barriers and events while maintaining configurable concurrency limits and - timing strategies for request scheduling. + Manages complete request lifecycle including queue consumption, backend processing, + timing strategy application, and status publication. Coordinates with other workers + through synchronization primitives while maintaining concurrency limits and handling + graceful shutdown scenarios including errors and cancellations. Example: :: worker = WorkerProcess( + worker_index=0, messaging=messaging_interface, + backend=backend_instance, + strategy=timing_strategy, async_limit=10, + fut_scheduling_time_limit=5.0, startup_barrier=barrier, + requests_generated_event=generated_event, + constraint_reached_event=constraint_event, shutdown_event=shutdown, error_event=error, - backend=backend_instance, - request_timings=timing_strategy ) worker.run() """ def __init__( self, + worker_index: int, messaging: InterProcessMessaging[ tuple[ ResponseT | None, @@ -80,8 +86,9 @@ def __init__( ], ], backend: BackendInterface[RequestT, ResponseT], - request_timings: ScheduledRequestTimings, + strategy: SchedulingStrategy, async_limit: int, + fut_scheduling_time_limit: float, startup_barrier: ProcessingBarrier, requests_generated_event: ProcessingEvent, constraint_reached_event: ProcessingEvent, @@ -91,22 +98,25 @@ def __init__( """ Initialize worker process instance. - :param messaging: Inter-process communication interface for request coordination - :param backend: Backend instance for processing requests - :param request_timings: Timing strategy for request scheduling - :param async_limit: Maximum concurrent requests this worker can handle - :param startup_barrier: Multiprocessing barrier for coordinated startup - :param requests_generated_event: Event signaling when request generation is - complete - :param constraint_reached_event: Event signaling when processing constraints - are met - :param shutdown_event: Event for signaling graceful shutdown - :param error_event: Event for signaling error conditions across processes + :param worker_index: Unique identifier for this worker within the process group + :param messaging: Inter-process messaging interface for request coordination + :param backend: Backend interface for processing requests + :param strategy: Scheduling strategy for determining request timing + :param async_limit: Maximum concurrent requests this worker can process + :param fut_scheduling_time_limit: Maximum time in seconds to schedule requests + into the future + :param startup_barrier: Synchronization barrier for coordinated startup + :param requests_generated_event: Event signaling request generation completion + :param constraint_reached_event: Event signaling processing constraint reached + :param shutdown_event: Event signaling graceful shutdown request + :param error_event: Event signaling error conditions across processes """ + self.worker_index = worker_index self.messaging = messaging self.backend = backend - self.request_timings = request_timings + self.strategy = strategy self.async_limit = async_limit + self.fut_scheduling_time_limit = fut_scheduling_time_limit self.startup_barrier = startup_barrier self.requests_generated_event = requests_generated_event self.constraint_reached_event = constraint_reached_event @@ -122,8 +132,8 @@ def run(self): """ Main entry point for worker process execution. - Initializes asyncio event loop with optional uvloop optimization and starts - worker async operations. Handles event loop cleanup for forked processes. + Initializes asyncio event loop with optional uvloop optimization and executes + worker async operations. Handles event loop cleanup and error propagation. :raises RuntimeError: If worker encounters unrecoverable error during execution """ @@ -142,9 +152,9 @@ async def run_async(self): """ Execute main asynchronous worker process logic. - Orchestrates concurrent execution of request processing and shutdown monitoring - tasks. Handles task cleanup, error propagation, and cancellation coordination - when any task completes or fails. + Orchestrates concurrent execution of request processing and shutdown monitoring. + Handles task cleanup, error propagation, and cancellation coordination when any + task completes or encounters an error. :raises RuntimeError: If worker tasks encounter unrecoverable errors :raises asyncio.CancelledError: If worker process was cancelled @@ -192,6 +202,7 @@ async def run_async(self): async def _stop_monitor( self, ) -> Literal["error_event", "shutdown_event"]: + """Monitor shutdown and error events for worker termination.""" exit_key = await wait_for_sync_objects( { "error_event": self.error_event, @@ -206,6 +217,12 @@ async def _stop_monitor( ) async def _process_requests(self): + """ + Manage request processing lifecycle from startup to shutdown. + + Coordinates startup synchronization, processes requests until constraints are + reached, then cancels pending requests until shutdown or error occurs. + """ try: # 1. Start up synchronization (backend, messaging, and other processes) # 2. Messaging startup, receive requests until requests_generated event @@ -227,6 +244,7 @@ async def _process_requests(self): await self._processing_shutdown() async def _processing_startup(self): + """Initialize backend, messaging, and synchronize with other workers.""" # Get backend ready await self.backend.process_startup() self.backend_started = True @@ -258,6 +276,12 @@ async def _processing_shutdown(self): self.startup_completed = False async def _process_requests_loop(self): + """ + Process requests continuously until cancelled with concurrency limits. + + Schedules and processes requests according to the timing strategy while + maintaining the configured concurrency limit through semaphore coordination. + """ try: # Run request processing async_semaphore = asyncio.Semaphore(self.async_limit) @@ -273,7 +297,18 @@ def _task_done(task): # Main loop; loop until canceled while True: await async_semaphore.acquire() - request_task = asyncio.create_task(self._process_next_request()) + request_time = await self.strategy.next_request_time( + offset=self.worker_index + ) + + if ( + time_until := request_time - time.time() + ) >= self.fut_scheduling_time_limit: + await asyncio.sleep(time_until - self.fut_scheduling_time_limit) + + request_task = asyncio.create_task( + self._process_next_request(target_start=request_time) + ) pending_tasks.add(request_task) request_task.add_done_callback(_task_done) except asyncio.CancelledError as err: @@ -284,6 +319,7 @@ def _task_done(task): raise err async def _cancel_requests_loop(self): + """Cancel all remaining queued requests until worker process terminates.""" while True: try: request: RequestT @@ -299,30 +335,32 @@ async def _cancel_requests_loop(self): request_info.timings.resolve_end = time.time() self._send_update("cancelled", None, request, request_info) - async def _process_next_request(self): + async def _process_next_request(self, target_start: float): + """ + Process a single request from queue to completion. + + Retrieves request from messaging queue, applies timing strategy, processes + through backend, and publishes status updates throughout the lifecycle. + + :param target_start: Unix timestamp when request should begin processing + """ request: RequestT | MultiTurnRequestT[RequestT] | None = None request_info: RequestInfo | None = None response: ResponseT | None = None try: - # Pull request from the queue + # Pull request from the queue, update state, and send "pending" update request, request_info = await self.messaging.get() + request_info.timings.dequeued = time.time() + request_info.scheduler_node_id = self.messaging.worker_index or -1 + request_info.timings.targeted_start = target_start + self._send_update("pending", response, request, request_info) if request is None or request_info is None: raise RuntimeError("Received invalid request or request info") - - if isinstance(request, (list, tuple)): + if isinstance(request, list | tuple): raise NotImplementedError("Multi-turn requests are not yet supported") - # Calculate targeted start and set pending state for request - request_info.scheduler_node_id = self.messaging.worker_index or -1 - request_info.timings.dequeued = time.time() - target_start = ( - request_info.scheduler_start_time + self.request_timings.next_offset() - ) - request_info.timings.targeted_start = target_start - self._send_update("pending", response, request, request_info) - # Schedule the request current_time = time.time() request_info.timings.scheduled_at = current_time @@ -355,6 +393,9 @@ async def _process_next_request(self): request_info.error = str(exc) request_info.timings.resolve_end = time.time() self._send_update("errored", response, request, request_info) + finally: + if request_info is not None: + self.strategy.request_completed(request_info) def _send_update( self, @@ -365,6 +406,18 @@ def _send_update( request: RequestT | MultiTurnRequestT[RequestT], request_info: RequestInfo, ): + """ + Publish request status update through messaging system. + + Updates request status and publishes to messaging queue for coordinator + consumption. Prevents duplicate status updates for the same state. + + :param new_status: New status for the request + :param response: Response object if available, None otherwise + :param request: Request object being processed + :param request_info: Request metadata and timing information + :raises Exception: If messaging system fails to publish the update + """ prev_status = request_info.status if new_status == prev_status: diff --git a/src/guidellm/scheduler/worker_group.py b/src/guidellm/scheduler/worker_group.py index 21394668..c6027989 100644 --- a/src/guidellm/scheduler/worker_group.py +++ b/src/guidellm/scheduler/worker_group.py @@ -14,7 +14,7 @@ import threading import time import uuid -from collections.abc import AsyncIterator, Generator, Iterable, Iterator +from collections.abc import AsyncIterator, Generator, Iterable from multiprocessing import get_context from multiprocessing.context import BaseContext from multiprocessing.managers import BaseManager @@ -62,7 +62,6 @@ class WorkerProcessGroup(Generic[RequestT, ResponseT]): group = WorkerProcessGroup( requests=request_iterable, - cycle_requests=None, backend=backend_instance, strategy=scheduling_strategy, constraints={"max_time": time_constraint} @@ -81,38 +80,25 @@ class WorkerProcessGroup(Generic[RequestT, ResponseT]): def __init__( self, - requests: Iterable[RequestT | MultiTurnRequestT[RequestT]] | None, - cycle_requests: Iterable[RequestT | MultiTurnRequestT[RequestT]] | None, + requests: Iterable[RequestT | MultiTurnRequestT[RequestT]], backend: BackendInterface[RequestT, ResponseT], strategy: SchedulingStrategy, - constraints: dict[str, Constraint], + startup_duration: float, + **constraints: dict[str, Constraint], ): """ Initialize a worker process group for distributed request processing. :param requests: Finite iterable of requests to process sequentially - :param cycle_requests: Iterable of requests to cycle through indefinitely :param backend: Backend interface for processing requests :param strategy: Scheduling strategy for request timing and distribution + :param startup_duration: Duration in seconds for request startup ramping :param constraints: Named constraints for controlling execution behavior - :raises ValueError: If neither requests nor cycle_requests are provided, - or if cycle_requests is an Iterator rather than Iterable """ - if requests is None and cycle_requests is None: - raise ValueError( - "At least one of 'requests' or 'cycle_requests' must be provided. " - ) - - if isinstance(cycle_requests, Iterator): - raise ValueError( - f"cycle_requests must be an Iterable or None, not an Iterator. " - f"Got {type(cycle_requests)}" - ) - self.requests = requests - self.cycle_requests = cycle_requests self.backend = backend self.strategy = strategy + self.startup_duration = startup_duration self.constraints = constraints # Multiprocessing contexts and primitives, created in create_processes @@ -186,9 +172,7 @@ async def create_processes(self): max_pending_size = max( 1, math.floor(max_conc * settings.mp_max_pending_buffer_percent) ) - per_proc_max_buffer_size = max( - 1, math.floor(per_proc_max_conc * settings.mp_max_worker_buffer_percent) - ) + per_proc_max_buffer_size = 1 # Initialize multiprocessing components self.mp_context = get_context(settings.mp_context_type) @@ -231,6 +215,11 @@ async def create_processes(self): # Initialize worker processes self.processes = [] + self.strategy.init_processes_timings( + worker_count=num_processes, + max_concurrency=max_conc, + startup_duration=self.startup_duration, + ) for rank in range(num_processes): # Distribute any remainder across the first N ranks async_limit = per_proc_max_conc + ( @@ -238,18 +227,16 @@ async def create_processes(self): ) worker = WorkerProcess[RequestT, ResponseT]( + worker_index=rank, messaging=self.messaging.create_worker_copy( worker_index=rank, max_buffer_send_size=None, max_buffer_receive_size=per_proc_max_buffer_size, ), backend=self.backend, - request_timings=self.strategy.create_request_timings( - local_rank=rank, - local_world_size=num_processes, - local_max_concurrency=async_limit, - ), + strategy=self.strategy, async_limit=async_limit, + fut_scheduling_time_limit=0.0, startup_barrier=self.startup_barrier, requests_generated_event=self.requests_generated_event, constraint_reached_event=self.constraint_reached_event, @@ -296,6 +283,7 @@ async def start(self, start_time: float): ): raise RuntimeError("create_processes() must be called before start()") + self.strategy.init_processes_start(start_time=start_time) stop_send_requests_event = threading.Event() send_requests_stopped_event = threading.Event() self.state = WorkerGroupState[RequestT, ResponseT]( @@ -308,11 +296,10 @@ async def start(self, start_time: float): constraint_reached_event=self.constraint_reached_event, shutdown_event=self.shutdown_event, error_event=self.error_event, + messaging=self.messaging, ) await self.messaging.start( - send_items=self.state.requests_generator( - self.requests, self.cycle_requests - ), + send_items=self.state.requests_generator(self.requests), receive_callback=self.state.received_callback, send_stopped_event=send_requests_stopped_event, send_stop_criteria=[stop_send_requests_event], @@ -424,6 +411,8 @@ async def shutdown(self) -> list[Exception]: # noqa: C901 class _StateUpdate(NamedTuple): + """Internal state update result with control flags.""" + state: SchedulerState stop_queueing: bool stop_processing: bool @@ -449,6 +438,15 @@ def __init__( constraint_reached_event: Event, shutdown_event: Event, error_event: Event, + messaging: InterProcessMessaging[ + tuple[RequestT | MultiTurnRequestT[RequestT], RequestInfo], + tuple[ + ResponseT | None, + RequestT | MultiTurnRequestT[RequestT], + RequestInfo, + SchedulerState, + ], + ], ): """ Initialize worker group state management. @@ -456,6 +454,7 @@ def __init__( :param start_time: Unix timestamp when processing should begin :param processes: List of worker process instances :param constraints: Named constraints for controlling execution behavior + :param stop_send_requests_event: Threading event for stopping request generation :param send_requests_stopped_event: Threading event for request coordination :param requests_generated_event: Multiprocessing event for generation completion :param constraint_reached_event: Multiprocessing event for constraint stopping @@ -471,6 +470,7 @@ def __init__( self.constraint_reached_event = constraint_reached_event self.shutdown_event = shutdown_event self.error_event = error_event + self.messaging = messaging self._update_lock: threading.Lock = threading.Lock() self._state: SchedulerState = SchedulerState( @@ -483,9 +483,7 @@ def __init__( self._processing_requests: set[RequestT | MultiTurnRequestT[RequestT]] = set() def requests_generator( - self, - requests: Iterable[RequestT | MultiTurnRequestT[RequestT]] | None, - cycle_requests: Iterable[RequestT | MultiTurnRequestT[RequestT]] | None, + self, requests: Iterable[RequestT | MultiTurnRequestT[RequestT]] ) -> Generator[ tuple[RequestT | MultiTurnRequestT[RequestT], RequestInfo], None, None ]: @@ -497,22 +495,12 @@ def requests_generator( constraints to determine when to stop request generation. :param requests: Finite iterable of requests to process sequentially - :param cycle_requests: Iterable of requests to cycle through indefinitely :return: Generator yielding (request, request_info) tuples """ - def _iter() -> Iterator[RequestT | MultiTurnRequestT[RequestT]]: - if requests is not None: - yield from requests - - if cycle_requests is not None: - while True: - yield from cycle_requests - try: count = 0 - request_iter = _iter() - for request in request_iter: + for request in iter(requests): count += 1 if hasattr(request, "request_id"): @@ -529,6 +517,9 @@ def _iter() -> Iterator[RequestT | MultiTurnRequestT[RequestT]]: ) state_update = self._locked_update(request_info) request_info.timings.queued = time.time() + self.messaging.buffer_receive_queue.sync_put( + (None, request, request_info, state_update.state) + ) yield (request, request_info) From 91f79b778020acc12163c5ae6231a99eea3cc3bd Mon Sep 17 00:00:00 2001 From: Mark Kurtz Date: Wed, 15 Oct 2025 13:15:56 -0400 Subject: [PATCH 34/57] Propagate valid failures from HuggingFace datasets loading (ones that are not related to not found errors) for better messaging to the user --- .../data/deserializers/deserializer.py | 2 +- .../data/deserializers/huggingface.py | 42 ++++++++++++------- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/src/guidellm/data/deserializers/deserializer.py b/src/guidellm/data/deserializers/deserializer.py index cb362710..d50e4a9c 100644 --- a/src/guidellm/data/deserializers/deserializer.py +++ b/src/guidellm/data/deserializers/deserializer.py @@ -77,7 +77,7 @@ def deserialize( if dataset is None: raise DataNotSupportedError( f"No suitable deserializer found for data {data} " - f"with kwargs {data_kwargs} and type_ {type_}." + f"with kwargs {data_kwargs} and deserializer type {type_}." ) if resolve_split: diff --git a/src/guidellm/data/deserializers/huggingface.py b/src/guidellm/data/deserializers/huggingface.py index e356043a..80e0ed8c 100644 --- a/src/guidellm/data/deserializers/huggingface.py +++ b/src/guidellm/data/deserializers/huggingface.py @@ -12,6 +12,11 @@ load_dataset, load_from_disk, ) +from datasets.exceptions import ( + DataFilesNotFoundError, + DatasetNotFoundError, + FileNotFoundDatasetsError, +) from transformers import PreTrainedTokenizerBase from guidellm.data.deserializers.deserializer import ( @@ -35,38 +40,45 @@ def __call__( _ = (processor_factory, random_seed) if isinstance( - data, (Dataset, IterableDataset, DatasetDict, IterableDatasetDict) + data, Dataset | IterableDataset | DatasetDict | IterableDatasetDict ): return data load_error = None if ( - isinstance(data, (str, Path)) + isinstance(data, str | Path) and (path := Path(data)).exists() and ((path.is_file() and path.suffix == ".py") or path.is_dir()) ): # Handle python script or nested python script in a directory try: return load_dataset(str(data), **data_kwargs) - except Exception as err: # noqa: BLE001 - load_error = err - - if ( - isinstance(data, (str, Path)) - and (path := Path(data)).exists() - and path.is_dir() - ): - # Handle local dataset directory - try: - return load_from_disk(str(data), **data_kwargs) - except Exception as err: # noqa: BLE001 + except ( + FileNotFoundDatasetsError, + DatasetNotFoundError, + DataFilesNotFoundError, + ) as err: load_error = err + except Exception: # noqa: BLE001 + # Try loading as a local dataset directory next + try: + return load_from_disk(str(data), **data_kwargs) + except ( + FileNotFoundDatasetsError, + DatasetNotFoundError, + DataFilesNotFoundError, + ) as err2: + load_error = err2 try: # Handle dataset identifier from the Hugging Face Hub return load_dataset(str(data), **data_kwargs) - except Exception as err: # noqa: BLE001 + except ( + FileNotFoundDatasetsError, + DatasetNotFoundError, + DataFilesNotFoundError, + ) as err: load_error = err not_supported = DataNotSupportedError( From 5f4a731cd795fba049ff655565981d8823c49cc5 Mon Sep 17 00:00:00 2001 From: Mark Kurtz Date: Wed, 15 Oct 2025 15:51:13 -0400 Subject: [PATCH 35/57] Fixes from review --- src/guidellm/scheduler/strategies.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/guidellm/scheduler/strategies.py b/src/guidellm/scheduler/strategies.py index 5e13a26d..0cd3bc63 100644 --- a/src/guidellm/scheduler/strategies.py +++ b/src/guidellm/scheduler/strategies.py @@ -65,7 +65,7 @@ def __pydantic_schema_base_type__(cls) -> type[SchedulingStrategy]: type_: Literal["strategy"] = Field( description="The type of scheduling strategy to schedule requests with", ) - worker_coount: int = Field( + worker_count: int = Field( default=0, description="Number of worker processes to use for this strategy", ge=0, @@ -117,7 +117,7 @@ def init_processes_timings( :param max_concurrency: Maximum number of concurrent requests allowed :param startup_duration: Duration in seconds for request startup ramping """ - self.worker_coount = worker_count + self.worker_count = worker_count self.max_concurrency = max_concurrency self.startup_duration = startup_duration @@ -309,7 +309,7 @@ async def next_request_time(self, offset: int) -> float: start_time = await self.get_processes_start_time() - return start_time + (offset / self.worker_coount) + return start_time + (offset / self.worker_count) def request_completed(self, request_info: RequestInfo): """ From 57683a2a681ca7f4898045e2dfd48d9058fdfe7c Mon Sep 17 00:00:00 2001 From: Mark Kurtz Date: Thu, 16 Oct 2025 15:42:47 -0400 Subject: [PATCH 36/57] [GuideLLM Refactor] Reenablement of scenarios and fixes for benchmark package and CLI pathways (#414) ## Summary Changed the benchmarking entrypoint to take in an Args object which is now used to load scenarios. It enables a single source of truth in addition to being able to save the exact configurations in the report output. ## Details - [ ] ## Test Plan - ## Related Issues - Resolves # --- - [ ] "I certify that all code in this PR is my own, except as noted below." ## Use of AI - [ ] Includes AI-assisted code completion - [ ] Includes code generated by an AI application - [ ] Includes AI-generated tests (NOTE: AI written tests should have a docstring that includes `## WRITTEN BY AI ##`) --- pyproject.toml | 1 + src/guidellm/__main__.py | 568 +++++--------- src/guidellm/benchmark/__init__.py | 20 +- src/guidellm/benchmark/benchmarker.py | 57 +- src/guidellm/benchmark/entrypoints.py | 267 ++++--- src/guidellm/benchmark/profile.py | 233 +++--- src/guidellm/benchmark/scenario.py | 169 ---- src/guidellm/benchmark/scenarios/__init__.py | 40 + src/guidellm/benchmark/scenarios/chat.json | 6 +- src/guidellm/benchmark/scenarios/rag.json | 6 +- src/guidellm/benchmark/schemas.py | 740 ++++++++++++++++-- src/guidellm/benchmark/types.py | 22 - .../data/deserializers/deserializer.py | 15 +- src/guidellm/presentation/data_models.py | 12 +- src/guidellm/utils/cli.py | 44 +- 15 files changed, 1317 insertions(+), 883 deletions(-) delete mode 100644 src/guidellm/benchmark/scenario.py delete mode 100644 src/guidellm/benchmark/types.py diff --git a/pyproject.toml b/pyproject.toml index 8fe6d950..5135edad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,7 @@ include = ["*"] [tool.setuptools.package-data] "guidellm.data" = ["*.gz"] +"guidellm.benchmark.scenarios" = ["*.json", "**/*.json"] [tool.pdm] distribution = true diff --git a/src/guidellm/__main__.py b/src/guidellm/__main__.py index 680ac852..1e9ba96f 100644 --- a/src/guidellm/__main__.py +++ b/src/guidellm/__main__.py @@ -1,14 +1,12 @@ """ -GuideLLM command-line interface providing benchmarking, dataset preprocessing, and -mock server functionality. +GuideLLM command-line interface entry point. -This module serves as the primary entry point for the GuideLLM CLI application, -offering a comprehensive suite of tools for language model evaluation and testing. -It provides three main command groups: benchmark operations for performance testing -against generative models, dataset preprocessing utilities for data preparation and -transformation, and a mock server for testing and development scenarios. The CLI -supports various backends, output formats, and configuration options to accommodate -different benchmarking needs and deployment environments. +Primary CLI application providing benchmark execution, dataset preprocessing, and +mock server functionality for language model evaluation. Organizes commands into +three main groups: benchmark operations for performance testing, preprocessing +utilities for data transformation, and mock server capabilities for development +and testing. Supports multiple backends, output formats, and flexible configuration +through CLI options and environment variables. Example: :: @@ -30,6 +28,7 @@ from pathlib import Path import click +from pydantic import ValidationError try: import uvloop @@ -38,12 +37,13 @@ from guidellm.backends import BackendType from guidellm.benchmark import ( + BenchmarkGenerativeTextArgs, GenerativeConsoleBenchmarkerProgress, ProfileType, benchmark_generative_text, + get_builtin_scenarios, reimport_benchmarks_report, ) -from guidellm.benchmark.scenario import GenerativeTextScenario from guidellm.mock_server import MockServer, MockServerConfig from guidellm.preprocess.dataset import ShortPromptStrategy, process_dataset from guidellm.scheduler import StrategyType @@ -65,22 +65,21 @@ "run", ] -# Available strategy and profile choices for benchmark execution types STRATEGY_PROFILE_CHOICES: list[str] = list(get_literal_vals(ProfileType | StrategyType)) +"""Available strategy and profile type choices for benchmark execution.""" def decode_escaped_str(_ctx, _param, value): """ Decode escape sequences in Click option values. - Click automatically escapes characters in option values, converting sequences - like "\\n" to "\\\\n". This function properly decodes these escape sequences - to their intended characters for use in CLI options. + Click automatically escapes characters converting sequences like "\\n" to + "\\\\n". This function decodes these sequences to their intended characters. :param _ctx: Click context (unused) :param _param: Click parameter (unused) - :param value: String value to decode escape sequences from - :return: Decoded string with proper escape sequences + :param value: String value to decode + :return: Decoded string with proper escape sequences, or None if input is None :raises click.BadParameter: When escape sequence decoding fails """ if value is None: @@ -94,89 +93,76 @@ def decode_escaped_str(_ctx, _param, value): @click.group() @click.version_option(package_name="guidellm", message="guidellm version: %(version)s") def cli(): - """ - Main entry point for the GuideLLM command-line interface. - - This is the root command group that organizes all GuideLLM CLI functionality - into logical subgroups for benchmarking, preprocessing, configuration, and - mock server operations. - """ + """GuideLLM CLI for benchmarking, preprocessing, and testing language models.""" @cli.group( - help="Commands to run a new benchmark or load a prior one.", + help="Run a benchmark or load a previously saved benchmark report.", cls=DefaultGroupHandler, default="run", ) def benchmark(): - """ - Benchmark command group for running and managing performance tests. - - This command group provides functionality to execute new benchmarks against - generative models and load previously saved benchmark reports for analysis. - Supports various benchmarking strategies, output formats, and backend types. - """ + """Benchmark commands for performance testing generative models.""" @benchmark.command( "run", - help="Run a benchmark against a generative model using the specified arguments.", + help=( + "Run a benchmark against a generative model. " + "Supports multiple backends, data sources, strategies, and output formats. " + "Configuration can be loaded from a scenario file or specified via options." + ), context_settings={"auto_envvar_prefix": "GUIDELLM"}, ) -# @click.option( -# "--scenario", -# type=cli_tools.Union( -# click.Path( -# exists=True, -# readable=True, -# file_okay=True, -# dir_okay=False, -# path_type=Path, -# ), -# click.Choice(get_builtin_scenarios()), -# ), -# default=None, -# help=( -# "The name of a builtin scenario or path to a config file. " -# "Missing values from the config will use defaults. " -# "Options specified on the commandline will override the scenario." -# ), -# ) +@click.option( + "--scenario", + type=cli_tools.Union( + click.Path( + exists=True, + readable=True, + file_okay=True, + dir_okay=False, + path_type=Path, + ), + click.Choice(get_builtin_scenarios().keys()), + ), + default=None, + help=( + "Builtin scenario name or path to config file. " + "CLI options override scenario settings." + ), +) @click.option( "--target", type=str, - help="The target path for the backend to run benchmarks against. For example, http://localhost:8000", + help="Target backend URL (e.g., http://localhost:8000).", ) @click.option( "--data", type=str, multiple=True, help=( - "The HuggingFace dataset ID, a path to a HuggingFace dataset, " - "a path to a data file csv, json, jsonl, or txt, " - "or a synthetic data config as a json or key=value string." + "HuggingFace dataset ID, path to dataset, path to data file " + "(csv/json/jsonl/txt), or synthetic data config (json/key=value)." ), ) @click.option( "--profile", "--rate-type", # legacy alias "profile", + default=BenchmarkGenerativeTextArgs.get_default("profile"), type=click.Choice(STRATEGY_PROFILE_CHOICES), - help=( - "The type of benchmark to run. " - f"Supported types {', '.join(STRATEGY_PROFILE_CHOICES)}. " - ), + help=f"Benchmark profile type. Options: {', '.join(STRATEGY_PROFILE_CHOICES)}.", ) @click.option( "--rate", - default=GenerativeTextScenario.get_default("rate"), + type=float, + multiple=True, + default=BenchmarkGenerativeTextArgs.get_default("rate"), help=( - "The rates to run the benchmark at. " - "Can be a single number or a comma-separated list of numbers. " - "For rate-type=sweep, this is the number of benchmarks it runs in the sweep. " - "For rate-type=concurrent, this is the number of concurrent requests. " - "For rate-type=async,constant,poisson, this is the rate requests per second. " - "For rate-type=synchronous,throughput, this must not be set." + "Benchmark rate(s) to test. Meaning depends on profile: " + "sweep=number of benchmarks, concurrent=concurrent requests, " + "async/constant/poisson=requests per second." ), ) # Backend configuration @@ -185,166 +171,132 @@ def benchmark(): "--backend-type", # legacy alias "backend", type=click.Choice(list(get_literal_vals(BackendType))), - default=GenerativeTextScenario.get_default("backend"), - help=( - "The type of backend to use to run requests against. Defaults to 'openai_http'." - f" Supported types: {', '.join(get_literal_vals(BackendType))}" - ), + default=BenchmarkGenerativeTextArgs.get_default("backend"), + help=f"Backend type. Options: {', '.join(get_literal_vals(BackendType))}.", ) @click.option( "--backend-kwargs", "--backend-args", # legacy alias "backend_kwargs", callback=cli_tools.parse_json, - default=GenerativeTextScenario.get_default("backend_kwargs"), - help=( - "A JSON string containing any arguments to pass to the backend as a " - "dict with **kwargs." - ), + default=BenchmarkGenerativeTextArgs.get_default("backend_kwargs"), + help="JSON string of arguments to pass to the backend.", ) @click.option( "--model", - default=GenerativeTextScenario.get_default("model"), + default=BenchmarkGenerativeTextArgs.get_default("model"), type=str, - help=( - "The ID of the model to benchmark within the backend. " - "If None provided (default), then it will use the first model available." - ), + help="Model ID to benchmark. If not provided, uses first available model.", ) # Data configuration @click.option( "--request-type", - default="chat_completions", + default=BenchmarkGenerativeTextArgs.get_default("data_request_formatter"), type=click.Choice(list(get_literal_vals(GenerativeRequestType))), help=( - "The type of request to create for each data sample and send to the backend. " - f"Supported types: {list(get_literal_vals(GenerativeRequestType))}." + f"Request type to create for each data sample. " + f"Options: {', '.join(get_literal_vals(GenerativeRequestType))}." ), ) @click.option( "--request-formatter-kwargs", default=None, callback=cli_tools.parse_json, - help=( - "A JSON string containing any arguments to pass to the request formatter " - "as a dict with **kwargs." - ), + help="JSON string of arguments to pass to the request formatter.", ) @click.option( "--processor", - default=GenerativeTextScenario.get_default("processor"), + default=BenchmarkGenerativeTextArgs.get_default("processor"), type=str, help=( - "The processor or tokenizer to use to calculate token counts for statistics " - "and synthetic data generation. If None provided (default), will load " - "using the model arg, if needed." + "Processor or tokenizer for token count calculations. " + "If not provided, loads from model." ), ) @click.option( "--processor-args", - default=GenerativeTextScenario.get_default("processor_args"), + default=BenchmarkGenerativeTextArgs.get_default("processor_args"), callback=cli_tools.parse_json, - help=( - "A JSON string containing any arguments to pass to the processor constructor " - "as a dict with **kwargs." - ), + help="JSON string of arguments to pass to the processor constructor.", ) @click.option( "--data-args", multiple=True, - default=None, + default=BenchmarkGenerativeTextArgs.get_default("data_args"), callback=cli_tools.parse_json, - help=( - "A JSON string containing any arguments to pass to the dataset creation " - "as a dict with **kwargs." - ), + help="JSON string of arguments to pass to dataset creation.", ) @click.option( "--data-samples", - default=-1, + default=BenchmarkGenerativeTextArgs.get_default("data_samples"), type=int, help=( - "The number of samples to use from the dataset. If -1 (default), will use all " - "samples in the dataset and dynamically generate samples. " - "If >1, will precompile that number of items from the dataset configs." + "Number of samples from dataset. -1 (default) uses all samples " + "and dynamically generates more." ), ) @click.option( - "--data-column-mappings", - default=None, + "--data-column-mapper", + default=BenchmarkGenerativeTextArgs.get_default("data_column_mapper"), callback=cli_tools.parse_json, - help=( - "A JSON string of column mappings to apply to the dataset to map into request " - "column types." - ), + help="JSON string of column mappings to apply to the dataset.", ) @click.option( "--data-sampler", - default=None, + default=BenchmarkGenerativeTextArgs.get_default("data_sampler"), type=click.Choice(["shuffle"]), - help="The data sampler type to use.", + help="Data sampler type.", ) @click.option( "--data-num-workers", - default=None, + default=BenchmarkGenerativeTextArgs.get_default("data_num_workers"), type=int, - help="The number of worker processes to use for data loading.", + help="Number of worker processes for data loading.", ) @click.option( "--dataloader_kwargs", - default=None, + default=BenchmarkGenerativeTextArgs.get_default("dataloader_kwargs"), callback=cli_tools.parse_json, - help=( - "A JSON string containing any arguments to pass to the dataloader constructor " - "as a dict with **kwargs." - ), + help="JSON string of arguments to pass to the dataloader constructor.", ) @click.option( "--random-seed", - default=GenerativeTextScenario.get_default("random_seed"), + default=BenchmarkGenerativeTextArgs.get_default("random_seed"), type=int, - help="The random seed to use for benchmarking to ensure reproducibility.", + help="Random seed for reproducibility.", ) # Output configuration @click.option( "--output-path", type=click.Path(), - default=Path.cwd(), + default=BenchmarkGenerativeTextArgs.get_default("output_path"), help=( - "The path to save the output formats to, if the format is a file type. " - "If it is a directory, it will save all output formats selected under it. " - "If it is a file, it will save the corresponding output format to that file. " - "Any output formats that were given that do not match the file extension will " - "be saved in the parent directory of the file path. " - "Defaults to the current working directory. " + "Path to save output files. Can be a directory or file. " + "If a file, saves that format; mismatched formats save to parent directory." ), ) @click.option( "--output-formats", multiple=True, type=str, - default=("console", "json"), # ("console", "json", "html", "csv") - help=( - "The output formats to use for the benchmark results. " - "Defaults to console, json, html, and csv where the file formats " - "will be saved at the specified output path." - ), + default=BenchmarkGenerativeTextArgs.get_default("output_formats"), + help="Output formats for results (e.g., console, json, html, csv).", ) @click.option( "--disable-console-outputs", is_flag=True, - help="Set this flag to disable console output", + help="Disable console output.", ) # Updates configuration @click.option( "--disable-progress", is_flag=True, - help="Set this flag to disable progress updates to the console", + help="Disable progress updates to the console.", ) @click.option( "--display-scheduler-stats", is_flag=True, - help="Set this flag to display stats for the processes running the benchmarks", + help="Display scheduler process statistics.", ) # Aggregators configuration @click.option( @@ -352,13 +304,10 @@ def benchmark(): "--warmup-percent", # legacy alias "warmup", type=float, - default=GenerativeTextScenario.get_default("warmup"), + default=BenchmarkGenerativeTextArgs.get_default("warmup"), help=( - "The specification around the number of requests to run before benchmarking. " - "If within (0, 1), then the percent of requests/time to use for warmup. " - "If >=1, then the number of requests or seconds to use for warmup." - "Whether it's requests/time used is dependent on which constraint is active. " - "Default None for no warmup." + "Warmup specification: if in (0,1) = percent, if >=1 = number of " + "requests/seconds (depends on active constraint)." ), ) @click.option( @@ -366,13 +315,10 @@ def benchmark(): "--cooldown-percent", # legacy alias "cooldown", type=float, - default=GenerativeTextScenario.get_default("cooldown"), + default=BenchmarkGenerativeTextArgs.get_default("cooldown"), help=( - "The specification around the number of requests to run after benchmarking. " - "If within (0, 1), then the percent of requests/time to use for cooldown. " - "If >=1, then the number of requests or seconds to use for cooldown." - "Whether it's requests/time used is dependent on which constraint is active. " - "Default None for no cooldown." + "Cooldown specification: if in (0,1) = percent, if >=1 = number of " + "requests/seconds (depends on active constraint)." ), ) @click.option( @@ -381,129 +327,86 @@ def benchmark(): "sample_requests", type=int, help=( - "The number of samples for each request status and each benchmark to save " - "in the output file. If None (default), will save all samples. " - "Defaults to 20." + "Number of sample requests per status to save. " + "None (default) saves all, recommended: 20." ), ) # Constraints configuration @click.option( "--max-seconds", type=float, - default=GenerativeTextScenario.get_default("max_seconds"), + default=BenchmarkGenerativeTextArgs.get_default("max_seconds"), help=( - "The maximum number of seconds each benchmark can run for. " - "If None, will run until max_requests or the data is exhausted." + "Maximum seconds per benchmark. " + "If None, runs until max_requests or data exhaustion." ), ) @click.option( "--max-requests", type=int, - default=GenerativeTextScenario.get_default("max_requests"), + default=BenchmarkGenerativeTextArgs.get_default("max_requests"), help=( - "The maximum number of requests each benchmark can run for. " - "If None, will run until max_seconds or the data is exhausted." + "Maximum requests per benchmark. " + "If None, runs until max_seconds or data exhaustion." ), ) @click.option( "--max-errors", type=int, - default=GenerativeTextScenario.get_default("max_errors"), - help="Maximum number of errors allowed before stopping the benchmark", + default=BenchmarkGenerativeTextArgs.get_default("max_errors"), + help="Maximum errors before stopping the benchmark.", ) @click.option( "--max-error-rate", type=float, - default=GenerativeTextScenario.get_default("max_error_rate"), - help="Maximum error rate allowed before stopping the benchmark", + default=BenchmarkGenerativeTextArgs.get_default("max_error_rate"), + help="Maximum error rate before stopping the benchmark.", ) @click.option( "--max-global-error-rate", type=float, - default=GenerativeTextScenario.get_default("max_global_error_rate"), - help="Maximum global error rate allowed across all benchmarks", + default=BenchmarkGenerativeTextArgs.get_default("max_global_error_rate"), + help="Maximum global error rate across all benchmarks.", ) -def run( - target, - data, - profile, - rate, - # Backend Configuration - backend, - backend_kwargs, - model, - # Data configuration - request_type, - request_formatter_kwargs, - processor, - processor_args, - data_args, - data_samples, - data_column_mappings, - data_sampler, - data_num_workers, - dataloader_kwargs, - random_seed, - # Output configuration - output_path, - output_formats, - # Updates configuration - disable_console_outputs, - disable_progress, - display_scheduler_stats, - # Benchmarker configuration - sample_requests, - warmup, - cooldown, - # Constraints configuration - max_seconds, - max_requests, - max_errors, - max_error_rate, - max_global_error_rate, -): - """ - Execute a generative text benchmark against a target model backend. - - Runs comprehensive performance testing using various strategies and profiles, - collecting metrics on latency, throughput, error rates, and resource usage. - Supports multiple backends, data sources, output formats, and constraint types - for flexible benchmark configuration. - """ - data_request_formatter = ( +def run(**kwargs): + request_type = kwargs.pop("request_type", None) + request_formatter_kwargs = kwargs.pop("request_formatter_kwargs", None) + kwargs["data_request_formatter"] = ( request_type if not request_formatter_kwargs else {"request_type": request_type, **request_formatter_kwargs} ) + kwargs["data"] = cli_tools.format_list_arg( + kwargs.get("data"), default=[], simplify_single=False + ) + kwargs["data_args"] = cli_tools.format_list_arg( + kwargs.get("data_args"), default=[], simplify_single=False + ) + kwargs["rate"] = cli_tools.format_list_arg( + kwargs.get("rate"), default=None, simplify_single=True + ) + + disable_console_outputs = kwargs.pop("disable_console_outputs", False) + display_scheduler_stats = kwargs.pop("display_scheduler_stats", False) + disable_progress = kwargs.pop("disable_progress", False) + + try: + args = BenchmarkGenerativeTextArgs.create( + scenario=kwargs.pop("scenario", None), **kwargs + ) + except ValidationError as err: + # Translate pydantic valdation error to click argument error + errs = err.errors(include_url=False, include_context=True, include_input=True) + param_name = "--" + str(errs[0]["loc"][0]).replace("_", "-") + raise click.BadParameter( + errs[0]["msg"], ctx=click.get_current_context(), param_hint=param_name + ) from err if uvloop is not None: asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) asyncio.run( benchmark_generative_text( - target=target, - data=list(data), - # Benchmark configuration - profile=profile, - rate=rate, - # Backend configuration - backend=backend, - backend_kwargs=backend_kwargs, - model=model, - # Data configuration - processor=processor, - processor_args=processor_args, - data_args=data_args, - data_samples=data_samples, - data_column_mapper=data_column_mappings, - data_request_formatter=data_request_formatter, - data_sampler=data_sampler, - data_num_workers=data_num_workers, - dataloader_kwargs=dataloader_kwargs, - random_seed=random_seed, - # Output configuration - output_path=output_path, - output_formats=output_formats, - # Updates configuration + args=args, progress=( GenerativeConsoleBenchmarkerProgress( display_scheduler_stats=display_scheduler_stats @@ -511,22 +414,18 @@ def run( if not disable_progress else None ), - print_updates=not disable_console_outputs, - # Benchmarker configuration - sample_requests=sample_requests, - warmup=warmup, - cooldown=cooldown, - # Constraints configuration - max_seconds=max_seconds, - max_requests=max_requests, - max_errors=max_errors, - max_error_rate=max_error_rate, - max_global_error_rate=max_global_error_rate, + console=Console() if not disable_console_outputs else None, ) ) -@benchmark.command("from-file", help="Load a saved benchmark report.") +@benchmark.command( + "from-file", + help=( + "Load a saved benchmark report and optionally re-export to other formats. " + "PATH: Path to the saved benchmark report file (default: ./benchmarks.json)." + ), +) @click.argument( "path", type=click.Path(file_okay=True, dir_okay=False, exists=True), @@ -537,13 +436,9 @@ def run( type=click.Path(), default=Path.cwd(), help=( - "Allows re-exporting the benchmarks to other formats. " - "The path to save the output formats to, if the format is a file type. " - "If it is a directory, it will save all output formats selected under it. " - "If it is a file, it will save the corresponding output format to that file. " - "Any output formats that were given that do not match the file extension will " - "be saved in the parent directory of the file path. " - "Defaults to the current working directory. " + "Directory or file path to save re-exported benchmark results. " + "If a directory, all output formats will be saved there. " + "If a file, the matching format will be saved to that file." ), ) @click.option( @@ -551,57 +446,33 @@ def run( multiple=True, type=str, default=("console", "json"), # ("console", "json", "html", "csv") - help=( - "The output formats to use for the benchmark results. " - "Defaults to console, json, html, and csv where the file formats " - "will be saved at the specified output path." - ), + help="Output formats for benchmark results (e.g., console, json, html, csv).", ) def from_file(path, output_path, output_formats): - """ - Load and optionally re-export a previously saved benchmark report. - - Imports benchmark results from a saved file and provides optional conversion - to different output formats. Supports JSON, YAML, and CSV export formats - based on the output file extension. - """ asyncio.run(reimport_benchmarks_report(path, output_path, output_formats)) @cli.command( - short_help="Prints environment variable settings.", - help=( - "Print out the available configuration settings that can be set " - "through environment variables." - ), + short_help="Show configuration settings.", + help="Display environment variables for configuring GuideLLM behavior.", ) def config(): - """ - Display available GuideLLM configuration environment variables. - - Prints a comprehensive list of all environment variables that can be used - to configure GuideLLM behavior, including their current values, defaults, - and descriptions. - """ print_config() -@cli.group(help="General preprocessing tools and utilities.") +@cli.group(help="Tools for preprocessing datasets for use in benchmarks.") def preprocess(): - """ - Preprocessing command group for dataset preparation and transformation. - - This command group provides utilities for converting, processing, and - optimizing datasets for use in GuideLLM benchmarks. Includes functionality - for token count adjustments, format conversions, and data validation. - """ + """Dataset preprocessing utilities.""" @preprocess.command( + "dataset", help=( - "Convert a dataset to have specific prompt and output token sizes.\n" - "DATA: Path to the input dataset or dataset ID.\n" - "OUTPUT_PATH: Path to save the converted dataset, including file suffix." + "Process a dataset to have specific prompt and output token sizes. " + "Supports multiple strategies for handling prompts and optional " + "Hugging Face Hub upload.\n\n" + "DATA: Path to the input dataset or dataset ID.\n\n" + "OUTPUT_PATH: Path to save the processed dataset, including file suffix." ), context_settings={"auto_envvar_prefix": "GUIDELLM"}, ) @@ -619,81 +490,70 @@ def preprocess(): "--processor", type=str, required=True, - help=( - "The processor or tokenizer to use to calculate token counts for statistics " - "and synthetic data generation." - ), + help="Processor or tokenizer name for calculating token counts.", ) @click.option( "--processor-args", default=None, callback=cli_tools.parse_json, - help=( - "A JSON string containing any arguments to pass to the processor constructor " - "as a dict with **kwargs." - ), + help="JSON string of arguments to pass to the processor constructor.", ) @click.option( "--data-args", callback=cli_tools.parse_json, - help=( - "A JSON string containing any arguments to pass to the dataset creation " - "as a dict with **kwargs." - ), + help="JSON string of arguments to pass to dataset creation.", ) @click.option( "--short-prompt-strategy", type=click.Choice([s.value for s in ShortPromptStrategy]), default=ShortPromptStrategy.IGNORE.value, show_default=True, - help="Strategy to handle prompts shorter than the target length. ", + help="Strategy for handling prompts shorter than target length.", ) @click.option( "--pad-char", type=str, default="", callback=decode_escaped_str, - help="The token to pad short prompts with when using the 'pad' strategy.", + help="Character to pad short prompts with when using 'pad' strategy.", ) @click.option( "--concat-delimiter", type=str, default="", help=( - "The delimiter to use when concatenating prompts that are too short." - " Used when strategy is 'concatenate'." + "Delimiter for concatenating short prompts (used with 'concatenate' strategy)." ), ) @click.option( "--prompt-tokens", type=str, default=None, - help="Prompt tokens config (JSON, YAML file or key=value string)", + help="Prompt tokens configuration (JSON, YAML file, or key=value string).", ) @click.option( "--output-tokens", type=str, default=None, - help="Output tokens config (JSON, YAML file or key=value string)", + help="Output tokens configuration (JSON, YAML file, or key=value string).", ) @click.option( "--push-to-hub", is_flag=True, - help="Set this flag to push the converted dataset to the Hugging Face Hub.", + help="Push the processed dataset to Hugging Face Hub.", ) @click.option( "--hub-dataset-id", type=str, default=None, - help="The Hugging Face Hub dataset ID to push to. " - "Required if --push-to-hub is used.", + help=("Hugging Face Hub dataset ID for upload (required if --push-to-hub is set)."), ) @click.option( "--random-seed", type=int, default=42, show_default=True, - help="Random seed for prompt token sampling and output tokens sampling.", + help="Random seed for reproducible token sampling.", ) def dataset( data, @@ -710,13 +570,6 @@ def dataset( hub_dataset_id, random_seed, ): - """ - Convert and process datasets for specific prompt and output token requirements. - - Transforms datasets to meet target token length specifications using various - strategies for handling short prompts and output length adjustments. Supports - multiple input formats and can optionally push results to Hugging Face Hub. - """ process_dataset( data=data, output_path=output_path, @@ -734,71 +587,87 @@ def dataset( ) -@cli.command(help="Start the GuideLLM mock OpenAI/vLLM server for testing.") -@click.option("--host", default="127.0.0.1", help="Host to bind the server to") -@click.option("--port", default=8000, type=int, help="Port to bind the server to") -@click.option("--workers", default=1, type=int, help="Number of worker processes") +@cli.command( + "mock-server", + help=( + "Start a mock OpenAI/vLLM-compatible server for testing. " + "Simulates model inference with configurable latency and token generation." + ), +) @click.option( - "--model", default="llama-3.1-8b-instruct", help="The name of the model to mock" + "--host", + default="127.0.0.1", + help="Host address to bind the server to.", +) +@click.option( + "--port", + default=8000, + type=int, + help="Port number to bind the server to.", +) +@click.option( + "--workers", + default=1, + type=int, + help="Number of worker processes.", +) +@click.option( + "--model", + default="llama-3.1-8b-instruct", + help="Name of the model to mock.", +) +@click.option( + "--processor", + default=None, + help="Processor or tokenizer to use for requests.", ) -@click.option("--processor", default=None, help="The processor to use for requests") @click.option( "--request-latency", default=3, type=float, - help="Request latency in seconds for non-streaming requests", + help="Request latency in seconds for non-streaming requests.", ) @click.option( "--request-latency-std", default=0, type=float, - help=( - "Request latency standard deviation (normal distribution) " - "in seconds for non-streaming requests" - ), + help="Request latency standard deviation in seconds (normal distribution).", ) @click.option( "--ttft-ms", default=150, type=float, - help="Time to first token in milliseconds for streaming requests", + help="Time to first token in milliseconds for streaming requests.", ) @click.option( "--ttft-ms-std", default=0, type=float, - help=( - "Time to first token standard deviation (normal distribution) in milliseconds" - ), + help="Time to first token standard deviation in milliseconds.", ) @click.option( "--itl-ms", default=10, type=float, - help="Inter token latency in milliseconds for streaming requests", + help="Inter-token latency in milliseconds for streaming requests.", ) @click.option( "--itl-ms-std", default=0, type=float, - help=( - "Inter token latency standard deviation (normal distribution) " - "in milliseconds for streaming requests" - ), + help="Inter-token latency standard deviation in milliseconds.", ) @click.option( "--output-tokens", default=128, type=int, - help="Output tokens for streaming requests", + help="Number of output tokens for streaming requests.", ) @click.option( "--output-tokens-std", default=0, type=float, - help=( - "Output tokens standard deviation (normal distribution) for streaming requests" - ), + help="Output tokens standard deviation (normal distribution).", ) def mock_server( host: str, @@ -815,15 +684,6 @@ def mock_server( output_tokens: int, output_tokens_std: float, ): - """ - Start a GuideLLM mock OpenAI/vLLM-compatible server for testing and development. - - Launches a mock server that simulates model inference with configurable latency - characteristics, token generation patterns, and response timing. Useful for - testing GuideLLM benchmarks without requiring actual model deployment or for - development scenarios requiring predictable server behavior. - """ - config = MockServerConfig( host=host, port=port, diff --git a/src/guidellm/benchmark/__init__.py b/src/guidellm/benchmark/__init__.py index 4c7cc4a5..ef7b2900 100644 --- a/src/guidellm/benchmark/__init__.py +++ b/src/guidellm/benchmark/__init__.py @@ -1,3 +1,15 @@ +""" +Benchmark execution and performance analysis framework. + +Provides comprehensive benchmarking capabilities for LLM inference workloads, +including profile-based execution strategies, metrics collection and aggregation, +progress tracking, and multi-format output generation. Supports synchronous, +asynchronous, concurrent, sweep, and throughput-based benchmarking profiles for +evaluating model performance under various load conditions. +""" + +from __future__ import annotations + from .benchmarker import Benchmarker from .entrypoints import benchmark_generative_text, reimport_benchmarks_report from .output import ( @@ -16,10 +28,12 @@ ThroughputProfile, ) from .progress import BenchmarkerProgress, GenerativeConsoleBenchmarkerProgress +from .scenarios import get_builtin_scenarios from .schemas import ( Benchmark, - BenchmarkArgs, + BenchmarkerArgs, BenchmarkerDict, + BenchmarkGenerativeTextArgs, BenchmarkSchedulerStats, EstimatedBenchmarkState, GenerativeAudioMetricsSummary, @@ -35,9 +49,10 @@ __all__ = [ "AsyncProfile", "Benchmark", - "BenchmarkArgs", + "BenchmarkGenerativeTextArgs", "BenchmarkSchedulerStats", "Benchmarker", + "BenchmarkerArgs", "BenchmarkerDict", "BenchmarkerProgress", "ConcurrentProfile", @@ -61,7 +76,6 @@ "SynchronousProfile", "ThroughputProfile", "benchmark_generative_text", - "enable_scenarios", "get_builtin_scenarios", "reimport_benchmarks_report", ] diff --git a/src/guidellm/benchmark/benchmarker.py b/src/guidellm/benchmark/benchmarker.py index 6a5a5627..35b9cbf1 100644 --- a/src/guidellm/benchmark/benchmarker.py +++ b/src/guidellm/benchmark/benchmarker.py @@ -3,16 +3,9 @@ Provides the core benchmarking engine that coordinates request scheduling, data aggregation, and result compilation across different execution strategies -and environments. - -Classes: - Benchmarker: Abstract benchmark orchestrator for request processing workflows. - -Type Variables: - BenchmarkT: Generic benchmark result type. - RequestT: Generic request object type. - RequestTimingsT: Generic request timing object type. - ResponseT: Generic response object type. +and environments. The Benchmarker acts as the primary workflow coordinator, +managing the complete benchmark lifecycle from request submission through +result compilation while supporting thread-safe singleton operations. """ from __future__ import annotations @@ -25,7 +18,7 @@ from guidellm.benchmark.profile import Profile from guidellm.benchmark.progress import BenchmarkerProgress from guidellm.benchmark.schemas import ( - BenchmarkArgs, + BenchmarkerArgs, BenchmarkT, EstimatedBenchmarkState, ) @@ -50,12 +43,11 @@ class Benchmarker( """ Abstract benchmark orchestrator for request processing workflows. - Coordinates the execution of benchmarking runs across different scheduling + Coordinates execution of benchmarking runs across different scheduling strategies, aggregating metrics and compiling results. Manages the complete - benchmark lifecycle from request submission through result compilation. - - Implements thread-safe singleton pattern to ensure consistent state across - concurrent benchmark operations. + benchmark lifecycle from request submission through result compilation while + implementing thread-safe singleton pattern to ensure consistent state across + concurrent operations. """ async def run( @@ -74,18 +66,23 @@ async def run( """ Execute benchmark runs across multiple scheduling strategies. - Orchestrates the complete benchmark workflow: iterates through scheduling - strategies from the profile, executes requests through the scheduler, - aggregates metrics, and compiles final benchmark results. - - :param requests: Request datasets for processing across strategies. - :param backend: Backend interface for request processing. - :param profile: Benchmark profile defining strategies and constraints. - :param environment: Execution environment for coordination. - :param benchmark_aggregators: Metric aggregation functions by name. - :param benchmark_class: Class for constructing final benchmark objects. - :yield: Tuples of (metrics_update, benchmark_result, strategy, state). - :raises Exception: If benchmark execution or compilation fails. + Orchestrates the complete benchmark workflow by iterating through scheduling + strategies from the profile, executing requests through the scheduler, + aggregating metrics, and compiling final benchmark results. + + :param benchmark_class: Class for constructing final benchmark objects + :param requests: Request datasets for processing across strategies + :param backend: Backend interface for request processing + :param profile: Benchmark profile defining strategies and constraints + :param environment: Execution environment for coordination + :param progress: Optional progress tracker for benchmark lifecycle events + :param sample_requests: Number of sample requests to use for estimation + :param warmup: Optional warmup duration in seconds before benchmarking + :param cooldown: Optional cooldown duration in seconds after benchmarking + :param prefer_response_metrics: Whether to prefer response-based metrics over + request-based metrics + :yield: Compiled benchmark results for each strategy execution + :raises Exception: If benchmark execution or compilation fails """ with self.thread_lock: if progress: @@ -99,7 +96,7 @@ async def run( if progress: await progress.on_benchmark_start(strategy) - args = BenchmarkArgs( + args = BenchmarkerArgs( run_id=run_id, run_index=len(profile.completed_strategies), sample_requests=sample_requests, @@ -137,7 +134,7 @@ async def run( await progress.on_benchmark_update( estimated_state, scheduler_state ) - except Exception as err: + except Exception as err: # noqa: BLE001 logger.error( f"Error updating benchmark estimate/progress: {err}" ) diff --git a/src/guidellm/benchmark/entrypoints.py b/src/guidellm/benchmark/entrypoints.py index 61dfa680..1962f552 100644 --- a/src/guidellm/benchmark/entrypoints.py +++ b/src/guidellm/benchmark/entrypoints.py @@ -1,3 +1,15 @@ +""" +High-level entry points for executing generative text benchmarks. + +This module provides the primary interface for running generative text benchmarks +through the `benchmark_generative_text` function and re-importing existing benchmark +reports via `reimport_benchmarks_report`. It orchestrates the initialization and +coordination of backends, data loaders, profiles, and output formats to execute +comprehensive benchmarking workflows. The module handles all resolution logic for +converting user-provided arguments into fully configured components ready for +benchmarking execution. +""" + from __future__ import annotations from collections.abc import Callable @@ -5,14 +17,19 @@ from typing import Any, Literal from torch.utils.data import Sampler +from transformers import PreTrainedTokenizerBase +from typing_extensions import TypeAliasType from guidellm.backends import Backend, BackendType from guidellm.benchmark.benchmarker import Benchmarker from guidellm.benchmark.output import GenerativeBenchmarkerOutput from guidellm.benchmark.profile import Profile, ProfileType -from guidellm.benchmark.progress import BenchmarkerProgress -from guidellm.benchmark.schemas import GenerativeBenchmark, GenerativeBenchmarksReport -from guidellm.benchmark.types import OutputFormatT, ProcessorInputT +from guidellm.benchmark.progress import GenerativeConsoleBenchmarkerProgress +from guidellm.benchmark.schemas import ( + BenchmarkGenerativeTextArgs, + GenerativeBenchmark, + GenerativeBenchmarksReport, +) from guidellm.data import ( DataLoader, DatasetPreprocessor, @@ -35,12 +52,17 @@ ] -# Helper Variables - -_CURRENT_WORKING_DIR = Path.cwd() +# Helper Functions +OutputFormatT = TypeAliasType( + "OutputFormatT", + tuple[str, ...] + | list[str] + | dict[str, str | dict[str, Any] | GenerativeBenchmarkerOutput] + | None, +) -# Helper Functions +ProcessorInputT = TypeAliasType("ProcessorInputT", str | Path | PreTrainedTokenizerBase) async def resolve_backend( @@ -50,6 +72,16 @@ async def resolve_backend( console: Console | None = None, **backend_kwargs: dict[str, Any], ) -> tuple[Backend, str | None]: + """ + Initialize and validate a backend instance for benchmarking. + + :param backend: Backend type identifier or pre-configured Backend instance + :param target: Target endpoint URL or connection string for the backend + :param model: Model identifier to use with the backend, or None to use default + :param console: Console instance for progress reporting, or None + :param backend_kwargs: Additional keyword arguments passed to backend initialization + :return: Tuple of initialized Backend instance and resolved model identifier + """ console_step = ( console.print_update_step(title=f"Initializing backend {backend}") if console @@ -94,6 +126,14 @@ async def resolve_processor( model: str | None, console: Console | None = None, ) -> ProcessorInputT | None: + """ + Resolve the processor for tokenization, defaulting to model if not provided. + + :param processor: Processor identifier, path, tokenizer instance, or None + :param model: Model identifier to use as fallback processor + :param console: Console instance for progress reporting, or None + :return: Resolved processor or None if neither processor nor model provided + """ console_step = ( console.print_update_step(title=f"Resolving processor {processor}") if console @@ -137,6 +177,25 @@ async def resolve_request_loader( console: Console | None = None, **dataloader_kwargs: dict[str, Any] | None, ) -> DataLoader[GenerationRequest]: + """ + Construct a DataLoader for GenerationRequest objects from raw data inputs. + + :param data: List of data sources to load requests from + :param model: Model identifier for request formatting + :param data_args: Arguments for each data source in the data list + :param data_samples: Number of samples to draw from the dataset + :param processor: Processor for tokenization operations + :param processor_args: Arguments for processor initialization + :param data_column_mapper: Preprocessor or mapping for standardizing column names + :param data_request_formatter: Preprocessor or config for formatting requests + :param data_collator: Collation function or type for batching requests + :param data_sampler: Sampler instance or type for data sampling + :param data_num_workers: Number of worker processes for data loading + :param random_seed: Seed for reproducible random operations + :param console: Console instance for progress reporting, or None + :param dataloader_kwargs: Additional arguments passed to DataLoader initialization + :return: Configured DataLoader instance for GenerationRequest objects + """ console_step = ( console.print_update_step(title=f"Initializing request loader from {data}") if console @@ -210,6 +269,22 @@ async def resolve_profile( max_global_error_rate: float | None, console: Console | None = None, ) -> Profile: + """ + Resolve and configure a benchmark profile with rate and constraint settings. + + :param profile: Profile type identifier or pre-configured Profile instance + :param rate: Request rate(s) for the benchmark execution + :param random_seed: Seed for reproducible random operations + :param constraints: Dictionary of constraint initializers for benchmark limits + :param max_seconds: Maximum duration in seconds for the benchmark + :param max_requests: Maximum number of requests to process + :param max_errors: Maximum number of errors before stopping + :param max_error_rate: Maximum error rate threshold before stopping + :param max_global_error_rate: Maximum global error rate threshold before stopping + :param console: Console instance for progress reporting, or None + :return: Configured Profile instance ready for benchmarking + :raises ValueError: If constraints are provided with a pre-configured Profile + """ console_step = ( console.print_update_step(title=f"Resolving profile {profile}") if console @@ -253,6 +328,14 @@ async def resolve_output_formats( output_path: str | Path | None, console: Console | None = None, ) -> dict[str, GenerativeBenchmarkerOutput]: + """ + Resolve output format specifications into configured output handler instances. + + :param output_formats: Specification of desired output formats + :param output_path: Base path for output file generation, or None for default + :param console: Console instance for progress reporting, or None + :return: Dictionary mapping format names to configured output handler instances + """ console_step = ( console.print_update_step(title="Resolving output formats") if console else None ) @@ -271,120 +354,93 @@ async def resolve_output_formats( return resolved -async def benchmark_generative_text( # noqa: C901, PLR0915, PLR0912 - # Required - target: str, - data: list[Any], - # Benchmark configuration - profile: StrategyType | ProfileType | Profile = "sweep", - rate: float | list[float] | None = None, - # Backend configuration - backend: BackendType | Backend = "openai_http", - backend_kwargs: dict[str, Any] | None = None, - model: str | None = None, - # Data configuration - processor: ProcessorInputT | None = None, - processor_args: dict[str, Any] | None = None, - data_args: list[dict[str, Any]] | None = None, - data_samples: int = -1, - data_column_mapper: ( - DatasetPreprocessor | dict[str, str] | Literal["generative_column_mapper"] - ) = "generative_column_mapper", - data_request_formatter: ( - DatasetPreprocessor | dict[str, str] | str - ) = "chat_completions", - data_collator: Callable | Literal["generative"] | None = "generative", - data_sampler: Sampler[int] | Literal["shuffle"] | None = None, - data_num_workers: int | None = None, - dataloader_kwargs: dict[str, Any] | None = None, - random_seed: int = 42, - # Output configuration - output_path: str | Path | None = _CURRENT_WORKING_DIR, - output_formats: ( - tuple[str, ...] - | list[str] - | dict[str, str | dict[str, Any] | GenerativeBenchmarkerOutput] - | None - ) = ("console", "json", "html", "csv"), - # Updates configuration - progress: BenchmarkerProgress | None = None, - print_updates: bool = False, - # Benchmarker configuration - benchmark_cls: type[GenerativeBenchmark] = GenerativeBenchmark, - sample_requests: int | None = 10, - warmup: float | None = None, - cooldown: float | None = None, - # Constraints configuration - max_seconds: int | float | None = None, - max_requests: int | None = None, - max_errors: int | None = None, - max_error_rate: float | None = None, - max_global_error_rate: float | None = None, +# Main Entrypoints Functions + + +async def benchmark_generative_text( + args: BenchmarkGenerativeTextArgs, + progress: GenerativeConsoleBenchmarkerProgress | None = None, + console: Console | None = None, **constraints: dict[str, ConstraintInitializer | Any], ) -> tuple[GenerativeBenchmarksReport, dict[str, Any]]: - console = Console(quiet=not print_updates) + """ + Execute a comprehensive generative text benchmarking workflow. + + Orchestrates the full benchmarking pipeline by resolving all components (backend, + data loader, profile, outputs) from provided arguments, executing the benchmark + runs, and finalizing results in the specified output formats. + + :param args: Configuration arguments for the benchmark execution + :param progress: Progress tracker for benchmark execution, or None for no tracking + :param console: Console instance for status reporting, or None for silent operation + :param constraints: Additional constraint initializers for benchmark limits + :return: Tuple of GenerativeBenchmarksReport and dictionary of output format results + """ backend, model = await resolve_backend( - backend=backend, - target=target, - model=model, + backend=args.backend, + target=args.target, + model=args.model, console=console, - **(backend_kwargs or {}), + **(args.backend_kwargs or {}), ) processor = await resolve_processor( - processor=processor, model=model, console=console + processor=args.processor, model=model, console=console ) request_loader = await resolve_request_loader( - data=data, + data=args.data, model=model, - data_args=data_args, - data_samples=data_samples, + data_args=args.data_args, + data_samples=args.data_samples, processor=processor, - processor_args=processor_args, - data_column_mapper=data_column_mapper, - data_request_formatter=data_request_formatter, - data_collator=data_collator, - data_sampler=data_sampler, - data_num_workers=data_num_workers, - random_seed=random_seed, + processor_args=args.processor_args, + data_column_mapper=args.data_column_mapper, + data_request_formatter=args.data_request_formatter, + data_collator=args.data_collator, + data_sampler=args.data_sampler, + data_num_workers=args.data_num_workers, + random_seed=args.random_seed, console=console, - **(dataloader_kwargs or {}), + **(args.dataloader_kwargs or {}), ) profile = await resolve_profile( - profile=profile, - rate=rate, - random_seed=random_seed, + profile=args.profile, + rate=args.rate, + random_seed=args.random_seed, constraints=constraints, - max_seconds=max_seconds, - max_requests=max_requests, - max_errors=max_errors, - max_error_rate=max_error_rate, - max_global_error_rate=max_global_error_rate, + max_seconds=args.max_seconds, + max_requests=args.max_requests, + max_errors=args.max_errors, + max_error_rate=args.max_error_rate, + max_global_error_rate=args.max_global_error_rate, console=console, ) output_formats = await resolve_output_formats( - output_formats=output_formats, output_path=output_path, console=console + output_formats=args.output_formats, + output_path=args.output_path, + console=console, ) - report = GenerativeBenchmarksReport() - console.print_update( - title="Setup complete, starting benchmarks...", status="success" - ) - console.print("\n\n") + report = GenerativeBenchmarksReport(args=args) + if console: + console.print_update( + title="Setup complete, starting benchmarks...", status="success" + ) + console.print("\n\n") benchmarker: Benchmarker[ GenerativeBenchmark, GenerationRequest, GenerationResponse ] = Benchmarker() async for benchmark in benchmarker.run( - benchmark_class=benchmark_cls, + benchmark_class=args.benchmark_cls, requests=request_loader, backend=backend, profile=profile, environment=NonDistributedEnvironment(), progress=progress, - sample_requests=sample_requests, - warmup=warmup, - cooldown=cooldown, - prefer_response_metrics=True, + sample_requests=args.sample_requests, + warmup=args.warmup, + cooldown=args.cooldown, + prefer_response_metrics=args.prefer_response_metrics, ): if benchmark: report.benchmarks.append(benchmark) @@ -394,13 +450,17 @@ async def benchmark_generative_text( # noqa: C901, PLR0915, PLR0912 output_result = await output.finalize(report) output_format_results[key] = output_result - console.print("\n\n") - console.print_update( - title=f"Benchmarking complete, generated {len(report.benchmarks)} benchmark(s)", - status="success", - ) - for key, value in output_format_results.items(): - console.print_update(title=f" {key:<8}: {value}", status="debug") + if console: + console.print("\n\n") + console.print_update( + title=( + "Benchmarking complete, generated " + f"{len(report.benchmarks)} benchmark(s)" + ), + status="success", + ) + for key, value in output_format_results.items(): + console.print_update(title=f" {key:<8}: {value}", status="debug") return report, output_format_results @@ -411,9 +471,12 @@ async def reimport_benchmarks_report( output_formats: OutputFormatT = ("console", "json", "html", "csv"), ) -> tuple[GenerativeBenchmarksReport, dict[str, Any]]: """ - The command-line entry point for re-importing and displaying an - existing benchmarks report. Can also specify an output format. - Assumes the file provided exists. + Load and re-export an existing benchmarks report in specified formats. + + :param file: Path to the existing benchmark report file to load + :param output_path: Base path for output file generation, or None for default + :param output_formats: Specification of desired output formats for the report + :return: Tuple of loaded GenerativeBenchmarksReport and dictionary of output results """ console = Console() diff --git a/src/guidellm/benchmark/profile.py b/src/guidellm/benchmark/profile.py index 8564afde..4b3f36fd 100644 --- a/src/guidellm/benchmark/profile.py +++ b/src/guidellm/benchmark/profile.py @@ -1,32 +1,17 @@ """ -Benchmarking profile configurations for coordinating multi-strategy execution. - -Provides configurable profile abstractions for orchestrating sequential and -parallel execution of different scheduling strategies during benchmarking, -with automatic strategy generation and constraint management. - -Classes: - Profile: Abstract base for multi-strategy benchmarking profiles. - SynchronousProfile: Single synchronous strategy execution profile. - ConcurrentProfile: Fixed-concurrency strategy execution profile. - ThroughputProfile: Maximum throughput strategy execution profile. - AsyncProfile: Rate-based asynchronous strategy execution profile. - SweepProfile: Adaptive multi-strategy sweep execution profile. - -Type Aliases: - ProfileType: Literal type for supported profile configurations. +Profile configurations for orchestrating multi-strategy benchmark execution. + +Provides configurable abstractions for coordinating sequential execution of +scheduling strategies during benchmarking workflows. Profiles automatically +generate strategies based on configuration parameters, manage runtime +constraints, and track completion state across the execution sequence. """ from __future__ import annotations from abc import ABC, abstractmethod from collections.abc import Generator -from typing import ( - TYPE_CHECKING, - Any, - ClassVar, - Literal, -) +from typing import TYPE_CHECKING, Any, ClassVar, Literal import numpy as np from pydantic import ( @@ -75,11 +60,14 @@ class Profile( ABC, ): """ - Abstract base for multi-strategy benchmarking execution profiles. + Abstract base for coordinating multi-strategy benchmark execution. - Coordinates sequential execution of scheduling strategies with automatic - strategy generation, constraint management, and completion tracking for - comprehensive benchmarking workflows. + Manages sequential execution of scheduling strategies with automatic strategy + generation, constraint management, and completion tracking. Subclasses define + specific execution patterns like synchronous, concurrent, throughput-focused, + rate-based async, or adaptive sweep profiles. + + :cvar schema_discriminator: Field name used for polymorphic deserialization """ schema_discriminator: ClassVar[str] = "type_" @@ -100,14 +88,14 @@ def create( **kwargs: Any, ) -> Profile: """ - Create a profile instance based on the specified type. + Factory method to create a profile instance based on type. - :param rate_type: The type of profile to create. - :param rate: Rate parameter for profile configuration. - :param random_seed: Random seed for stochastic strategies. - :param kwargs: Additional arguments for profile configuration. - :return: Configured profile instance for the specified type. - :raises ValueError: If the profile type is not registered. + :param rate_type: Profile type identifier to instantiate + :param rate: Rate configuration for the profile strategy + :param random_seed: Seed for stochastic strategy reproducibility + :param kwargs: Additional profile-specific configuration parameters + :return: Configured profile instance for the specified type + :raises ValueError: If rate_type is not registered """ profile_class: type[Profile] = cls.get_registered_object(rate_type) resolved_kwargs = profile_class.resolve_args( @@ -128,33 +116,31 @@ def resolve_args( """ Resolve and validate arguments for profile construction. - :param rate_type: The type of the profile. - :param rate: Rate parameter for configuration. - :param random_seed: Random seed for stochastic strategies. - :param kwargs: Additional arguments to resolve. - :return: Dictionary of resolved arguments for profile construction. + :param rate_type: Profile type identifier + :param rate: Rate configuration parameter + :param random_seed: Seed for stochastic strategies + :param kwargs: Additional arguments to resolve and validate + :return: Resolved arguments dictionary for profile initialization """ ... type_: Literal["profile"] = Field( - description="The type of benchmarking profile to use", + description="Profile type discriminator for polymorphic serialization", ) completed_strategies: list[SchedulingStrategy] = Field( default_factory=list, - description="The strategies that have completed execution", + description="Strategies that have completed execution in this profile", ) constraints: dict[str, Any | dict[str, Any] | ConstraintInitializer] | None = Field( default=None, - description="Runtime constraints to apply during strategy execution", + description="Runtime constraints applied to strategy execution", ) @computed_field # type: ignore[misc] @property def strategy_types(self) -> list[StrategyType]: """ - :return: List of all strategy types expected to be executed or have been - executed in this profile. By default, this returns just the - completed strategies. + :return: Strategy types executed or expected to execute in this profile """ return [strat.type_ for strat in self.completed_strategies] @@ -169,10 +155,10 @@ def strategies_generator( None, ]: """ - Generate strategies and constraints for sequential profile execution. + Generate strategies and constraints for sequential execution. - :return: Generator yielding (strategy, constraints) tuples and - receiving benchmark results from each execution. + :return: Generator yielding (strategy, constraints) tuples and receiving + benchmark results after each execution """ prev_strategy: SchedulingStrategy | None = None prev_benchmark: Benchmark | None = None @@ -197,11 +183,11 @@ def next_strategy( prev_benchmark: Benchmark | None, ) -> SchedulingStrategy | None: """ - Generate the next strategy to execute in the profile sequence. + Generate the next strategy in the profile execution sequence. - :param prev_strategy: The previously completed strategy. - :param prev_benchmark: Benchmark results from the previous strategy. - :return: Next strategy to execute, or None if profile is complete. + :param prev_strategy: Previously completed strategy instance + :param prev_benchmark: Benchmark results from previous strategy execution + :return: Next strategy to execute, or None if profile complete """ ... @@ -214,10 +200,10 @@ def next_strategy_constraints( """ Generate constraints for the next strategy execution. - :param next_strategy: The next strategy to be executed. - :param prev_strategy: The previously completed strategy. - :param prev_benchmark: Benchmark results from the previous strategy. - :return: Constraints dictionary for the next strategy, or None. + :param next_strategy: Strategy to be executed next + :param prev_strategy: Previously completed strategy instance + :param prev_benchmark: Benchmark results from previous strategy execution + :return: Constraints dictionary for next strategy, or None """ _ = (prev_strategy, prev_benchmark) # unused return ( @@ -281,12 +267,12 @@ def resolve_args( """ Resolve arguments for synchronous profile construction. - :param rate_type: The type/strategy of the profile (ignored). - :param rate: Rate parameter (must be None, will be stripped). - :param random_seed: Random seed (ignored and stripped). - :param kwargs: Additional arguments to pass through. - :return: Dictionary of resolved arguments. - :raises ValueError: If rate is not None. + :param rate_type: Profile type identifier (ignored) + :param rate: Rate parameter (must be None) + :param random_seed: Random seed (ignored) + :param kwargs: Additional arguments passed through unchanged + :return: Resolved arguments dictionary + :raises ValueError: If rate is not None """ _ = (rate_type, random_seed) # unused if rate is not None: @@ -297,7 +283,7 @@ def resolve_args( @property def strategy_types(self) -> list[StrategyType]: """ - :return: The single synchronous strategy type. + :return: Single synchronous strategy type """ return [self.type_] @@ -309,9 +295,9 @@ def next_strategy( """ Generate synchronous strategy or None if already completed. - :param prev_strategy: The previously completed strategy (unused). - :param prev_benchmark: Benchmark results from the previous strategy (unused). - :return: SynchronousStrategy for the first execution, None afterward. + :param prev_strategy: Previously completed strategy (unused) + :param prev_benchmark: Benchmark results from previous execution (unused) + :return: SynchronousStrategy for first execution, None afterward """ _ = (prev_strategy, prev_benchmark) # unused if len(self.completed_strategies) >= 1: @@ -326,7 +312,7 @@ class ConcurrentProfile(Profile): type_: Literal["concurrent"] = "concurrent" # type: ignore[assignment] streams: list[PositiveInt] = Field( - description="Number of concurrent streams for request scheduling", + description="Concurrent stream counts for request scheduling", ) startup_duration: NonNegativeFloat = Field( default=0.0, @@ -347,20 +333,23 @@ def resolve_args( """ Resolve arguments for concurrent profile construction. - :param rate_type: The type/strategy of the profile (ignored). - :param rate: Rate parameter, remapped to streams. - :param random_seed: Random seed (ignored and stripped). - :param kwargs: Additional arguments to pass through. - :return: Dictionary of resolved arguments. - :raises ValueError: If rate is None. + :param rate_type: Profile type identifier (ignored) + :param rate: Rate parameter remapped to streams + :param random_seed: Random seed (ignored) + :param kwargs: Additional arguments passed through unchanged + :return: Resolved arguments dictionary + :raises ValueError: If rate is None """ _ = (rate_type, random_seed) # unused - kwargs["streams"] = [int(r) for r in rate] if rate else None + rate = rate if isinstance(rate, list) or rate is None else [rate] + kwargs["streams"] = [int(stream) for stream in rate] if rate else None return kwargs @property def strategy_types(self) -> list[StrategyType]: - """Get concurrent strategy types for each configured stream count.""" + """ + :return: Concurrent strategy types for each configured stream count + """ return [self.type_] * len(self.streams) def next_strategy( @@ -371,9 +360,9 @@ def next_strategy( """ Generate concurrent strategy for the next stream count. - :param prev_strategy: The previously completed strategy (unused). - :param prev_benchmark: Benchmark results from the previous strategy (unused). - :return: ConcurrentStrategy with next stream count, or None if complete. + :param prev_strategy: Previously completed strategy (unused) + :param prev_benchmark: Benchmark results from previous execution (unused) + :return: ConcurrentStrategy with next stream count, or None if complete """ _ = (prev_strategy, prev_benchmark) # unused @@ -395,7 +384,7 @@ class ThroughputProfile(Profile): type_: Literal["throughput"] = "throughput" # type: ignore[assignment] max_concurrency: PositiveInt | None = Field( default=None, - description="Maximum number of concurrent requests to schedule", + description="Maximum concurrent requests to schedule", ) startup_duration: NonNegativeFloat = Field( default=0.0, @@ -416,11 +405,11 @@ def resolve_args( """ Resolve arguments for throughput profile construction. - :param rate_type: The type/strategy of the profile (ignored). - :param rate: Rate parameter to remap to max_concurrency. - :param random_seed: Random seed (ignored and stripped). - :param kwargs: Additional arguments to pass through. - :return: Dictionary of resolved arguments. + :param rate_type: Profile type identifier (ignored) + :param rate: Rate parameter remapped to max_concurrency + :param random_seed: Random seed (ignored) + :param kwargs: Additional arguments passed through unchanged + :return: Resolved arguments dictionary """ _ = (rate_type, random_seed) # unused # Remap rate to max_concurrency, strip out random_seed @@ -431,7 +420,9 @@ def resolve_args( @property def strategy_types(self) -> list[StrategyType]: - """Get the single throughput strategy type.""" + """ + :return: Single throughput strategy type + """ return [self.type_] def next_strategy( @@ -442,9 +433,9 @@ def next_strategy( """ Generate throughput strategy or None if already completed. - :param prev_strategy: The previously completed strategy (unused). - :param prev_benchmark: Benchmark results from the previous strategy (unused). - :return: ThroughputStrategy for the first execution, None afterward. + :param prev_strategy: Previously completed strategy (unused) + :param prev_benchmark: Benchmark results from previous execution (unused) + :return: ThroughputStrategy for first execution, None afterward """ _ = (prev_strategy, prev_benchmark) # unused if len(self.completed_strategies) >= 1: @@ -458,13 +449,11 @@ def next_strategy( @Profile.register(["async", "constant", "poisson"]) class AsyncProfile(Profile): - """ - Rate-based asynchronous strategy execution profile with configurable patterns. - """ + """Rate-based asynchronous strategy execution profile with configurable patterns.""" type_: Literal["async", "constant", "poisson"] = "async" # type: ignore[assignment] strategy_type: Literal["constant", "poisson"] = Field( - description="Type of asynchronous strategy pattern to use", + description="Asynchronous strategy pattern type to use", ) rate: list[PositiveFloat] = Field( description="Request scheduling rate in requests per second", @@ -478,7 +467,7 @@ class AsyncProfile(Profile): ) max_concurrency: PositiveInt | None = Field( default=None, - description="Maximum number of concurrent requests to schedule", + description="Maximum concurrent requests to schedule", ) random_seed: int = Field( default=42, @@ -496,12 +485,12 @@ def resolve_args( """ Resolve arguments for async profile construction. - :param rate_type: The type/strategy of the profile. - :param rate: Rate parameter for the profile. - :param random_seed: Random seed for stochastic strategies. - :param kwargs: Additional arguments to pass through. - :return: Dictionary of resolved arguments. - :raises ValueError: If rate is None. + :param rate_type: Profile type identifier + :param rate: Rate configuration for the profile + :param random_seed: Seed for stochastic strategies + :param kwargs: Additional arguments passed through unchanged + :return: Resolved arguments dictionary + :raises ValueError: If rate is None """ if rate is None: raise ValueError("AsyncProfile requires a rate parameter") @@ -516,13 +505,15 @@ def resolve_args( if rate_type in ["constant", "poisson"] else kwargs.get("strategy_type", "constant") ) - kwargs["rate"] = rate + kwargs["rate"] = rate if isinstance(rate, list) else [rate] kwargs["random_seed"] = random_seed return kwargs @property def strategy_types(self) -> list[StrategyType]: - """Get async strategy types for each configured rate.""" + """ + :return: Async strategy types for each configured rate + """ num_strategies = len(self.rate) return [self.strategy_type] * num_strategies @@ -534,11 +525,11 @@ def next_strategy( """ Generate async strategy for the next configured rate. - :param prev_strategy: The previously completed strategy (unused). - :param prev_benchmark: Benchmark results from the previous strategy (unused). + :param prev_strategy: Previously completed strategy (unused) + :param prev_benchmark: Benchmark results from previous execution (unused) :return: AsyncConstantStrategy or AsyncPoissonStrategy for next rate, - or None if all rates completed. - :raises ValueError: If strategy_type is neither 'constant' nor 'poisson'. + or None if all rates completed + :raises ValueError: If strategy_type is neither 'constant' nor 'poisson' """ _ = (prev_strategy, prev_benchmark) # unused @@ -566,9 +557,7 @@ def next_strategy( @Profile.register("sweep") class SweepProfile(Profile): - """ - Adaptive multi-strategy sweep execution profile with rate discovery. - """ + """Adaptive multi-strategy sweep execution profile with rate discovery.""" type_: Literal["sweep"] = "sweep" # type: ignore[assignment] sweep_size: int = Field( @@ -585,7 +574,7 @@ class SweepProfile(Profile): ) max_concurrency: PositiveInt | None = Field( default=None, - description="Maximum number of concurrent requests to schedule", + description="Maximum concurrent requests to schedule", ) random_seed: int = Field( default=42, @@ -605,7 +594,7 @@ class SweepProfile(Profile): ) measured_rates: list[float] = Field( default_factory=list, - description="Calculated interpolated rates between synchronous and throughput", + description="Interpolated rates between synchronous and throughput", ) @classmethod @@ -619,11 +608,11 @@ def resolve_args( """ Resolve arguments for sweep profile construction. - :param rate_type: The type/strategy for async strategies in the sweep. - :param rate: Rate parameter (ignored for sweep). - :param random_seed: Random seed for stochastic strategies. - :param kwargs: Additional arguments to pass through. - :return: Dictionary of resolved arguments. + :param rate_type: Async strategy type for sweep execution + :param rate: Rate parameter specifying sweep size (if provided) + :param random_seed: Seed for stochastic strategies + :param kwargs: Additional arguments passed through unchanged + :return: Resolved arguments dictionary """ sweep_size_from_rate = int(rate[0]) if rate else settings.default_sweep_number kwargs["sweep_size"] = kwargs.get("sweep_size", sweep_size_from_rate) @@ -634,7 +623,9 @@ def resolve_args( @property def strategy_types(self) -> list[StrategyType]: - """Get strategy types for the complete sweep sequence.""" + """ + :return: Strategy types for the complete sweep sequence + """ types = ["synchronous", "throughput"] types += [self.strategy_type] * (self.sweep_size - len(types)) return types @@ -653,13 +644,13 @@ def next_strategy( """ Generate the next strategy in the adaptive sweep sequence. - Executes synchronous and throughput strategies first to measure - baseline rates, then generates interpolated rates for async strategies. + Executes synchronous and throughput strategies first to measure baseline + rates, then generates interpolated rates for async strategies. - :param prev_strategy: The previously completed strategy. - :param prev_benchmark: Benchmark results from the previous strategy. - :return: Next strategy in sweep sequence, or None if complete. - :raises ValueError: If strategy_type is neither 'constant' nor 'poisson'. + :param prev_strategy: Previously completed strategy instance + :param prev_benchmark: Benchmark results from previous strategy execution + :return: Next strategy in sweep sequence, or None if complete + :raises ValueError: If strategy_type is neither 'constant' nor 'poisson' """ if prev_strategy is None: return SynchronousStrategy() diff --git a/src/guidellm/benchmark/scenario.py b/src/guidellm/benchmark/scenario.py deleted file mode 100644 index 59cdef27..00000000 --- a/src/guidellm/benchmark/scenario.py +++ /dev/null @@ -1,169 +0,0 @@ -from __future__ import annotations - -import json -from collections.abc import Callable -from functools import cache, wraps -from inspect import Parameter, signature -from pathlib import Path -from typing import Annotated, Any, Literal, TypeVar - -import yaml -from loguru import logger -from pydantic import BeforeValidator, Field, PositiveFloat, PositiveInt - -from guidellm.backends import Backend, BackendType -from guidellm.benchmark.profile import Profile, ProfileType -from guidellm.benchmark.types import ProcessorInputT -from guidellm.scheduler import StrategyType -from guidellm.utils import StandardBaseModel - -__all__ = [ - "GenerativeTextScenario", - "Scenario", - "enable_scenarios", - "get_builtin_scenarios", -] - -SCENARIO_DIR = Path(__file__).parent / "scenarios/" - - -@cache -def get_builtin_scenarios() -> list[str]: - """Returns list of builtin scenario names.""" - return [p.stem for p in SCENARIO_DIR.glob("*.json")] - - -def parse_float_list(value: str | float | list[float]) -> list[float]: - """ - Parse a comma separated string to a list of float - or convert single float list of one or pass float - list through. - """ - if isinstance(value, int | float): - return [value] - elif isinstance(value, list): - return value - - values = value.split(",") if "," in value else [value] - - try: - return [float(val) for val in values] - except ValueError as err: - raise ValueError( - "must be a number or comma-separated list of numbers." - ) from err - - -T = TypeVar("T", bound="Scenario") - - -class Scenario(StandardBaseModel): - """ - Parent Scenario class with common options for all benchmarking types. - """ - - target: str - - @classmethod - def get_default(cls: type[T], field: str) -> Any: - """Get default values for model fields""" - return cls.model_fields[field].default - - @classmethod - def from_file(cls: type[T], filename: Path, overrides: dict | None = None) -> T: - """ - Attempt to create a new instance of the model using - data loaded from json or yaml file. - """ - try: - with filename.open() as f: - if str(filename).endswith(".json"): - data = json.load(f) - else: # Assume everything else is yaml - data = yaml.safe_load(f) - except (json.JSONDecodeError, yaml.YAMLError) as e: - logger.error(f"Failed to parse {filename} as type {cls.__name__}") - raise ValueError(f"Error when parsing file: {filename}") from e - - data.update(overrides or {}) - return cls.model_validate(data) - - @classmethod - def from_builtin(cls: type[T], name: str, overrides: dict | None = None) -> T: - filename = SCENARIO_DIR / f"{name}.json" - - if not filename.is_file(): - raise ValueError(f"{name} is not a valid builtin scenario") - - return cls.from_file(filename, overrides) - - -class GenerativeTextScenario(Scenario): - """ - Scenario class for generative text benchmarks. - """ - - class Config: - # NOTE: This prevents errors due to unvalidatable - # types like PreTrainedTokenizerBase - arbitrary_types_allowed = True - - data: Any - profile: StrategyType | ProfileType | Profile - rate: Annotated[list[PositiveFloat] | None, BeforeValidator(parse_float_list)] = ( - None - ) - random_seed: int = 42 - # Backend configuration - backend: BackendType | Backend = "openai_http" - backend_kwargs: dict[str, Any] | None = None - model: str | None = None - # Data configuration - processor: ProcessorInputT | None = None - processor_args: dict[str, Any] | None = None - data_args: dict[str, Any] | None = None - data_sampler: Literal["random"] | None = None - # Aggregators configuration - warmup: Annotated[float | None, Field(gt=0, le=1)] = None - cooldown: Annotated[float | None, Field(gt=0, le=1)] = None - request_samples: PositiveInt | None = 20 - # Constraints configuration - max_seconds: PositiveFloat | PositiveInt | None = None - max_requests: PositiveInt | None = None - max_errors: PositiveInt | None = None - max_error_rate: PositiveFloat | None = None - max_global_error_rate: PositiveFloat | None = None - - -# Decorator function to apply scenario to a function -def enable_scenarios(func: Callable) -> Any: - @wraps(func) - async def decorator(*args, scenario: Scenario | None = None, **kwargs) -> Any: - if scenario is not None: - kwargs.update(scenario.model_dump()) - return await func(*args, **kwargs) - - # Modify the signature of the decorator to include the `scenario` argument - sig = signature(func) - params = list(sig.parameters.values()) - # Place `scenario` before `**kwargs` or any parameter with a default value - loc = next( - ( - i - for i, p in enumerate(params) - if p.kind is Parameter.VAR_KEYWORD or p.default is not Parameter.empty - ), - len(params), - ) - params.insert( - loc, - Parameter( - "scenario", - Parameter.POSITIONAL_OR_KEYWORD, - default=None, - annotation=Scenario | None, - ), - ) - decorator.__signature__ = sig.replace(parameters=params) # type: ignore [attr-defined] - - return decorator diff --git a/src/guidellm/benchmark/scenarios/__init__.py b/src/guidellm/benchmark/scenarios/__init__.py index e69de29b..030f9bbd 100644 --- a/src/guidellm/benchmark/scenarios/__init__.py +++ b/src/guidellm/benchmark/scenarios/__init__.py @@ -0,0 +1,40 @@ +""" +Builtin benchmark scenario definitions and discovery utilities. + +This module provides access to predefined benchmark scenarios stored as JSON files +within the scenarios directory. It enables discovery and retrieval of builtin +scenarios by name or filename, supporting both stem names (without extension) and +full filenames for flexible scenario loading. +""" + +from __future__ import annotations + +from functools import cache +from pathlib import Path +from typing import Annotated + +__all__ = ["SCENARIO_DIR", "get_builtin_scenarios"] + +SCENARIO_DIR: Annotated[ + Path, + "Directory path containing builtin scenario JSON files", +] = Path(__file__).parent + + +@cache +def get_builtin_scenarios() -> dict[str, Path]: + """ + Retrieve all builtin scenario definitions from the scenarios directory. + + Scans the scenarios directory for JSON files and returns a mapping of scenario + names to their file paths. Each scenario is indexed by both its stem name + (filename without extension) and full filename for convenient lookup. + + :return: Dictionary mapping scenario names and filenames to their Path objects + """ + builtin = {} + for path in SCENARIO_DIR.glob("*.json"): + builtin[path.stem] = path + builtin[path.name] = path + + return builtin diff --git a/src/guidellm/benchmark/scenarios/chat.json b/src/guidellm/benchmark/scenarios/chat.json index 7ed4ce16..58fd18e2 100644 --- a/src/guidellm/benchmark/scenarios/chat.json +++ b/src/guidellm/benchmark/scenarios/chat.json @@ -1,4 +1,6 @@ { "profile": "sweep", - "data": "prompt_tokens=512,prompt_tokens_stdev=128,prompt_tokens_min=1,prompt_tokens_max=1024,output_tokens=256,output_tokens_stdev=64,output_tokens_min=1,output_tokens_max=1024" -} + "data": [ + "prompt_tokens=512,prompt_tokens_stdev=128,prompt_tokens_min=1,prompt_tokens_max=1024,output_tokens=256,output_tokens_stdev=64,output_tokens_min=1,output_tokens_max=1024" + ] +} \ No newline at end of file diff --git a/src/guidellm/benchmark/scenarios/rag.json b/src/guidellm/benchmark/scenarios/rag.json index d790ce60..ea38d76e 100644 --- a/src/guidellm/benchmark/scenarios/rag.json +++ b/src/guidellm/benchmark/scenarios/rag.json @@ -1,4 +1,6 @@ { "profile": "sweep", - "data": "prompt_tokens=4096,prompt_tokens_stdev=512,prompt_tokens_min=2048,prompt_tokens_max=6144,output_tokens=512,output_tokens_stdev=128,output_tokens_min=1,output_tokens_max=1024" -} + "data": [ + "prompt_tokens=4096,prompt_tokens_stdev=512,prompt_tokens_min=2048,prompt_tokens_max=6144,output_tokens=512,output_tokens_stdev=128,output_tokens_min=1,output_tokens_max=1024" + ] +} \ No newline at end of file diff --git a/src/guidellm/benchmark/schemas.py b/src/guidellm/benchmark/schemas.py index 2f2d8f98..9fd09461 100644 --- a/src/guidellm/benchmark/schemas.py +++ b/src/guidellm/benchmark/schemas.py @@ -1,24 +1,13 @@ """ -Benchmark data models and metrics for performance measurement and analysis. +Benchmark data models and metrics for generative AI performance measurement. Provides comprehensive data structures for capturing, storing, and analyzing -benchmark results from scheduler executions. Includes timing measurements, -token statistics, and performance metrics for generative AI workloads. - -Classes: - BenchmarkSchedulerStats: Scheduler timing and performance statistics. - BenchmarkMetrics: Core benchmark metrics and distributions. - BenchmarkRequestStats: Individual request processing statistics. - Benchmark: Base benchmark result container with generic metrics. - GenerativeRequestStats: Request statistics for generative AI workloads. - GenerativeMetrics: Comprehensive metrics for generative benchmarks. - GenerativeBenchmark: Complete generative benchmark results and analysis. - GenerativeBenchmarksReport: Container for multiple benchmark results. - -Type Variables: - BenchmarkMetricsT: Generic benchmark metrics type. - BenchmarkRequestStatsT: Generic request statistics type. - BenchmarkT: Generic benchmark container type. +benchmark results from scheduler-driven generative AI workload executions. +Core abstractions include base benchmark interfaces, generative-specific +metrics with token/latency distributions, request-level statistics tracking, +and multi-benchmark reporting capabilities. These models enable detailed +performance analysis including throughput, latency, concurrency patterns, and +domain-specific metrics for text, image, video, and audio generation tasks. """ from __future__ import annotations @@ -28,27 +17,33 @@ import time import uuid from abc import ABC, abstractmethod -from collections.abc import Iterable +from collections.abc import Callable, Iterable from pathlib import Path from typing import Any, ClassVar, Literal, TypeVar, cast import yaml -from pydantic import Field, computed_field - -from guidellm.benchmark.profile import Profile +from pydantic import ConfigDict, Field, computed_field, model_serializer +from torch.utils.data import Sampler +from transformers import PreTrainedTokenizerBase + +from guidellm.backends import Backend, BackendType +from guidellm.benchmark.profile import Profile, ProfileType +from guidellm.benchmark.scenarios import get_builtin_scenarios +from guidellm.data import DatasetPreprocessor from guidellm.scheduler import ( BackendInterface, Environment, SchedulerState, SchedulingStrategy, + StrategyType, ) from guidellm.schemas import ( GenerationRequest, GenerationResponse, GenerativeRequestStats, RequestInfo, + UsageMetrics, ) -from guidellm.schemas.request import UsageMetrics from guidellm.utils import ( InfoMixin, StandardBaseDict, @@ -59,9 +54,10 @@ __all__ = [ "Benchmark", - "BenchmarkArgs", + "BenchmarkGenerativeTextArgs", "BenchmarkSchedulerStats", "BenchmarkT", + "BenchmarkerArgs", "BenchmarkerDict", "EstimatedBenchmarkState", "GenerativeAudioMetricsSummary", @@ -77,6 +73,19 @@ class EstimatedBenchmarkState(dict[str, Any]): + """ + Accumulator for real-time benchmark metrics during scheduler execution. + + Tracks incremental metrics, running averages, and time-based statistics as + requests are processed. Maintains grouped metrics for benchmark state, + benchmark-level metrics, and scheduler-level metrics with support for + average, rate, and time-averaged metric calculations. + + :cvar benchmark_state_group: Metric group key for benchmark state tracking + :cvar benchmark_metrics_group: Metric group key for benchmark-level metrics + :cvar scheduler_state_group: Metric group key for scheduler-level metrics + """ + benchmark_state_group: ClassVar[Literal["benchmark_state"]] = "benchmark_state" benchmark_metrics_group: ClassVar[Literal["benchmark_metrics"]] = ( "benchmark_metrics" @@ -89,6 +98,14 @@ def get_metric( key: str, default: int | float | None = None, ) -> int | float | None: + """ + Retrieve a grouped metric value by group and key. + + :param group: Metric group identifier + :param key: Metric key within the group + :param default: Value returned if metric doesn't exist + :return: The metric value or default if not found + """ return self.get(f"{group}_{key}", default) def set_metric( @@ -98,6 +115,15 @@ def set_metric( value: bool | int | float | None, start_val: bool | int | float | None = None, ) -> bool | int | float | None: + """ + Set a grouped metric value, optionally adjusting by a starting value. + + :param group: Metric group identifier + :param key: Metric key within the group + :param value: Metric value to set + :param start_val: Optional starting value to subtract from the metric value + :return: The adjusted metric value or None if value is None + """ if value is None: return None @@ -115,6 +141,15 @@ def add_avg_metric( start_val: bool | int | float | None = 0.0, count: int | None = 1, ): + """ + Add a value to a running average metric calculation. + + :param group: Metric group identifier + :param key: Metric key within the group + :param value: Value to add to the average + :param start_val: Optional starting value to subtract before adding + :param count: Number of observations this value represents + """ if value is None or count is None: return @@ -143,6 +178,17 @@ def add_avg_rate_metric( end_time: float | None = None, numerator_type: Literal["avg", "total", "count"] = "total", ): + """ + Add a value to a rate-based average metric calculation. + + :param group: Metric group identifier + :param key: Metric key within the group + :param value: Value to add to the average + :param start_val: Optional starting value to subtract before adding + :param start_time: Start time for rate calculation, defaults to current time + :param end_time: End time for rate calculation, defaults to current time + :param numerator_type: Type of numerator for rate calculation + """ if value is None: return @@ -183,6 +229,14 @@ def add_time_averaged_metric( value: bool | int | float | None, recorded_time: float | None = None, ): + """ + Add a value to a time-weighted average metric calculation. + + :param group: Metric group identifier + :param key: Metric key within the group + :param value: Value to add to the time-weighted average + :param recorded_time: Time of the observation, defaults to current time + """ if value is None: return @@ -218,7 +272,16 @@ def add_time_averaged_metric( ) -class BenchmarkArgs(StandardBaseDict): +class BenchmarkerArgs(StandardBaseDict): + """ + Configuration parameters for benchmark execution and request sampling. + + Defines run identification, request sampling strategy, warmup/cooldown phases, + and metric preferences for benchmark executions. Provides methods to determine + whether a request falls within warmup or cooldown periods based on time, + request count, or percentage-based thresholds. + """ + run_id: str = Field( default_factory=lambda: str(uuid.uuid4()), description="Unique identifier for the benchmark run", @@ -226,7 +289,9 @@ class BenchmarkArgs(StandardBaseDict): run_index: int = Field(default=0, description="Index of the benchmark run") sample_requests: int | None = Field( default=20, - description="Number of requests to sample and keep in the final benchmark for metrics", + description=( + "Number of requests to sample and keep in the final benchmark for metrics" + ), ) warmup: int | float | None = Field( default=None, description="Warmup time before benchmarking starts" @@ -242,6 +307,13 @@ class BenchmarkArgs(StandardBaseDict): def is_in_warmup( self, request_info: RequestInfo, scheduler_state: SchedulerState ) -> bool: + """ + Check if a request is in the warmup phase. + + :param request_info: Information about the current request + :param scheduler_state: Current state of the scheduler + :return: True if the request is in warmup phase, False otherwise + """ if self.warmup is not None and 0 < self.warmup < 1: # Percentage-based warmup return ( @@ -265,6 +337,13 @@ def is_in_warmup( def is_in_cooldown( self, request_info: RequestInfo, scheduler_state: SchedulerState ) -> bool: + """ + Check if a request is in the cooldown phase. + + :param request_info: Information about the current request + :param scheduler_state: Current state of the scheduler + :return: True if the request is in cooldown phase, False otherwise + """ if self.cooldown is not None and 0 < self.cooldown < 1: # Percentage-based cooldown return ( @@ -293,10 +372,24 @@ def is_in_cooldown( class Benchmark(ABC): + """ + Abstract base interface for benchmark result implementations. + + Defines the contract for benchmark classes to provide run metrics sampling, + request metrics sampling, real-time estimate updates, and final compilation + of benchmark results from scheduler execution data. + """ + @abstractmethod def get_run_metrics_sample( self, - ) -> dict[Literal["start_time", "end_time", "duration"], float]: ... + ) -> dict[Literal["start_time", "end_time", "duration"], float]: + """ + Get a sample of run-level timing metrics. + + :return: Dictionary containing start_time, end_time, and duration metrics + """ + ... @abstractmethod def get_request_metrics_sample( @@ -309,25 +402,43 @@ def get_request_metrics_sample( "request_concurrency", ], float, - ]: ... + ]: + """ + Get a sample of request-level performance metrics. + + :return: Dictionary containing request count, latency, throughput, and + concurrency metrics + """ + ... @classmethod @abstractmethod def update_estimate( cls, - args: BenchmarkArgs, + args: BenchmarkerArgs, state: EstimatedBenchmarkState, response: Any, request: Any, request_info: RequestInfo, scheduler_state: SchedulerState, - ): ... + ): + """ + Update real-time benchmark estimates with new request data. + + :param args: Benchmark configuration arguments + :param state: Current estimated benchmark state to update + :param response: Response received from the backend + :param request: Original request sent to the backend + :param request_info: Metadata about the request execution + :param scheduler_state: Current state of the scheduler + """ + ... @classmethod @abstractmethod def compile( cls, - args: BenchmarkArgs, + args: BenchmarkerArgs, estimated_state: EstimatedBenchmarkState, scheduler_state: SchedulerState, profile: Profile, @@ -336,7 +447,22 @@ def compile( environment: Environment, strategy: SchedulingStrategy, constraints: dict[str, dict[str, Any]], - ) -> Any: ... + ) -> Any: + """ + Compile final benchmark results from accumulated state. + + :param args: Benchmark configuration arguments + :param estimated_state: Accumulated benchmark state from execution + :param scheduler_state: Final state of the scheduler + :param profile: Benchmark profile configuration + :param requests: Collection of requests executed + :param backend: Backend interface used for execution + :param environment: Execution environment configuration + :param strategy: Scheduling strategy used + :param constraints: Execution constraints applied + :return: Compiled benchmark results instance + """ + ... BenchmarkT = TypeVar("BenchmarkT", bound=Benchmark) @@ -382,6 +508,12 @@ class BenchmarkSchedulerStats(StandardBaseDict): @classmethod def update_estimate(cls, state: EstimatedBenchmarkState, request_info: RequestInfo): + """ + Update estimated scheduler statistics with request timing information. + + :param state: Current estimated benchmark state to update + :param request_info: Metadata about the request execution with timing data + """ state.set_metric(group=cls.group_name, key="updated", value=True) state.add_avg_metric( group=cls.group_name, @@ -442,6 +574,13 @@ def update_estimate(cls, state: EstimatedBenchmarkState, request_info: RequestIn def compile( cls, estimated_state: EstimatedBenchmarkState, scheduler_state: SchedulerState ) -> BenchmarkSchedulerStats: + """ + Compile final scheduler statistics from accumulated state. + + :param estimated_state: Accumulated benchmark state with scheduler metrics + :param scheduler_state: Final state of the scheduler + :return: Compiled scheduler statistics instance + """ return BenchmarkSchedulerStats( start_time=scheduler_state.start_time, end_time=scheduler_state.end_time or scheduler_state.start_time, @@ -517,17 +656,42 @@ def compile( class GenerativeMetricsSummary(StandardBaseDict): - input: StatusDistributionSummary = Field(description="") - input_per_second: StatusDistributionSummary = Field(description="") - input_concurrency: StatusDistributionSummary = Field(description="") + """ + Statistical summaries for input, output, and total metrics. + + Provides distribution summaries across successful, incomplete, and errored + requests for absolute values, per-second rates, and concurrency levels. + """ - output: StatusDistributionSummary = Field(description="") - output_per_second: StatusDistributionSummary = Field(description="") - output_concurrency: StatusDistributionSummary = Field(description="") + input: StatusDistributionSummary = Field( + description="Distribution of input metric values" + ) + input_per_second: StatusDistributionSummary = Field( + description="Distribution of input metric rates per second" + ) + input_concurrency: StatusDistributionSummary = Field( + description="Distribution of concurrent input metric values" + ) - total: StatusDistributionSummary = Field(description="") - total_per_second: StatusDistributionSummary = Field(description="") - total_concurrency: StatusDistributionSummary = Field(description="") + output: StatusDistributionSummary = Field( + description="Distribution of output metric values" + ) + output_per_second: StatusDistributionSummary = Field( + description="Distribution of output metric rates per second" + ) + output_concurrency: StatusDistributionSummary = Field( + description="Distribution of concurrent output metric values" + ) + + total: StatusDistributionSummary = Field( + description="Distribution of total metric values (input + output)" + ) + total_per_second: StatusDistributionSummary = Field( + description="Distribution of total metric rates per second" + ) + total_concurrency: StatusDistributionSummary = Field( + description="Distribution of concurrent total metric values" + ) @classmethod def compile( @@ -537,6 +701,15 @@ def compile( input_values: list[int | float], output_values: list[int | float], ) -> GenerativeMetricsSummary: + """ + Compile generative metrics summary from request data. + + :param request_types: Status types for each request + :param request_times: Start and end times for each request + :param input_values: Input metric values for each request + :param output_values: Output metric values for each request + :return: Compiled generative metrics summary + """ total_values = [ input_val + output_val for input_val, output_val in zip(input_values, output_values, strict=False) @@ -595,9 +768,22 @@ def compile( class GenerativeTextMetricsSummary(StandardBaseDict): - tokens: GenerativeMetricsSummary = Field(description="") - words: GenerativeMetricsSummary = Field(description="") - characters: GenerativeMetricsSummary = Field(description="") + """ + Text-specific metric summaries for generative benchmarks. + + Tracks token, word, and character-level metrics across input, output, and + total usage for text generation workloads. + """ + + tokens: GenerativeMetricsSummary = Field( + description="Token count metrics and distributions" + ) + words: GenerativeMetricsSummary = Field( + description="Word count metrics and distributions" + ) + characters: GenerativeMetricsSummary = Field( + description="Character count metrics and distributions" + ) @classmethod def compile( @@ -607,6 +793,15 @@ def compile( input_metrics: list[UsageMetrics], output_metrics: list[UsageMetrics], ) -> GenerativeTextMetricsSummary: + """ + Compile text metrics summary from request usage data. + + :param request_types: Status types for each request + :param request_times: Start and end times for each request + :param input_metrics: Input usage metrics for each request + :param output_metrics: Output usage metrics for each request + :return: Compiled text metrics summary + """ return GenerativeTextMetricsSummary( tokens=GenerativeMetricsSummary.compile( request_types=request_types, @@ -634,10 +829,25 @@ def compile( class GenerativeImageMetricsSummary(StandardBaseDict): - tokens: GenerativeMetricsSummary = Field(description="") - images: GenerativeMetricsSummary = Field(description="") - pixels: GenerativeMetricsSummary = Field(description="") - bytes: GenerativeMetricsSummary = Field(description="") + """ + Image-specific metric summaries for generative benchmarks. + + Tracks token, image count, pixel, and byte-level metrics across input, output, + and total usage for image generation workloads. + """ + + tokens: GenerativeMetricsSummary = Field( + description="Image token count metrics and distributions" + ) + images: GenerativeMetricsSummary = Field( + description="Image count metrics and distributions" + ) + pixels: GenerativeMetricsSummary = Field( + description="Pixel count metrics and distributions" + ) + bytes: GenerativeMetricsSummary = Field( + description="Byte size metrics and distributions" + ) @classmethod def compile( @@ -647,6 +857,15 @@ def compile( input_metrics: list[UsageMetrics], output_metrics: list[UsageMetrics], ) -> GenerativeImageMetricsSummary: + """ + Compile image metrics summary from request usage data. + + :param request_types: Status types for each request + :param request_times: Start and end times for each request + :param input_metrics: Input usage metrics for each request + :param output_metrics: Output usage metrics for each request + :return: Compiled image metrics summary + """ return GenerativeImageMetricsSummary( tokens=GenerativeMetricsSummary.compile( request_types=request_types, @@ -676,10 +895,25 @@ def compile( class GenerativeVideoMetricsSummary(StandardBaseDict): - tokens: GenerativeMetricsSummary = Field(description="") - frames: GenerativeMetricsSummary = Field(description="") - seconds: GenerativeMetricsSummary = Field(description="") - bytes: GenerativeMetricsSummary = Field(description="") + """ + Video-specific metric summaries for generative benchmarks. + + Tracks token, frame count, duration, and byte-level metrics across input, + output, and total usage for video generation workloads. + """ + + tokens: GenerativeMetricsSummary = Field( + description="Video token count metrics and distributions" + ) + frames: GenerativeMetricsSummary = Field( + description="Frame count metrics and distributions" + ) + seconds: GenerativeMetricsSummary = Field( + description="Duration metrics in seconds and distributions" + ) + bytes: GenerativeMetricsSummary = Field( + description="Byte size metrics and distributions" + ) @classmethod def compile( @@ -689,6 +923,15 @@ def compile( input_metrics: list[UsageMetrics], output_metrics: list[UsageMetrics], ) -> GenerativeVideoMetricsSummary: + """ + Compile video metrics summary from request usage data. + + :param request_types: Status types for each request + :param request_times: Start and end times for each request + :param input_metrics: Input usage metrics for each request + :param output_metrics: Output usage metrics for each request + :return: Compiled video metrics summary + """ return GenerativeVideoMetricsSummary( tokens=GenerativeMetricsSummary.compile( request_types=request_types, @@ -720,10 +963,25 @@ def compile( class GenerativeAudioMetricsSummary(StandardBaseDict): - tokens: GenerativeMetricsSummary = Field(description="") - samples: GenerativeMetricsSummary = Field(description="") - seconds: GenerativeMetricsSummary = Field(description="") - bytes: GenerativeMetricsSummary = Field(description="") + """ + Audio-specific metric summaries for generative benchmarks. + + Tracks token, sample count, duration, and byte-level metrics across input, + output, and total usage for audio generation workloads. + """ + + tokens: GenerativeMetricsSummary = Field( + description="Audio token count metrics and distributions" + ) + samples: GenerativeMetricsSummary = Field( + description="Sample count metrics and distributions" + ) + seconds: GenerativeMetricsSummary = Field( + description="Duration metrics in seconds and distributions" + ) + bytes: GenerativeMetricsSummary = Field( + description="Byte size metrics and distributions" + ) @classmethod def compile( @@ -733,6 +991,15 @@ def compile( input_metrics: list[UsageMetrics], output_metrics: list[UsageMetrics], ) -> GenerativeAudioMetricsSummary: + """ + Compile audio metrics summary from request usage data. + + :param request_types: Status types for each request + :param request_times: Start and end times for each request + :param input_metrics: Input usage metrics for each request + :param output_metrics: Output usage metrics for each request + :return: Compiled audio metrics summary + """ return GenerativeAudioMetricsSummary( tokens=GenerativeMetricsSummary.compile( request_types=request_types, @@ -802,7 +1069,10 @@ class GenerativeMetrics(StandardBaseDict): description="Distribution of inter-token latencies in milliseconds" ) output_tokens_wo_first_per_iteration: StatusDistributionSummary = Field( - description="Distribution of output tokens (without first) generated per streaming iteration" + description=( + "Distribution of output tokens (without first) generated per " + "streaming iteration" + ) ) output_tokens_per_second: StatusDistributionSummary = Field( description="Distribution of output token generation rates" @@ -815,10 +1085,18 @@ class GenerativeMetrics(StandardBaseDict): ) # Domain specific stats - text: GenerativeTextMetricsSummary = Field(description="") - image: GenerativeImageMetricsSummary = Field(description="") - video: GenerativeVideoMetricsSummary = Field(description="") - audio: GenerativeAudioMetricsSummary = Field(description="") + text: GenerativeTextMetricsSummary = Field( + description="Text-specific metrics for tokens, words, and characters" + ) + image: GenerativeImageMetricsSummary = Field( + description="Image-specific metrics for tokens, images, pixels, and bytes" + ) + video: GenerativeVideoMetricsSummary = Field( + description="Video-specific metrics for tokens, frames, duration, and bytes" + ) + audio: GenerativeAudioMetricsSummary = Field( + description="Audio-specific metrics for tokens, samples, duration, and bytes" + ) @classmethod def update_estimate( @@ -829,6 +1107,15 @@ def update_estimate( request_info: RequestInfo, scheduler_state: SchedulerState, ): + """ + Update real-time generative metrics estimates with new request data. + + :param state: Current estimated benchmark state to update + :param response: Response received from the backend + :param request: Original request sent to the backend + :param request_info: Metadata about the request execution + :param scheduler_state: Current state of the scheduler + """ benchmark_start_time = scheduler_state.start_time request_start_time = ( request_info.timings.request_start or request_info.timings.resolve_start @@ -1025,6 +1312,14 @@ def compile( errored: list[GenerativeRequestStats], incomplete: list[GenerativeRequestStats], ) -> GenerativeMetrics: + """ + Compile final generative metrics from request statistics. + + :param completed: Successfully completed request statistics + :param errored: Failed request statistics + :param incomplete: Incomplete/cancelled request statistics + :return: Compiled generative metrics with full distributions + """ requests = completed + errored + incomplete request_types = cast( "list[Literal['successful', 'error', 'incomplete']]", @@ -1139,19 +1434,30 @@ def compile( class SchedulerDict(StandardBaseDict): """Scheduler configuration and execution state dictionary.""" - strategy: SchedulingStrategy - constraints: dict[str, dict[str, Any]] - state: SchedulerState + strategy: SchedulingStrategy = Field( + description="Scheduling strategy used for request distribution" + ) + constraints: dict[str, dict[str, Any]] = Field( + description="Execution constraints applied during benchmarking" + ) + state: SchedulerState = Field( + description="Final state of the scheduler after execution" + ) class BenchmarkerDict(StandardBaseDict): """Benchmarker configuration and component settings dictionary.""" - args: BenchmarkArgs - profile: Profile - requests: dict[str, Any] - backend: dict[str, Any] - environment: dict[str, Any] + profile: Profile = Field(description="Benchmark profile configuration") + requests: dict[str, Any] = Field( + description="Request configuration and dataset information" + ) + backend: dict[str, Any] = Field( + description="Backend configuration and connection details" + ) + environment: dict[str, Any] = Field( + description="Execution environment configuration" + ) class GenerativeBenchmark(Benchmark, StandardBaseDict): @@ -1241,13 +1547,26 @@ def duration(self) -> float: @classmethod def update_estimate( cls, - args: BenchmarkArgs, + args: BenchmarkerArgs, state: EstimatedBenchmarkState, response: GenerationResponse | None, request: GenerationRequest, request_info: RequestInfo, scheduler_state: SchedulerState, ): + """ + Update generative benchmark estimates with new request data. + + Handles warmup/cooldown filtering, request sampling via reservoir sampling, + and delegates metric updates to child metric classes. + + :param args: Benchmark configuration arguments + :param state: Current estimated benchmark state to update + :param response: Response received from the backend + :param request: Original request sent to the backend + :param request_info: Metadata about the request execution + :param scheduler_state: Current state of the scheduler + """ if ( request_info.status == "cancelled" and request_info.timings.resolve_start is None @@ -1344,7 +1663,7 @@ def update_estimate( @classmethod def compile( cls, - args: BenchmarkArgs, + args: BenchmarkerArgs, estimated_state: EstimatedBenchmarkState, scheduler_state: SchedulerState, profile: Profile, @@ -1354,6 +1673,20 @@ def compile( strategy: SchedulingStrategy, constraints: dict[str, dict[str, Any]], ) -> GenerativeBenchmark: + """ + Compile final generative benchmark from accumulated state. + + :param args: Benchmark configuration arguments + :param estimated_state: Accumulated benchmark state from execution + :param scheduler_state: Final state of the scheduler + :param profile: Benchmark profile configuration + :param requests: Collection of requests executed + :param backend: Backend interface used for execution + :param environment: Execution environment configuration + :param strategy: Scheduling strategy used + :param constraints: Execution constraints applied + :return: Compiled generative benchmark instance + """ return GenerativeBenchmark( run_id=args.run_id, run_index=args.run_index, @@ -1366,7 +1699,6 @@ def compile( state=scheduler_state, ), benchmarker=BenchmarkerDict( - args=args, profile=profile, requests=InfoMixin.extract_from_obj(requests), backend=backend.info, @@ -1404,6 +1736,267 @@ def compile( ) +class BenchmarkGenerativeTextArgs(StandardBaseModel): + """ + Configuration arguments for generative text benchmark execution. + + Defines all parameters for benchmark setup including target endpoint, data + sources, backend configuration, processing pipeline, output formatting, and + execution constraints. Supports loading from scenario files and merging with + runtime overrides. + """ + + @classmethod + def create( + cls, scenario: Path | str | None, **kwargs: dict[str, Any] + ) -> BenchmarkGenerativeTextArgs: + """ + Create benchmark args from scenario file and/or keyword arguments. + + :param scenario: Path to scenario file or name of built-in scenario + :param kwargs: Additional keyword arguments to override scenario values + :return: Configured benchmark args instance + :raises ValueError: If scenario is not found or file format is unsupported + """ + constructor_kwargs = {} + + if scenario is not None: + if isinstance(scenario, str) and scenario in ( + builtin_scenarios := get_builtin_scenarios() + ): + scenario_path = builtin_scenarios[scenario] + elif Path(scenario).exists() and Path(scenario).is_file(): + scenario_path = Path(scenario) + else: + raise ValueError(f"Scenario '{scenario}' not found.") + + with scenario_path.open() as file: + if scenario_path.suffix == ".json": + scenario_data = json.load(file) + elif scenario_path.suffix in {".yaml", ".yml"}: + scenario_data = yaml.safe_load(file) + else: + raise ValueError( + f"Unsupported scenario file format: {scenario_path.suffix}" + ) + if "args" in scenario_data: + # loading from a report file + scenario_data = scenario_data["args"] + constructor_kwargs.update(scenario_data) + + for key, value in kwargs.items(): + if value != cls.get_default(key): + constructor_kwargs[key] = value + + return cls.model_validate(constructor_kwargs) + + @classmethod + def get_default(cls: BenchmarkGenerativeTextArgs, field: str) -> Any: + """ + Get default value for a model field. + + :param field: Name of the field to retrieve default for + :return: Default value for the specified field + :raises ValueError: If field is not found in model + """ + if field not in BenchmarkGenerativeTextArgs.model_fields: + raise ValueError( + f"Field '{field}' not found in BenchmarkGenerativeTextArgs" + ) + + field_info = BenchmarkGenerativeTextArgs.model_fields[field] + if field_info.default_factory is not None: + return field_info.default_factory() + + return field_info.default + + model_config = ConfigDict( + extra="ignore", + use_enum_values=True, + from_attributes=True, + arbitrary_types_allowed=True, + ) + + # Required + target: str = Field(description="Target endpoint URL for benchmark execution") + data: list[Any] = Field( + description="List of dataset sources or data files", + default_factory=list, + min_length=1, + ) + # Benchmark configuration + profile: StrategyType | ProfileType | Profile = Field( + default="sweep", description="Benchmark profile or scheduling strategy type" + ) + rate: float | list[float] | None = Field( + default=None, description="Request rate(s) for rate-based scheduling" + ) + # Backend configuration + backend: BackendType | Backend = Field( + default="openai_http", description="Backend type or instance for execution" + ) + backend_kwargs: dict[str, Any] | None = Field( + default=None, description="Additional backend configuration arguments" + ) + model: str | None = Field(default=None, description="Model identifier for backend") + # Data configuration + processor: str | Path | PreTrainedTokenizerBase | None = Field( + default=None, description="Tokenizer path, name, or instance for processing" + ) + processor_args: dict[str, Any] | None = Field( + default=None, description="Additional tokenizer configuration arguments" + ) + data_args: list[dict[str, Any]] | None = Field( + default_factory=list, description="Per-dataset configuration arguments" + ) + data_samples: int = Field( + default=-1, description="Number of samples to use from datasets (-1 for all)" + ) + data_column_mapper: ( + DatasetPreprocessor | dict[str, str] | Literal["generative_column_mapper"] + ) = Field( + default="generative_column_mapper", + description="Column mapping preprocessor for dataset fields", + ) + data_request_formatter: DatasetPreprocessor | dict[str, str] | str = Field( + default="chat_completions", + description="Request formatting preprocessor or template name", + ) + data_collator: Callable | Literal["generative"] | None = Field( + default="generative", description="Data collator for batch processing" + ) + data_sampler: Sampler[int] | Literal["shuffle"] | None = Field( + default=None, description="Data sampler for request ordering" + ) + data_num_workers: int | None = Field( + default=None, description="Number of workers for data loading" + ) + dataloader_kwargs: dict[str, Any] | None = Field( + default=None, description="Additional dataloader configuration arguments" + ) + random_seed: int = Field(default=42, description="Random seed for reproducibility") + # Output configuration + output_path: str | Path | None = Field( + default_factory=Path.cwd, description="Directory path for output files" + ) + output_formats: list[str] | dict[str, str | dict[str, Any]] | None = Field( + default_factory=lambda: ["console", "json"], + description="Output format names or configuration mappings", + ) + # Benchmarker configuration + benchmark_cls: type[GenerativeBenchmark] = Field( + default=GenerativeBenchmark, + description="Benchmark class to use for result compilation", + ) + sample_requests: int | None = Field( + default=10, + description="Number of requests to sample for detailed metrics (None for all)", + ) + warmup: float | None = Field( + default=None, + description="Warmup period in seconds, requests, or fraction (0-1)", + ) + cooldown: float | None = Field( + default=None, + description="Cooldown period in seconds, requests, or fraction (0-1)", + ) + prefer_response_metrics: bool = Field( + default=True, + description="Whether to prefer backend response metrics over request metrics", + ) + # Constraints configuration + max_seconds: int | float | None = Field( + default=None, description="Maximum benchmark execution time in seconds" + ) + max_requests: int | None = Field( + default=None, description="Maximum number of requests to execute" + ) + max_errors: int | None = Field( + default=None, description="Maximum number of errors before stopping" + ) + max_error_rate: float | None = Field( + default=None, description="Maximum error rate (0-1) before stopping" + ) + max_global_error_rate: float | None = Field( + default=None, description="Maximum global error rate (0-1) before stopping" + ) + + @model_serializer + def serialize_model(self): + """ + Custom serialization logic for benchmark args. + + Converts complex types to serializable formats including Profile to type + string, Backend to type string, and Path objects to strings. + + :return: Dictionary representation suitable for JSON/YAML serialization + """ + return { + # target - serialize as is + "target": self.target, + "data": [ + item if isinstance(item, str | type(None)) else str(item) + for item in self.data + ], # data - for each item in the list, if not a str or None, save str(item) + "profile": ( + self.profile.type_ + if isinstance(self.profile, Profile) + else self.profile + ), # profile - if instance of Profile, then save as profile.type_ + "rate": self.rate, + "backend": ( + self.backend.type_ + if isinstance(self.backend, Backend) + else self.backend + ), # backend - if instance of Backend, then save as backend.type_ + "backend_kwargs": self.backend_kwargs, + "model": self.model, + "processor": ( + self.processor + if isinstance(self.processor, str) + else str(self.processor) + if self.processor is not None + else None + ), # processor - if not str, then save as str(processor) + "processor_args": self.processor_args, + "data_args": self.data_args, + "data_samples": self.data_samples, + "data_column_mapper": ( + self.data_column_mapper + if isinstance(self.data_column_mapper, dict | str) + else {} + ), # data_column_mapper - if not dict or str, then save as an empty dict + "data_request_formatter": ( + self.data_request_formatter + if isinstance(self.data_request_formatter, dict | str) + else {} + ), # data_request_formatter - if not dict or str, then save as empty dict + "data_collator": ( + self.data_collator if isinstance(self.data_collator, str) else None + ), # data_collator - if not str, then save as None + "data_sampler": ( + self.data_sampler if isinstance(self.data_sampler, str) else None + ), # data_sampler - if not str, then save as None + "data_num_workers": self.data_num_workers, + "dataloader_kwargs": self.dataloader_kwargs, + "random_seed": self.random_seed, + "output_path": ( + str(self.output_path) if self.output_path is not None else None + ), # output_path - if not None, then ensure it's a str + "output_formats": self.output_formats, + # benchmark_cls - don't save at all (excluded) + "sample_requests": self.sample_requests, + "warmup": self.warmup, + "cooldown": self.cooldown, + "prefer_response_metrics": self.prefer_response_metrics, + "max_seconds": self.max_seconds, + "max_requests": self.max_requests, + "max_errors": self.max_errors, + "max_error_rate": self.max_error_rate, + "max_global_error_rate": self.max_global_error_rate, + } + + class GenerativeBenchmarksReport(StandardBaseModel): """Container for multiple benchmark results with load/save functionality.""" @@ -1439,6 +2032,9 @@ def load_file( return GenerativeBenchmarksReport.model_validate(model_dict) + args: BenchmarkGenerativeTextArgs = Field( + description="The benchmark arguments used for all benchmarks in the report." + ) benchmarks: list[GenerativeBenchmark] = Field( description="The list of completed benchmarks contained within the report.", default_factory=list, diff --git a/src/guidellm/benchmark/types.py b/src/guidellm/benchmark/types.py deleted file mode 100644 index 983e3189..00000000 --- a/src/guidellm/benchmark/types.py +++ /dev/null @@ -1,22 +0,0 @@ -from __future__ import annotations - -from pathlib import Path -from typing import Any - -from transformers import PreTrainedTokenizerBase # type: ignore[import] -from typing_extensions import TypeAliasType - -from guidellm.benchmark.output import GenerativeBenchmarkerOutput - -__all__ = ["OutputFormatT", "ProcessorInputT"] - - -OutputFormatT = TypeAliasType( - "OutputFormatT", - tuple[str, ...] - | list[str] - | dict[str, str | dict[str, Any] | GenerativeBenchmarkerOutput] - | None, -) - -ProcessorInputT = TypeAliasType("ProcessorInputT", str | Path | PreTrainedTokenizerBase) diff --git a/src/guidellm/data/deserializers/deserializer.py b/src/guidellm/data/deserializers/deserializer.py index d50e4a9c..7f0dae39 100644 --- a/src/guidellm/data/deserializers/deserializer.py +++ b/src/guidellm/data/deserializers/deserializer.py @@ -50,7 +50,11 @@ def deserialize( dataset = None if type_ is None: - for deserializer in cls.registered_objects(): + for name, deserializer in cls.registry.items(): + if name == "huggingface": + # Save Hugging Face til the end since it is a catch-all. + continue + deserializer_fn: DatasetDeserializer = ( deserializer() if isinstance(deserializer, type) else deserializer ) @@ -62,6 +66,15 @@ def deserialize( random_seed=random_seed, **data_kwargs, ) + + if dataset is None: + deserializer_fn = cls.get_registered_object("huggingface")() + dataset = deserializer_fn( + data=data, + processor_factory=processor_factory, + random_seed=random_seed, + **data_kwargs, + ) elif deserializer := cls.get_registered_object(type_) is not None: deserializer_fn: DatasetDeserializer = ( deserializer() if isinstance(deserializer, type) else deserializer diff --git a/src/guidellm/presentation/data_models.py b/src/guidellm/presentation/data_models.py index ff2863b4..62bf97d8 100644 --- a/src/guidellm/presentation/data_models.py +++ b/src/guidellm/presentation/data_models.py @@ -72,7 +72,7 @@ def from_benchmarks(cls, benchmarks: list["GenerativeBenchmark"]): bm.run_stats.start_time for bm in benchmarks if bm.start_time is not None ) return cls( - model=Model(name=model, size=0), + model=Model(name=model or "", size=0), task="N/A", timestamp=timestamp, dataset=Dataset(name="N/A"), @@ -117,11 +117,15 @@ def from_benchmarks(cls, benchmarks: list["GenerativeBenchmark"]): range(len(successful_requests)), min(5, len(successful_requests)) ) sample_prompts = [ - successful_requests[i].prompt.replace("\n", " ").replace('"', "'") + successful_requests[i].request_args.replace("\n", " ").replace('"', "'") + if successful_requests[i].request_args is not None + else "" for i in sample_indices ] sample_outputs = [ successful_requests[i].output.replace("\n", " ").replace('"', "'") + if successful_requests[i].output is not None + else "" for i in sample_indices ] @@ -155,10 +159,10 @@ def from_benchmarks(cls, benchmarks: list["GenerativeBenchmark"]): min_start_time = benchmarks[0].start_time all_req_times = [ - req.scheduler_info.started_at - min_start_time + req.info.timings.request_start - min_start_time for bm in benchmarks for req in bm.requests.successful - if req.scheduler_info.started_at is not None + if req.info.timings.request_start is not None ] number_of_buckets = len(benchmarks) request_over_time_buckets, bucket_width = Bucket.from_data( diff --git a/src/guidellm/utils/cli.py b/src/guidellm/utils/cli.py index f049e94e..a75c37a8 100644 --- a/src/guidellm/utils/cli.py +++ b/src/guidellm/utils/cli.py @@ -3,12 +3,31 @@ import click +__all__ = ["Union", "format_list_arg", "parse_json", "set_if_not_default"] + def parse_json(ctx, param, value): # noqa: ARG001 if value is None or value == [None]: return None - if isinstance(value, (list, tuple)): + if isinstance(value, list | tuple): return [parse_json(ctx, param, val) for val in value] + + if "{" not in value and "}" not in value and "=" in value: + # Treat it as a key=value pair if it doesn't look like JSON. + result = {} + for pair in value.split(","): + if "=" not in pair: + raise click.BadParameter( + f"{param.name} must be a valid JSON string or key=value pairs." + ) + key, val = pair.split("=", 1) + result[key.strip()] = val.strip() + return result + + if "{" not in value and "}" not in value: + # Treat it as a plain string if it doesn't look like JSON. + return value + try: return json.loads(value) except json.JSONDecodeError as err: @@ -28,6 +47,29 @@ def set_if_not_default(ctx: click.Context, **kwargs) -> dict[str, Any]: return values +def format_list_arg( + value: Any, default: Any = None, simplify_single: bool = False +) -> list[Any] | Any: + """ + Format a multi-argument value for display. + + :param value: The value to format, which can be a single value or a list/tuple. + :param default: The default value to set if the value is non truthy. + :param simplify_single: If True and the value is a single-item list/tuple, + return the single item instead of a list. + :return: Formatted list of values, or single value if simplify_single and applicable + """ + if not value: + return default + + if isinstance(value, tuple): + value = list(value) + elif not isinstance(value, list): + value = [value] + + return value if not simplify_single or len(value) != 1 else value[0] + + class Union(click.ParamType): """ A custom click parameter type that allows for multiple types to be accepted. From a40116594761543947373dd9ba2ffab420e92efc Mon Sep 17 00:00:00 2001 From: Samuel Monson Date: Tue, 14 Oct 2025 16:10:00 -0400 Subject: [PATCH 37/57] Replace pydub, librosa, and soundfile with torchcodec Signed-off-by: Samuel Monson --- pylock.toml | 409 +++++++++++++++++++++++++++++++++++-------------- pyproject.toml | 5 +- 2 files changed, 293 insertions(+), 121 deletions(-) diff --git a/pylock.toml b/pylock.toml index c9329a22..779ab1c8 100644 --- a/pylock.toml +++ b/pylock.toml @@ -6,7 +6,7 @@ environments = [ "python_version ~= \"3.12\"", "python_full_version >= \"3.10.0\" and python_version < \"3.12\"", ] -extras = ["dev", "recommended"] +extras = ["dev", "openai", "perf", "recommended"] dependency-groups = ["default"] default-groups = ["default"] created-by = "pdm" @@ -52,7 +52,7 @@ sdist = {name = "blobfile-3.1.0.tar.gz", url = "https://files.pythonhosted.org/p wheels = [ {name = "blobfile-3.1.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/77/a7/51af11120d75af2828f8eede0b13a4caff650d708ac50e62d000aefe1ffb/blobfile-3.1.0-py3-none-any.whl",hashes = {sha256 = "2b4c5e766ebb7dfa20e4990cf6ec3d2106bdc91d632fb9377f170a234c5a5c6a"}}, ] -marker = "\"recommended\" in extras" +marker = "\"openai\" in extras or \"recommended\" in extras" [packages.tool.pdm] dependencies = [ @@ -247,6 +247,73 @@ dependencies = [ "tomli>=1.1.0; python_version < \"3.11\"", ] +[[packages]] +name = "numpy" +version = "2.3.3" +requires-python = ">=3.11" +sdist = {name = "numpy-2.3.3.tar.gz", url = "https://files.pythonhosted.org/packages/d0/19/95b3d357407220ed24c139018d2518fab0a61a948e68286a25f1a4d049ff/numpy-2.3.3.tar.gz", hashes = {sha256 = "ddc7c39727ba62b80dfdbedf400d1c10ddfa8eefbd7ec8dcb118be8b56d31029"}} +wheels = [ + {name = "numpy-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/6b/01/342ad585ad82419b99bcf7cebe99e61da6bedb89e213c5fd71acc467faee/numpy-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl",hashes = {sha256 = "cd052f1fa6a78dee696b58a914b7229ecfa41f0a6d96dc663c1220a55e137593"}}, + {name = "numpy-2.3.3-cp314-cp314-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/ef/d8/204e0d73fc1b7a9ee80ab1fe1983dd33a4d64a4e30a05364b0208e9a241a/numpy-2.3.3-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "414a97499480067d305fcac9716c29cf4d0d76db6ebf0bf3cbce666677f12652"}}, + {name = "numpy-2.3.3-cp314-cp314-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/22/af/f11c916d08f3a18fb8ba81ab72b5b74a6e42ead4c2846d270eb19845bf74/numpy-2.3.3-cp314-cp314-macosx_14_0_arm64.whl",hashes = {sha256 = "50a5fe69f135f88a2be9b6ca0481a68a136f6febe1916e4920e12f1a34e708a7"}}, + {name = "numpy-2.3.3-cp314-cp314-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/fb/11/0ed919c8381ac9d2ffacd63fd1f0c34d27e99cab650f0eb6f110e6ae4858/numpy-2.3.3-cp314-cp314-macosx_14_0_x86_64.whl",hashes = {sha256 = "b912f2ed2b67a129e6a601e9d93d4fa37bef67e54cac442a2f588a54afe5c67a"}}, + {name = "numpy-2.3.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/ee/83/deb5f77cb0f7ba6cb52b91ed388b47f8f3c2e9930d4665c600408d9b90b9/numpy-2.3.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "9e318ee0596d76d4cb3d78535dc005fa60e5ea348cd131a51e99d0bdbe0b54fe"}}, + {name = "numpy-2.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/77/cc/70e59dcb84f2b005d4f306310ff0a892518cc0c8000a33d0e6faf7ca8d80/numpy-2.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "ce020080e4a52426202bdb6f7691c65bb55e49f261f31a8f506c9f6bc7450421"}}, + {name = "numpy-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/b6/5a/b2ab6c18b4257e099587d5b7f903317bd7115333ad8d4ec4874278eafa61/numpy-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl",hashes = {sha256 = "e6687dc183aa55dae4a705b35f9c0f8cb178bcaa2f029b241ac5356221d5c021"}}, + {name = "numpy-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/b8/f1/8b3fdc44324a259298520dd82147ff648979bed085feeacc1250ef1656c0/numpy-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl",hashes = {sha256 = "d8f3b1080782469fdc1718c4ed1d22549b5fb12af0d57d35e992158a772a37cf"}}, + {name = "numpy-2.3.3-cp314-cp314-win32.whl",url = "https://files.pythonhosted.org/packages/f0/a1/b87a284fb15a42e9274e7fcea0dad259d12ddbf07c1595b26883151ca3b4/numpy-2.3.3-cp314-cp314-win32.whl",hashes = {sha256 = "cb248499b0bc3be66ebd6578b83e5acacf1d6cb2a77f2248ce0e40fbec5a76d0"}}, + {name = "numpy-2.3.3-cp314-cp314-win_amd64.whl",url = "https://files.pythonhosted.org/packages/70/5f/1816f4d08f3b8f66576d8433a66f8fa35a5acfb3bbd0bf6c31183b003f3d/numpy-2.3.3-cp314-cp314-win_amd64.whl",hashes = {sha256 = "691808c2b26b0f002a032c73255d0bd89751425f379f7bcd22d140db593a96e8"}}, + {name = "numpy-2.3.3-cp314-cp314-win_arm64.whl",url = "https://files.pythonhosted.org/packages/8c/de/072420342e46a8ea41c324a555fa90fcc11637583fb8df722936aed1736d/numpy-2.3.3-cp314-cp314-win_arm64.whl",hashes = {sha256 = "9ad12e976ca7b10f1774b03615a2a4bab8addce37ecc77394d8e986927dc0dfe"}}, + {name = "numpy-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/d5/df/ee2f1c0a9de7347f14da5dd3cd3c3b034d1b8607ccb6883d7dd5c035d631/numpy-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl",hashes = {sha256 = "9cc48e09feb11e1db00b320e9d30a4151f7369afb96bd0e48d942d09da3a0d00"}}, + {name = "numpy-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/d6/92/9453bdc5a4e9e69cf4358463f25e8260e2ffc126d52e10038b9077815989/numpy-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl",hashes = {sha256 = "901bf6123879b7f251d3631967fd574690734236075082078e0571977c6a8e6a"}}, + {name = "numpy-2.3.3-cp314-cp314t-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/13/77/1447b9eb500f028bb44253105bd67534af60499588a5149a94f18f2ca917/numpy-2.3.3-cp314-cp314t-macosx_14_0_arm64.whl",hashes = {sha256 = "7f025652034199c301049296b59fa7d52c7e625017cae4c75d8662e377bf487d"}}, + {name = "numpy-2.3.3-cp314-cp314t-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/3d/f9/d72221b6ca205f9736cb4b2ce3b002f6e45cd67cd6a6d1c8af11a2f0b649/numpy-2.3.3-cp314-cp314t-macosx_14_0_x86_64.whl",hashes = {sha256 = "533ca5f6d325c80b6007d4d7fb1984c303553534191024ec6a524a4c92a5935a"}}, + {name = "numpy-2.3.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/3c/5f/d12834711962ad9c46af72f79bb31e73e416ee49d17f4c797f72c96b6ca5/numpy-2.3.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "0edd58682a399824633b66885d699d7de982800053acf20be1eaa46d92009c54"}}, + {name = "numpy-2.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/a1/0d/fdbec6629d97fd1bebed56cd742884e4eead593611bbe1abc3eb40d304b2/numpy-2.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "367ad5d8fbec5d9296d18478804a530f1191e24ab4d75ab408346ae88045d25e"}}, + {name = "numpy-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/9b/09/0a35196dc5575adde1eb97ddfbc3e1687a814f905377621d18ca9bc2b7dd/numpy-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "8f6ac61a217437946a1fa48d24c47c91a0c4f725237871117dea264982128097"}}, + {name = "numpy-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/7a/ca/c9de3ea397d576f1b6753eaa906d4cdef1bf97589a6d9825a349b4729cc2/numpy-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "179a42101b845a816d464b6fe9a845dfaf308fdfc7925387195570789bb2c970"}}, + {name = "numpy-2.3.3-cp314-cp314t-win32.whl",url = "https://files.pythonhosted.org/packages/fd/c2/e5ed830e08cd0196351db55db82f65bc0ab05da6ef2b72a836dcf1936d2f/numpy-2.3.3-cp314-cp314t-win32.whl",hashes = {sha256 = "1250c5d3d2562ec4174bce2e3a1523041595f9b651065e4a4473f5f48a6bc8a5"}}, + {name = "numpy-2.3.3-cp314-cp314t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/47/c7/b0f6b5b67f6788a0725f744496badbb604d226bf233ba716683ebb47b570/numpy-2.3.3-cp314-cp314t-win_amd64.whl",hashes = {sha256 = "b37a0b2e5935409daebe82c1e42274d30d9dd355852529eab91dab8dcca7419f"}}, + {name = "numpy-2.3.3-cp314-cp314t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/06/b9/33bba5ff6fb679aa0b1f8a07e853f002a6b04b9394db3069a1270a7784ca/numpy-2.3.3-cp314-cp314t-win_arm64.whl",hashes = {sha256 = "78c9f6560dc7e6b3990e32df7ea1a50bbd0e2a111e05209963f5ddcab7073b0b"}}, + {name = "numpy-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/7d/b9/984c2b1ee61a8b803bf63582b4ac4242cf76e2dbd663efeafcb620cc0ccb/numpy-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl",hashes = {sha256 = "f5415fb78995644253370985342cd03572ef8620b934da27d77377a2285955bf"}}, + {name = "numpy-2.3.3-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/a6/e4/07970e3bed0b1384d22af1e9912527ecbeb47d3b26e9b6a3bced068b3bea/numpy-2.3.3-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "d00de139a3324e26ed5b95870ce63be7ec7352171bc69a4cf1f157a48e3eb6b7"}}, + {name = "numpy-2.3.3-cp313-cp313-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/35/c7/477a83887f9de61f1203bad89cf208b7c19cc9fef0cebef65d5a1a0619f2/numpy-2.3.3-cp313-cp313-macosx_14_0_arm64.whl",hashes = {sha256 = "9dc13c6a5829610cc07422bc74d3ac083bd8323f14e2827d992f9e52e22cd6a6"}}, + {name = "numpy-2.3.3-cp313-cp313-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/52/47/93b953bd5866a6f6986344d045a207d3f1cfbad99db29f534ea9cee5108c/numpy-2.3.3-cp313-cp313-macosx_14_0_x86_64.whl",hashes = {sha256 = "d79715d95f1894771eb4e60fb23f065663b2298f7d22945d66877aadf33d00c7"}}, + {name = "numpy-2.3.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/23/83/377f84aaeb800b64c0ef4de58b08769e782edcefa4fea712910b6f0afd3c/numpy-2.3.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "952cfd0748514ea7c3afc729a0fc639e61655ce4c55ab9acfab14bda4f402b4c"}}, + {name = "numpy-2.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/9a/a5/bf3db6e66c4b160d6ea10b534c381a1955dfab34cb1017ea93aa33c70ed3/numpy-2.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "5b83648633d46f77039c29078751f80da65aa64d5622a3cd62aaef9d835b6c93"}}, + {name = "numpy-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/a2/59/1287924242eb4fa3f9b3a2c30400f2e17eb2707020d1c5e3086fe7330717/numpy-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "b001bae8cea1c7dfdb2ae2b017ed0a6f2102d7a70059df1e338e307a4c78a8ae"}}, + {name = "numpy-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/e6/93/b3d47ed882027c35e94ac2320c37e452a549f582a5e801f2d34b56973c97/numpy-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "8e9aced64054739037d42fb84c54dd38b81ee238816c948c8f3ed134665dcd86"}}, + {name = "numpy-2.3.3-cp313-cp313-win32.whl",url = "https://files.pythonhosted.org/packages/20/d9/487a2bccbf7cc9d4bfc5f0f197761a5ef27ba870f1e3bbb9afc4bbe3fcc2/numpy-2.3.3-cp313-cp313-win32.whl",hashes = {sha256 = "9591e1221db3f37751e6442850429b3aabf7026d3b05542d102944ca7f00c8a8"}}, + {name = "numpy-2.3.3-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/1b/b5/263ebbbbcede85028f30047eab3d58028d7ebe389d6493fc95ae66c636ab/numpy-2.3.3-cp313-cp313-win_amd64.whl",hashes = {sha256 = "f0dadeb302887f07431910f67a14d57209ed91130be0adea2f9793f1a4f817cf"}}, + {name = "numpy-2.3.3-cp313-cp313-win_arm64.whl",url = "https://files.pythonhosted.org/packages/fa/75/67b8ca554bbeaaeb3fac2e8bce46967a5a06544c9108ec0cf5cece559b6c/numpy-2.3.3-cp313-cp313-win_arm64.whl",hashes = {sha256 = "3c7cf302ac6e0b76a64c4aecf1a09e51abd9b01fc7feee80f6c43e3ab1b1dbc5"}}, + {name = "numpy-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/11/d0/0d1ddec56b162042ddfafeeb293bac672de9b0cfd688383590090963720a/numpy-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl",hashes = {sha256 = "eda59e44957d272846bb407aad19f89dc6f58fecf3504bd144f4c5cf81a7eacc"}}, + {name = "numpy-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/36/9e/1996ca6b6d00415b6acbdd3c42f7f03ea256e2c3f158f80bd7436a8a19f3/numpy-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl",hashes = {sha256 = "823d04112bc85ef5c4fda73ba24e6096c8f869931405a80aa8b0e604510a26bc"}}, + {name = "numpy-2.3.3-cp313-cp313t-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/05/24/43da09aa764c68694b76e84b3d3f0c44cb7c18cdc1ba80e48b0ac1d2cd39/numpy-2.3.3-cp313-cp313t-macosx_14_0_arm64.whl",hashes = {sha256 = "40051003e03db4041aa325da2a0971ba41cf65714e65d296397cc0e32de6018b"}}, + {name = "numpy-2.3.3-cp313-cp313t-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/bc/14/50ffb0f22f7218ef8af28dd089f79f68289a7a05a208db9a2c5dcbe123c1/numpy-2.3.3-cp313-cp313t-macosx_14_0_x86_64.whl",hashes = {sha256 = "6ee9086235dd6ab7ae75aba5662f582a81ced49f0f1c6de4260a78d8f2d91a19"}}, + {name = "numpy-2.3.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/55/52/af46ac0795e09657d45a7f4db961917314377edecf66db0e39fa7ab5c3d3/numpy-2.3.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "94fcaa68757c3e2e668ddadeaa86ab05499a70725811e582b6a9858dd472fb30"}}, + {name = "numpy-2.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/a7/b1/dc226b4c90eb9f07a3fff95c2f0db3268e2e54e5cce97c4ac91518aee71b/numpy-2.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "da1a74b90e7483d6ce5244053399a614b1d6b7bc30a60d2f570e5071f8959d3e"}}, + {name = "numpy-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/9d/9d/9d8d358f2eb5eced14dba99f110d83b5cd9a4460895230f3b396ad19a323/numpy-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "2990adf06d1ecee3b3dcbb4977dfab6e9f09807598d647f04d385d29e7a3c3d3"}}, + {name = "numpy-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/b6/27/b3922660c45513f9377b3fb42240bec63f203c71416093476ec9aa0719dc/numpy-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "ed635ff692483b8e3f0fcaa8e7eb8a75ee71aa6d975388224f70821421800cea"}}, + {name = "numpy-2.3.3-cp313-cp313t-win32.whl",url = "https://files.pythonhosted.org/packages/5b/8e/3ab61a730bdbbc201bb245a71102aa609f0008b9ed15255500a99cd7f780/numpy-2.3.3-cp313-cp313t-win32.whl",hashes = {sha256 = "a333b4ed33d8dc2b373cc955ca57babc00cd6f9009991d9edc5ddbc1bac36bcd"}}, + {name = "numpy-2.3.3-cp313-cp313t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/1c/3a/e22b766b11f6030dc2decdeff5c2fb1610768055603f9f3be88b6d192fb2/numpy-2.3.3-cp313-cp313t-win_amd64.whl",hashes = {sha256 = "4384a169c4d8f97195980815d6fcad04933a7e1ab3b530921c3fef7a1c63426d"}}, + {name = "numpy-2.3.3-cp313-cp313t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/7b/42/c2e2bc48c5e9b2a83423f99733950fbefd86f165b468a3d85d52b30bf782/numpy-2.3.3-cp313-cp313t-win_arm64.whl",hashes = {sha256 = "75370986cc0bc66f4ce5110ad35aae6d182cc4ce6433c40ad151f53690130bf1"}}, + {name = "numpy-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/51/5d/bb7fc075b762c96329147799e1bcc9176ab07ca6375ea976c475482ad5b3/numpy-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl",hashes = {sha256 = "cfdd09f9c84a1a934cde1eec2267f0a43a7cd44b2cca4ff95b7c0d14d144b0bf"}}, + {name = "numpy-2.3.3-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/6b/0e/c6211bb92af26517acd52125a237a92afe9c3124c6a68d3b9f81b62a0568/numpy-2.3.3-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "cb32e3cf0f762aee47ad1ddc6672988f7f27045b0783c887190545baba73aa25"}}, + {name = "numpy-2.3.3-cp312-cp312-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/22/f2/07bb754eb2ede9073f4054f7c0286b0d9d2e23982e090a80d478b26d35ca/numpy-2.3.3-cp312-cp312-macosx_14_0_arm64.whl",hashes = {sha256 = "396b254daeb0a57b1fe0ecb5e3cff6fa79a380fa97c8f7781a6d08cd429418fe"}}, + {name = "numpy-2.3.3-cp312-cp312-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/81/0a/afa51697e9fb74642f231ea36aca80fa17c8fb89f7a82abd5174023c3960/numpy-2.3.3-cp312-cp312-macosx_14_0_x86_64.whl",hashes = {sha256 = "067e3d7159a5d8f8a0b46ee11148fc35ca9b21f61e3c49fbd0a027450e65a33b"}}, + {name = "numpy-2.3.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/5d/f5/122d9cdb3f51c520d150fef6e87df9279e33d19a9611a87c0d2cf78a89f4/numpy-2.3.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "1c02d0629d25d426585fb2e45a66154081b9fa677bc92a881ff1d216bc9919a8"}}, + {name = "numpy-2.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/51/64/7de3c91e821a2debf77c92962ea3fe6ac2bc45d0778c1cbe15d4fce2fd94/numpy-2.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "d9192da52b9745f7f0766531dcfa978b7763916f158bb63bdb8a1eca0068ab20"}}, + {name = "numpy-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/30/e4/961a5fa681502cd0d68907818b69f67542695b74e3ceaa513918103b7e80/numpy-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "cd7de500a5b66319db419dc3c345244404a164beae0d0937283b907d8152e6ea"}}, + {name = "numpy-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/99/26/92c912b966e47fbbdf2ad556cb17e3a3088e2e1292b9833be1dfa5361a1a/numpy-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "93d4962d8f82af58f0b2eb85daaf1b3ca23fe0a85d0be8f1f2b7bb46034e56d7"}}, + {name = "numpy-2.3.3-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/17/b6/fc8f82cb3520768718834f310c37d96380d9dc61bfdaf05fe5c0b7653e01/numpy-2.3.3-cp312-cp312-win32.whl",hashes = {sha256 = "5534ed6b92f9b7dca6c0a19d6df12d41c68b991cef051d108f6dbff3babc4ebf"}}, + {name = "numpy-2.3.3-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/32/ee/de999f2625b80d043d6d2d628c07d0d5555a677a3cf78fdf868d409b8766/numpy-2.3.3-cp312-cp312-win_amd64.whl",hashes = {sha256 = "497d7cad08e7092dba36e3d296fe4c97708c93daf26643a1ae4b03f6294d30eb"}}, + {name = "numpy-2.3.3-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/49/6e/b479032f8a43559c383acb20816644f5f91c88f633d9271ee84f3b3a996c/numpy-2.3.3-cp312-cp312-win_arm64.whl",hashes = {sha256 = "ca0309a18d4dfea6fc6262a66d06c26cfe4640c3926ceec90e57791a82b6eee5"}}, +] +marker = "\"default\" in dependency_groups and python_version ~= \"3.12\" or \"dev\" in extras and python_version ~= \"3.12\"" + +[packages.tool.pdm] +dependencies = [] + [[packages]] name = "pre-commit" version = "3.5.0" @@ -554,80 +621,13 @@ wheels = [ {name = "scipy-1.16.2-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/34/95/20e02ca66fb495a95fba0642fd48e0c390d0ece9b9b14c6e931a60a12dea/scipy-1.16.2-cp312-cp312-win_amd64.whl",hashes = {sha256 = "0ce54e07bbb394b417457409a64fd015be623f36e330ac49306433ffe04bc97e"}}, {name = "scipy-1.16.2-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/92/ad/13646b9beb0a95528ca46d52b7babafbe115017814a611f2065ee4e61d20/scipy-1.16.2-cp312-cp312-win_arm64.whl",hashes = {sha256 = "2a8ffaa4ac0df81a0b94577b18ee079f13fecdb924df3328fc44a7dc5ac46851"}}, ] -marker = "python_version ~= \"3.12\"" +marker = "python_version ~= \"3.12\" and \"dev\" in extras" [packages.tool.pdm] dependencies = [ "numpy<2.6,>=1.25.2", ] -[[packages]] -name = "numpy" -version = "2.3.3" -requires-python = ">=3.11" -sdist = {name = "numpy-2.3.3.tar.gz", url = "https://files.pythonhosted.org/packages/d0/19/95b3d357407220ed24c139018d2518fab0a61a948e68286a25f1a4d049ff/numpy-2.3.3.tar.gz", hashes = {sha256 = "ddc7c39727ba62b80dfdbedf400d1c10ddfa8eefbd7ec8dcb118be8b56d31029"}} -wheels = [ - {name = "numpy-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/6b/01/342ad585ad82419b99bcf7cebe99e61da6bedb89e213c5fd71acc467faee/numpy-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl",hashes = {sha256 = "cd052f1fa6a78dee696b58a914b7229ecfa41f0a6d96dc663c1220a55e137593"}}, - {name = "numpy-2.3.3-cp314-cp314-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/ef/d8/204e0d73fc1b7a9ee80ab1fe1983dd33a4d64a4e30a05364b0208e9a241a/numpy-2.3.3-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "414a97499480067d305fcac9716c29cf4d0d76db6ebf0bf3cbce666677f12652"}}, - {name = "numpy-2.3.3-cp314-cp314-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/22/af/f11c916d08f3a18fb8ba81ab72b5b74a6e42ead4c2846d270eb19845bf74/numpy-2.3.3-cp314-cp314-macosx_14_0_arm64.whl",hashes = {sha256 = "50a5fe69f135f88a2be9b6ca0481a68a136f6febe1916e4920e12f1a34e708a7"}}, - {name = "numpy-2.3.3-cp314-cp314-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/fb/11/0ed919c8381ac9d2ffacd63fd1f0c34d27e99cab650f0eb6f110e6ae4858/numpy-2.3.3-cp314-cp314-macosx_14_0_x86_64.whl",hashes = {sha256 = "b912f2ed2b67a129e6a601e9d93d4fa37bef67e54cac442a2f588a54afe5c67a"}}, - {name = "numpy-2.3.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/ee/83/deb5f77cb0f7ba6cb52b91ed388b47f8f3c2e9930d4665c600408d9b90b9/numpy-2.3.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "9e318ee0596d76d4cb3d78535dc005fa60e5ea348cd131a51e99d0bdbe0b54fe"}}, - {name = "numpy-2.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/77/cc/70e59dcb84f2b005d4f306310ff0a892518cc0c8000a33d0e6faf7ca8d80/numpy-2.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "ce020080e4a52426202bdb6f7691c65bb55e49f261f31a8f506c9f6bc7450421"}}, - {name = "numpy-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/b6/5a/b2ab6c18b4257e099587d5b7f903317bd7115333ad8d4ec4874278eafa61/numpy-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl",hashes = {sha256 = "e6687dc183aa55dae4a705b35f9c0f8cb178bcaa2f029b241ac5356221d5c021"}}, - {name = "numpy-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/b8/f1/8b3fdc44324a259298520dd82147ff648979bed085feeacc1250ef1656c0/numpy-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl",hashes = {sha256 = "d8f3b1080782469fdc1718c4ed1d22549b5fb12af0d57d35e992158a772a37cf"}}, - {name = "numpy-2.3.3-cp314-cp314-win32.whl",url = "https://files.pythonhosted.org/packages/f0/a1/b87a284fb15a42e9274e7fcea0dad259d12ddbf07c1595b26883151ca3b4/numpy-2.3.3-cp314-cp314-win32.whl",hashes = {sha256 = "cb248499b0bc3be66ebd6578b83e5acacf1d6cb2a77f2248ce0e40fbec5a76d0"}}, - {name = "numpy-2.3.3-cp314-cp314-win_amd64.whl",url = "https://files.pythonhosted.org/packages/70/5f/1816f4d08f3b8f66576d8433a66f8fa35a5acfb3bbd0bf6c31183b003f3d/numpy-2.3.3-cp314-cp314-win_amd64.whl",hashes = {sha256 = "691808c2b26b0f002a032c73255d0bd89751425f379f7bcd22d140db593a96e8"}}, - {name = "numpy-2.3.3-cp314-cp314-win_arm64.whl",url = "https://files.pythonhosted.org/packages/8c/de/072420342e46a8ea41c324a555fa90fcc11637583fb8df722936aed1736d/numpy-2.3.3-cp314-cp314-win_arm64.whl",hashes = {sha256 = "9ad12e976ca7b10f1774b03615a2a4bab8addce37ecc77394d8e986927dc0dfe"}}, - {name = "numpy-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/d5/df/ee2f1c0a9de7347f14da5dd3cd3c3b034d1b8607ccb6883d7dd5c035d631/numpy-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl",hashes = {sha256 = "9cc48e09feb11e1db00b320e9d30a4151f7369afb96bd0e48d942d09da3a0d00"}}, - {name = "numpy-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/d6/92/9453bdc5a4e9e69cf4358463f25e8260e2ffc126d52e10038b9077815989/numpy-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl",hashes = {sha256 = "901bf6123879b7f251d3631967fd574690734236075082078e0571977c6a8e6a"}}, - {name = "numpy-2.3.3-cp314-cp314t-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/13/77/1447b9eb500f028bb44253105bd67534af60499588a5149a94f18f2ca917/numpy-2.3.3-cp314-cp314t-macosx_14_0_arm64.whl",hashes = {sha256 = "7f025652034199c301049296b59fa7d52c7e625017cae4c75d8662e377bf487d"}}, - {name = "numpy-2.3.3-cp314-cp314t-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/3d/f9/d72221b6ca205f9736cb4b2ce3b002f6e45cd67cd6a6d1c8af11a2f0b649/numpy-2.3.3-cp314-cp314t-macosx_14_0_x86_64.whl",hashes = {sha256 = "533ca5f6d325c80b6007d4d7fb1984c303553534191024ec6a524a4c92a5935a"}}, - {name = "numpy-2.3.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/3c/5f/d12834711962ad9c46af72f79bb31e73e416ee49d17f4c797f72c96b6ca5/numpy-2.3.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "0edd58682a399824633b66885d699d7de982800053acf20be1eaa46d92009c54"}}, - {name = "numpy-2.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/a1/0d/fdbec6629d97fd1bebed56cd742884e4eead593611bbe1abc3eb40d304b2/numpy-2.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "367ad5d8fbec5d9296d18478804a530f1191e24ab4d75ab408346ae88045d25e"}}, - {name = "numpy-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/9b/09/0a35196dc5575adde1eb97ddfbc3e1687a814f905377621d18ca9bc2b7dd/numpy-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "8f6ac61a217437946a1fa48d24c47c91a0c4f725237871117dea264982128097"}}, - {name = "numpy-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/7a/ca/c9de3ea397d576f1b6753eaa906d4cdef1bf97589a6d9825a349b4729cc2/numpy-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "179a42101b845a816d464b6fe9a845dfaf308fdfc7925387195570789bb2c970"}}, - {name = "numpy-2.3.3-cp314-cp314t-win32.whl",url = "https://files.pythonhosted.org/packages/fd/c2/e5ed830e08cd0196351db55db82f65bc0ab05da6ef2b72a836dcf1936d2f/numpy-2.3.3-cp314-cp314t-win32.whl",hashes = {sha256 = "1250c5d3d2562ec4174bce2e3a1523041595f9b651065e4a4473f5f48a6bc8a5"}}, - {name = "numpy-2.3.3-cp314-cp314t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/47/c7/b0f6b5b67f6788a0725f744496badbb604d226bf233ba716683ebb47b570/numpy-2.3.3-cp314-cp314t-win_amd64.whl",hashes = {sha256 = "b37a0b2e5935409daebe82c1e42274d30d9dd355852529eab91dab8dcca7419f"}}, - {name = "numpy-2.3.3-cp314-cp314t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/06/b9/33bba5ff6fb679aa0b1f8a07e853f002a6b04b9394db3069a1270a7784ca/numpy-2.3.3-cp314-cp314t-win_arm64.whl",hashes = {sha256 = "78c9f6560dc7e6b3990e32df7ea1a50bbd0e2a111e05209963f5ddcab7073b0b"}}, - {name = "numpy-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/7d/b9/984c2b1ee61a8b803bf63582b4ac4242cf76e2dbd663efeafcb620cc0ccb/numpy-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl",hashes = {sha256 = "f5415fb78995644253370985342cd03572ef8620b934da27d77377a2285955bf"}}, - {name = "numpy-2.3.3-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/a6/e4/07970e3bed0b1384d22af1e9912527ecbeb47d3b26e9b6a3bced068b3bea/numpy-2.3.3-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "d00de139a3324e26ed5b95870ce63be7ec7352171bc69a4cf1f157a48e3eb6b7"}}, - {name = "numpy-2.3.3-cp313-cp313-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/35/c7/477a83887f9de61f1203bad89cf208b7c19cc9fef0cebef65d5a1a0619f2/numpy-2.3.3-cp313-cp313-macosx_14_0_arm64.whl",hashes = {sha256 = "9dc13c6a5829610cc07422bc74d3ac083bd8323f14e2827d992f9e52e22cd6a6"}}, - {name = "numpy-2.3.3-cp313-cp313-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/52/47/93b953bd5866a6f6986344d045a207d3f1cfbad99db29f534ea9cee5108c/numpy-2.3.3-cp313-cp313-macosx_14_0_x86_64.whl",hashes = {sha256 = "d79715d95f1894771eb4e60fb23f065663b2298f7d22945d66877aadf33d00c7"}}, - {name = "numpy-2.3.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/23/83/377f84aaeb800b64c0ef4de58b08769e782edcefa4fea712910b6f0afd3c/numpy-2.3.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "952cfd0748514ea7c3afc729a0fc639e61655ce4c55ab9acfab14bda4f402b4c"}}, - {name = "numpy-2.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/9a/a5/bf3db6e66c4b160d6ea10b534c381a1955dfab34cb1017ea93aa33c70ed3/numpy-2.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "5b83648633d46f77039c29078751f80da65aa64d5622a3cd62aaef9d835b6c93"}}, - {name = "numpy-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/a2/59/1287924242eb4fa3f9b3a2c30400f2e17eb2707020d1c5e3086fe7330717/numpy-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "b001bae8cea1c7dfdb2ae2b017ed0a6f2102d7a70059df1e338e307a4c78a8ae"}}, - {name = "numpy-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/e6/93/b3d47ed882027c35e94ac2320c37e452a549f582a5e801f2d34b56973c97/numpy-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "8e9aced64054739037d42fb84c54dd38b81ee238816c948c8f3ed134665dcd86"}}, - {name = "numpy-2.3.3-cp313-cp313-win32.whl",url = "https://files.pythonhosted.org/packages/20/d9/487a2bccbf7cc9d4bfc5f0f197761a5ef27ba870f1e3bbb9afc4bbe3fcc2/numpy-2.3.3-cp313-cp313-win32.whl",hashes = {sha256 = "9591e1221db3f37751e6442850429b3aabf7026d3b05542d102944ca7f00c8a8"}}, - {name = "numpy-2.3.3-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/1b/b5/263ebbbbcede85028f30047eab3d58028d7ebe389d6493fc95ae66c636ab/numpy-2.3.3-cp313-cp313-win_amd64.whl",hashes = {sha256 = "f0dadeb302887f07431910f67a14d57209ed91130be0adea2f9793f1a4f817cf"}}, - {name = "numpy-2.3.3-cp313-cp313-win_arm64.whl",url = "https://files.pythonhosted.org/packages/fa/75/67b8ca554bbeaaeb3fac2e8bce46967a5a06544c9108ec0cf5cece559b6c/numpy-2.3.3-cp313-cp313-win_arm64.whl",hashes = {sha256 = "3c7cf302ac6e0b76a64c4aecf1a09e51abd9b01fc7feee80f6c43e3ab1b1dbc5"}}, - {name = "numpy-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/11/d0/0d1ddec56b162042ddfafeeb293bac672de9b0cfd688383590090963720a/numpy-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl",hashes = {sha256 = "eda59e44957d272846bb407aad19f89dc6f58fecf3504bd144f4c5cf81a7eacc"}}, - {name = "numpy-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/36/9e/1996ca6b6d00415b6acbdd3c42f7f03ea256e2c3f158f80bd7436a8a19f3/numpy-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl",hashes = {sha256 = "823d04112bc85ef5c4fda73ba24e6096c8f869931405a80aa8b0e604510a26bc"}}, - {name = "numpy-2.3.3-cp313-cp313t-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/05/24/43da09aa764c68694b76e84b3d3f0c44cb7c18cdc1ba80e48b0ac1d2cd39/numpy-2.3.3-cp313-cp313t-macosx_14_0_arm64.whl",hashes = {sha256 = "40051003e03db4041aa325da2a0971ba41cf65714e65d296397cc0e32de6018b"}}, - {name = "numpy-2.3.3-cp313-cp313t-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/bc/14/50ffb0f22f7218ef8af28dd089f79f68289a7a05a208db9a2c5dcbe123c1/numpy-2.3.3-cp313-cp313t-macosx_14_0_x86_64.whl",hashes = {sha256 = "6ee9086235dd6ab7ae75aba5662f582a81ced49f0f1c6de4260a78d8f2d91a19"}}, - {name = "numpy-2.3.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/55/52/af46ac0795e09657d45a7f4db961917314377edecf66db0e39fa7ab5c3d3/numpy-2.3.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "94fcaa68757c3e2e668ddadeaa86ab05499a70725811e582b6a9858dd472fb30"}}, - {name = "numpy-2.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/a7/b1/dc226b4c90eb9f07a3fff95c2f0db3268e2e54e5cce97c4ac91518aee71b/numpy-2.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "da1a74b90e7483d6ce5244053399a614b1d6b7bc30a60d2f570e5071f8959d3e"}}, - {name = "numpy-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/9d/9d/9d8d358f2eb5eced14dba99f110d83b5cd9a4460895230f3b396ad19a323/numpy-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "2990adf06d1ecee3b3dcbb4977dfab6e9f09807598d647f04d385d29e7a3c3d3"}}, - {name = "numpy-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/b6/27/b3922660c45513f9377b3fb42240bec63f203c71416093476ec9aa0719dc/numpy-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "ed635ff692483b8e3f0fcaa8e7eb8a75ee71aa6d975388224f70821421800cea"}}, - {name = "numpy-2.3.3-cp313-cp313t-win32.whl",url = "https://files.pythonhosted.org/packages/5b/8e/3ab61a730bdbbc201bb245a71102aa609f0008b9ed15255500a99cd7f780/numpy-2.3.3-cp313-cp313t-win32.whl",hashes = {sha256 = "a333b4ed33d8dc2b373cc955ca57babc00cd6f9009991d9edc5ddbc1bac36bcd"}}, - {name = "numpy-2.3.3-cp313-cp313t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/1c/3a/e22b766b11f6030dc2decdeff5c2fb1610768055603f9f3be88b6d192fb2/numpy-2.3.3-cp313-cp313t-win_amd64.whl",hashes = {sha256 = "4384a169c4d8f97195980815d6fcad04933a7e1ab3b530921c3fef7a1c63426d"}}, - {name = "numpy-2.3.3-cp313-cp313t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/7b/42/c2e2bc48c5e9b2a83423f99733950fbefd86f165b468a3d85d52b30bf782/numpy-2.3.3-cp313-cp313t-win_arm64.whl",hashes = {sha256 = "75370986cc0bc66f4ce5110ad35aae6d182cc4ce6433c40ad151f53690130bf1"}}, - {name = "numpy-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/51/5d/bb7fc075b762c96329147799e1bcc9176ab07ca6375ea976c475482ad5b3/numpy-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl",hashes = {sha256 = "cfdd09f9c84a1a934cde1eec2267f0a43a7cd44b2cca4ff95b7c0d14d144b0bf"}}, - {name = "numpy-2.3.3-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/6b/0e/c6211bb92af26517acd52125a237a92afe9c3124c6a68d3b9f81b62a0568/numpy-2.3.3-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "cb32e3cf0f762aee47ad1ddc6672988f7f27045b0783c887190545baba73aa25"}}, - {name = "numpy-2.3.3-cp312-cp312-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/22/f2/07bb754eb2ede9073f4054f7c0286b0d9d2e23982e090a80d478b26d35ca/numpy-2.3.3-cp312-cp312-macosx_14_0_arm64.whl",hashes = {sha256 = "396b254daeb0a57b1fe0ecb5e3cff6fa79a380fa97c8f7781a6d08cd429418fe"}}, - {name = "numpy-2.3.3-cp312-cp312-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/81/0a/afa51697e9fb74642f231ea36aca80fa17c8fb89f7a82abd5174023c3960/numpy-2.3.3-cp312-cp312-macosx_14_0_x86_64.whl",hashes = {sha256 = "067e3d7159a5d8f8a0b46ee11148fc35ca9b21f61e3c49fbd0a027450e65a33b"}}, - {name = "numpy-2.3.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/5d/f5/122d9cdb3f51c520d150fef6e87df9279e33d19a9611a87c0d2cf78a89f4/numpy-2.3.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "1c02d0629d25d426585fb2e45a66154081b9fa677bc92a881ff1d216bc9919a8"}}, - {name = "numpy-2.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/51/64/7de3c91e821a2debf77c92962ea3fe6ac2bc45d0778c1cbe15d4fce2fd94/numpy-2.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "d9192da52b9745f7f0766531dcfa978b7763916f158bb63bdb8a1eca0068ab20"}}, - {name = "numpy-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/30/e4/961a5fa681502cd0d68907818b69f67542695b74e3ceaa513918103b7e80/numpy-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "cd7de500a5b66319db419dc3c345244404a164beae0d0937283b907d8152e6ea"}}, - {name = "numpy-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/99/26/92c912b966e47fbbdf2ad556cb17e3a3088e2e1292b9833be1dfa5361a1a/numpy-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "93d4962d8f82af58f0b2eb85daaf1b3ca23fe0a85d0be8f1f2b7bb46034e56d7"}}, - {name = "numpy-2.3.3-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/17/b6/fc8f82cb3520768718834f310c37d96380d9dc61bfdaf05fe5c0b7653e01/numpy-2.3.3-cp312-cp312-win32.whl",hashes = {sha256 = "5534ed6b92f9b7dca6c0a19d6df12d41c68b991cef051d108f6dbff3babc4ebf"}}, - {name = "numpy-2.3.3-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/32/ee/de999f2625b80d043d6d2d628c07d0d5555a677a3cf78fdf868d409b8766/numpy-2.3.3-cp312-cp312-win_amd64.whl",hashes = {sha256 = "497d7cad08e7092dba36e3d296fe4c97708c93daf26643a1ae4b03f6294d30eb"}}, - {name = "numpy-2.3.3-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/49/6e/b479032f8a43559c383acb20816644f5f91c88f633d9271ee84f3b3a996c/numpy-2.3.3-cp312-cp312-win_arm64.whl",hashes = {sha256 = "ca0309a18d4dfea6fc6262a66d06c26cfe4640c3926ceec90e57791a82b6eee5"}}, -] -marker = "python_version ~= \"3.12\"" - -[packages.tool.pdm] -dependencies = [] - [[packages]] name = "setuptools" version = "80.9.0" @@ -728,7 +728,7 @@ wheels = [ {name = "tiktoken-0.12.0-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/f8/00/c6681c7f833dd410576183715a530437a9873fa910265817081f65f9105f/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "82991e04fc860afb933efb63957affc7ad54f83e2216fe7d319007dab1ba5892"}}, {name = "tiktoken-0.12.0-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/5f/d2/82e795a6a9bafa034bf26a58e68fe9a89eeaaa610d51dbeb22106ba04f0a/tiktoken-0.12.0-cp310-cp310-win_amd64.whl",hashes = {sha256 = "6fb2995b487c2e31acf0a9e17647e3b242235a20832642bb7a9d1a181c0c1bb1"}}, ] -marker = "\"recommended\" in extras" +marker = "\"openai\" in extras or \"recommended\" in extras" [packages.tool.pdm] dependencies = [ @@ -819,7 +819,7 @@ wheels = [ {name = "uvloop-0.21.0-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/bf/fe/5e94a977d058a54a19df95f12f7161ab6e323ad49f4dabc28822eb2df7ea/uvloop-0.21.0-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f"}}, {name = "uvloop-0.21.0-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/26/dd/c7179618e46092a77e036650c1f056041a028a35c4d76945089fcfc38af8/uvloop-0.21.0-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c"}}, ] -marker = "\"default\" in dependency_groups" +marker = "\"default\" in dependency_groups or \"perf\" in extras or \"recommended\" in extras" [packages.tool.pdm] dependencies = [] @@ -956,7 +956,86 @@ wheels = [ {name = "msgpack-1.1.2-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/4b/4f/05fcebd3b4977cb3d840f7ef6b77c51f8582086de5e642f3fefee35c86fc/msgpack-1.1.2-cp310-cp310-win32.whl",hashes = {sha256 = "e64c8d2f5e5d5fda7b842f55dec6133260ea8f53c4257d64494c534f306bf7a9"}}, {name = "msgpack-1.1.2-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/d0/3e/b4547e3a34210956382eed1c85935fff7e0f9b98be3106b3745d7dec9c5e/msgpack-1.1.2-cp310-cp310-win_amd64.whl",hashes = {sha256 = "db6192777d943bdaaafb6ba66d44bf65aa0e9c5616fa1d2da9bb08828c6b39aa"}}, ] -marker = "\"default\" in dependency_groups" +marker = "\"default\" in dependency_groups or \"perf\" in extras or \"recommended\" in extras" + +[packages.tool.pdm] +dependencies = [] + +[[packages]] +name = "msgspec" +version = "0.19.0" +requires-python = ">=3.9" +sdist = {name = "msgspec-0.19.0.tar.gz", url = "https://files.pythonhosted.org/packages/cf/9b/95d8ce458462b8b71b8a70fa94563b2498b89933689f3a7b8911edfae3d7/msgspec-0.19.0.tar.gz", hashes = {sha256 = "604037e7cd475345848116e89c553aa9a233259733ab51986ac924ab1b976f8e"}} +wheels = [ + {name = "msgspec-0.19.0-cp313-cp313-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/3c/cb/2842c312bbe618d8fefc8b9cedce37f773cdc8fa453306546dba2c21fd98/msgspec-0.19.0-cp313-cp313-macosx_10_13_x86_64.whl",hashes = {sha256 = "f12d30dd6266557aaaf0aa0f9580a9a8fbeadfa83699c487713e355ec5f0bd86"}}, + {name = "msgspec-0.19.0-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/58/95/c40b01b93465e1a5f3b6c7d91b10fb574818163740cc3acbe722d1e0e7e4/msgspec-0.19.0-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "82b2c42c1b9ebc89e822e7e13bbe9d17ede0c23c187469fdd9505afd5a481314"}}, + {name = "msgspec-0.19.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/e8/f0/5b764e066ce9aba4b70d1db8b087ea66098c7c27d59b9dd8a3532774d48f/msgspec-0.19.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "19746b50be214a54239aab822964f2ac81e38b0055cca94808359d779338c10e"}}, + {name = "msgspec-0.19.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/9d/87/bc14f49bc95c4cb0dd0a8c56028a67c014ee7e6818ccdce74a4862af259b/msgspec-0.19.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "60ef4bdb0ec8e4ad62e5a1f95230c08efb1f64f32e6e8dd2ced685bcc73858b5"}}, + {name = "msgspec-0.19.0-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/53/2f/2b1c2b056894fbaa975f68f81e3014bb447516a8b010f1bed3fb0e016ed7/msgspec-0.19.0-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "ac7f7c377c122b649f7545810c6cd1b47586e3aa3059126ce3516ac7ccc6a6a9"}}, + {name = "msgspec-0.19.0-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/aa/5a/4cd408d90d1417e8d2ce6a22b98a6853c1b4d7cb7669153e4424d60087f6/msgspec-0.19.0-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "a5bc1472223a643f5ffb5bf46ccdede7f9795078194f14edd69e3aab7020d327"}}, + {name = "msgspec-0.19.0-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/23/d8/f15b40611c2d5753d1abb0ca0da0c75348daf1252220e5dda2867bd81062/msgspec-0.19.0-cp313-cp313-win_amd64.whl",hashes = {sha256 = "317050bc0f7739cb30d257ff09152ca309bf5a369854bbf1e57dffc310c1f20f"}}, + {name = "msgspec-0.19.0-cp312-cp312-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/b2/5f/a70c24f075e3e7af2fae5414c7048b0e11389685b7f717bb55ba282a34a7/msgspec-0.19.0-cp312-cp312-macosx_10_13_x86_64.whl",hashes = {sha256 = "f98bd8962ad549c27d63845b50af3f53ec468b6318400c9f1adfe8b092d7b62f"}}, + {name = "msgspec-0.19.0-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/89/b0/1b9763938cfae12acf14b682fcf05c92855974d921a5a985ecc197d1c672/msgspec-0.19.0-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "43bbb237feab761b815ed9df43b266114203f53596f9b6e6f00ebd79d178cdf2"}}, + {name = "msgspec-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/87/81/0c8c93f0b92c97e326b279795f9c5b956c5a97af28ca0fbb9fd86c83737a/msgspec-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "4cfc033c02c3e0aec52b71710d7f84cb3ca5eb407ab2ad23d75631153fdb1f12"}}, + {name = "msgspec-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/d0/ef/c5422ce8af73928d194a6606f8ae36e93a52fd5e8df5abd366903a5ca8da/msgspec-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "d911c442571605e17658ca2b416fd8579c5050ac9adc5e00c2cb3126c97f73bc"}}, + {name = "msgspec-0.19.0-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/19/2b/4137bc2ed45660444842d042be2cf5b18aa06efd2cda107cff18253b9653/msgspec-0.19.0-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "757b501fa57e24896cf40a831442b19a864f56d253679f34f260dcb002524a6c"}}, + {name = "msgspec-0.19.0-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/9d/e6/8ad51bdc806aac1dc501e8fe43f759f9ed7284043d722b53323ea421c360/msgspec-0.19.0-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "5f0f65f29b45e2816d8bded36e6b837a4bf5fb60ec4bc3c625fa2c6da4124537"}}, + {name = "msgspec-0.19.0-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/b1/ef/27dd35a7049c9a4f4211c6cd6a8c9db0a50647546f003a5867827ec45391/msgspec-0.19.0-cp312-cp312-win_amd64.whl",hashes = {sha256 = "067f0de1c33cfa0b6a8206562efdf6be5985b988b53dd244a8e06f993f27c8c0"}}, +] +marker = "\"perf\" in extras or \"recommended\" in extras" + +[packages.tool.pdm] +dependencies = [] + +[[packages]] +name = "orjson" +version = "3.11.3" +requires-python = ">=3.9" +sdist = {name = "orjson-3.11.3.tar.gz", url = "https://files.pythonhosted.org/packages/be/4d/8df5f83256a809c22c4d6792ce8d43bb503be0fb7a8e4da9025754b09658/orjson-3.11.3.tar.gz", hashes = {sha256 = "1c0603b1d2ffcd43a411d64797a19556ef76958aef1c182f22dc30860152a98a"}} +wheels = [ + {name = "orjson-3.11.3-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl",url = "https://files.pythonhosted.org/packages/ef/77/d3b1fef1fc6aaeed4cbf3be2b480114035f4df8fa1a99d2dac1d40d6e924/orjson-3.11.3-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl",hashes = {sha256 = "cf4b81227ec86935568c7edd78352a92e97af8da7bd70bdfdaa0d2e0011a1ab4"}}, + {name = "orjson-3.11.3-cp314-cp314-macosx_15_0_arm64.whl",url = "https://files.pythonhosted.org/packages/e4/6d/468d21d49bb12f900052edcfbf52c292022d0a323d7828dc6376e6319703/orjson-3.11.3-cp314-cp314-macosx_15_0_arm64.whl",hashes = {sha256 = "bc8bc85b81b6ac9fc4dae393a8c159b817f4c2c9dee5d12b773bddb3b95fc07e"}}, + {name = "orjson-3.11.3-cp314-cp314-manylinux_2_34_aarch64.whl",url = "https://files.pythonhosted.org/packages/67/46/1e2588700d354aacdf9e12cc2d98131fb8ac6f31ca65997bef3863edb8ff/orjson-3.11.3-cp314-cp314-manylinux_2_34_aarch64.whl",hashes = {sha256 = "88dcfc514cfd1b0de038443c7b3e6a9797ffb1b3674ef1fd14f701a13397f82d"}}, + {name = "orjson-3.11.3-cp314-cp314-manylinux_2_34_x86_64.whl",url = "https://files.pythonhosted.org/packages/3b/94/11137c9b6adb3779f1b34fd98be51608a14b430dbc02c6d41134fbba484c/orjson-3.11.3-cp314-cp314-manylinux_2_34_x86_64.whl",hashes = {sha256 = "d61cd543d69715d5fc0a690c7c6f8dcc307bc23abef9738957981885f5f38229"}}, + {name = "orjson-3.11.3-cp314-cp314-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/10/61/dccedcf9e9bcaac09fdabe9eaee0311ca92115699500efbd31950d878833/orjson-3.11.3-cp314-cp314-musllinux_1_2_aarch64.whl",hashes = {sha256 = "2b7b153ed90ababadbef5c3eb39549f9476890d339cf47af563aea7e07db2451"}}, + {name = "orjson-3.11.3-cp314-cp314-musllinux_1_2_armv7l.whl",url = "https://files.pythonhosted.org/packages/0e/fd/0e935539aa7b08b3ca0f817d73034f7eb506792aae5ecc3b7c6e679cdf5f/orjson-3.11.3-cp314-cp314-musllinux_1_2_armv7l.whl",hashes = {sha256 = "7909ae2460f5f494fecbcd10613beafe40381fd0316e35d6acb5f3a05bfda167"}}, + {name = "orjson-3.11.3-cp314-cp314-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/4a/2b/50ae1a5505cd1043379132fdb2adb8a05f37b3e1ebffe94a5073321966fd/orjson-3.11.3-cp314-cp314-musllinux_1_2_i686.whl",hashes = {sha256 = "2030c01cbf77bc67bee7eef1e7e31ecf28649353987775e3583062c752da0077"}}, + {name = "orjson-3.11.3-cp314-cp314-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/cd/1d/a473c158e380ef6f32753b5f39a69028b25ec5be331c2049a2201bde2e19/orjson-3.11.3-cp314-cp314-musllinux_1_2_x86_64.whl",hashes = {sha256 = "a0169ebd1cbd94b26c7a7ad282cf5c2744fce054133f959e02eb5265deae1872"}}, + {name = "orjson-3.11.3-cp314-cp314-win32.whl",url = "https://files.pythonhosted.org/packages/da/09/17d9d2b60592890ff7382e591aa1d9afb202a266b180c3d4049b1ec70e4a/orjson-3.11.3-cp314-cp314-win32.whl",hashes = {sha256 = "0c6d7328c200c349e3a4c6d8c83e0a5ad029bdc2d417f234152bf34842d0fc8d"}}, + {name = "orjson-3.11.3-cp314-cp314-win_amd64.whl",url = "https://files.pythonhosted.org/packages/15/58/358f6846410a6b4958b74734727e582ed971e13d335d6c7ce3e47730493e/orjson-3.11.3-cp314-cp314-win_amd64.whl",hashes = {sha256 = "317bbe2c069bbc757b1a2e4105b64aacd3bc78279b66a6b9e51e846e4809f804"}}, + {name = "orjson-3.11.3-cp314-cp314-win_arm64.whl",url = "https://files.pythonhosted.org/packages/28/01/d6b274a0635be0468d4dbd9cafe80c47105937a0d42434e805e67cd2ed8b/orjson-3.11.3-cp314-cp314-win_arm64.whl",hashes = {sha256 = "e8f6a7a27d7b7bec81bd5924163e9af03d49bbb63013f107b48eb5d16db711bc"}}, + {name = "orjson-3.11.3-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl",url = "https://files.pythonhosted.org/packages/fc/79/8932b27293ad35919571f77cb3693b5906cf14f206ef17546052a241fdf6/orjson-3.11.3-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl",hashes = {sha256 = "af40c6612fd2a4b00de648aa26d18186cd1322330bd3a3cc52f87c699e995810"}}, + {name = "orjson-3.11.3-cp313-cp313-macosx_15_0_arm64.whl",url = "https://files.pythonhosted.org/packages/1c/82/cb93cd8cf132cd7643b30b6c5a56a26c4e780c7a145db6f83de977b540ce/orjson-3.11.3-cp313-cp313-macosx_15_0_arm64.whl",hashes = {sha256 = "9f1587f26c235894c09e8b5b7636a38091a9e6e7fe4531937534749c04face43"}}, + {name = "orjson-3.11.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/a4/b8/2d9eb181a9b6bb71463a78882bcac1027fd29cf62c38a40cc02fc11d3495/orjson-3.11.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "61dcdad16da5bb486d7227a37a2e789c429397793a6955227cedbd7252eb5a27"}}, + {name = "orjson-3.11.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",url = "https://files.pythonhosted.org/packages/b4/14/a0e971e72d03b509190232356d54c0f34507a05050bd026b8db2bf2c192c/orjson-3.11.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",hashes = {sha256 = "11c6d71478e2cbea0a709e8a06365fa63da81da6498a53e4c4f065881d21ae8f"}}, + {name = "orjson-3.11.3-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/8e/af/dc74536722b03d65e17042cc30ae586161093e5b1f29bccda24765a6ae47/orjson-3.11.3-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "ff94112e0098470b665cb0ed06efb187154b63649403b8d5e9aedeb482b4548c"}}, + {name = "orjson-3.11.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/62/e6/7a3b63b6677bce089fe939353cda24a7679825c43a24e49f757805fc0d8a/orjson-3.11.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "ae8b756575aaa2a855a75192f356bbda11a89169830e1439cfb1a3e1a6dde7be"}}, + {name = "orjson-3.11.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/fc/cd/ce2ab93e2e7eaf518f0fd15e3068b8c43216c8a44ed82ac2b79ce5cef72d/orjson-3.11.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "c9416cc19a349c167ef76135b2fe40d03cea93680428efee8771f3e9fb66079d"}}, + {name = "orjson-3.11.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/d0/b4/f98355eff0bd1a38454209bbc73372ce351ba29933cb3e2eba16c04b9448/orjson-3.11.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "b822caf5b9752bc6f246eb08124c3d12bf2175b66ab74bac2ef3bbf9221ce1b2"}}, + {name = "orjson-3.11.3-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/eb/92/8f5182d7bc2a1bed46ed960b61a39af8389f0ad476120cd99e67182bfb6d/orjson-3.11.3-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "414f71e3bdd5573893bf5ecdf35c32b213ed20aa15536fe2f588f946c318824f"}}, + {name = "orjson-3.11.3-cp313-cp313-musllinux_1_2_armv7l.whl",url = "https://files.pythonhosted.org/packages/1a/60/c41ca753ce9ffe3d0f67b9b4c093bdd6e5fdb1bc53064f992f66bb99954d/orjson-3.11.3-cp313-cp313-musllinux_1_2_armv7l.whl",hashes = {sha256 = "828e3149ad8815dc14468f36ab2a4b819237c155ee1370341b91ea4c8672d2ee"}}, + {name = "orjson-3.11.3-cp313-cp313-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/dd/13/e4a4f16d71ce1868860db59092e78782c67082a8f1dc06a3788aef2b41bc/orjson-3.11.3-cp313-cp313-musllinux_1_2_i686.whl",hashes = {sha256 = "ac9e05f25627ffc714c21f8dfe3a579445a5c392a9c8ae7ba1d0e9fb5333f56e"}}, + {name = "orjson-3.11.3-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/8d/8b/bafb7f0afef9344754a3a0597a12442f1b85a048b82108ef2c956f53babd/orjson-3.11.3-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "e44fbe4000bd321d9f3b648ae46e0196d21577cf66ae684a96ff90b1f7c93633"}}, + {name = "orjson-3.11.3-cp313-cp313-win32.whl",url = "https://files.pythonhosted.org/packages/60/d4/bae8e4f26afb2c23bea69d2f6d566132584d1c3a5fe89ee8c17b718cab67/orjson-3.11.3-cp313-cp313-win32.whl",hashes = {sha256 = "2039b7847ba3eec1f5886e75e6763a16e18c68a63efc4b029ddf994821e2e66b"}}, + {name = "orjson-3.11.3-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/88/76/224985d9f127e121c8cad882cea55f0ebe39f97925de040b75ccd4b33999/orjson-3.11.3-cp313-cp313-win_amd64.whl",hashes = {sha256 = "29be5ac4164aa8bdcba5fa0700a3c9c316b411d8ed9d39ef8a882541bd452fae"}}, + {name = "orjson-3.11.3-cp313-cp313-win_arm64.whl",url = "https://files.pythonhosted.org/packages/e2/cf/0dce7a0be94bd36d1346be5067ed65ded6adb795fdbe3abd234c8d576d01/orjson-3.11.3-cp313-cp313-win_arm64.whl",hashes = {sha256 = "18bd1435cb1f2857ceb59cfb7de6f92593ef7b831ccd1b9bfb28ca530e539dce"}}, + {name = "orjson-3.11.3-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl",url = "https://files.pythonhosted.org/packages/3d/b0/a7edab2a00cdcb2688e1c943401cb3236323e7bfd2839815c6131a3742f4/orjson-3.11.3-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl",hashes = {sha256 = "8c752089db84333e36d754c4baf19c0e1437012242048439c7e80eb0e6426e3b"}}, + {name = "orjson-3.11.3-cp312-cp312-macosx_15_0_arm64.whl",url = "https://files.pythonhosted.org/packages/e1/c6/ff4865a9cc398a07a83342713b5932e4dc3cb4bf4bc04e8f83dedfc0d736/orjson-3.11.3-cp312-cp312-macosx_15_0_arm64.whl",hashes = {sha256 = "9b8761b6cf04a856eb544acdd82fc594b978f12ac3602d6374a7edb9d86fd2c2"}}, + {name = "orjson-3.11.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/6e/e6/e00bea2d9472f44fe8794f523e548ce0ad51eb9693cf538a753a27b8bda4/orjson-3.11.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "8b13974dc8ac6ba22feaa867fc19135a3e01a134b4f7c9c28162fed4d615008a"}}, + {name = "orjson-3.11.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",url = "https://files.pythonhosted.org/packages/54/31/9fbb78b8e1eb3ac605467cb846e1c08d0588506028b37f4ee21f978a51d4/orjson-3.11.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",hashes = {sha256 = "f83abab5bacb76d9c821fd5c07728ff224ed0e52d7a71b7b3de822f3df04e15c"}}, + {name = "orjson-3.11.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/36/88/b0604c22af1eed9f98d709a96302006915cfd724a7ebd27d6dd11c22d80b/orjson-3.11.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "e6fbaf48a744b94091a56c62897b27c31ee2da93d826aa5b207131a1e13d4064"}}, + {name = "orjson-3.11.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/0e/9d/1c1238ae9fffbfed51ba1e507731b3faaf6b846126a47e9649222b0fd06f/orjson-3.11.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "bc779b4f4bba2847d0d2940081a7b6f7b5877e05408ffbb74fa1faf4a136c424"}}, + {name = "orjson-3.11.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/a3/b5/c06f1b090a1c875f337e21dd71943bc9d84087f7cdf8c6e9086902c34e42/orjson-3.11.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "bd4b909ce4c50faa2192da6bb684d9848d4510b736b0611b6ab4020ea6fd2d23"}}, + {name = "orjson-3.11.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/a0/26/5f028c7d81ad2ebbf84414ba6d6c9cac03f22f5cd0d01eb40fb2d6a06b07/orjson-3.11.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "524b765ad888dc5518bbce12c77c2e83dee1ed6b0992c1790cc5fb49bb4b6667"}}, + {name = "orjson-3.11.3-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/fe/d4/b8df70d9cfb56e385bf39b4e915298f9ae6c61454c8154a0f5fd7efcd42e/orjson-3.11.3-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "84fd82870b97ae3cdcea9d8746e592b6d40e1e4d4527835fc520c588d2ded04f"}}, + {name = "orjson-3.11.3-cp312-cp312-musllinux_1_2_armv7l.whl",url = "https://files.pythonhosted.org/packages/da/5e/afe6a052ebc1a4741c792dd96e9f65bf3939d2094e8b356503b68d48f9f5/orjson-3.11.3-cp312-cp312-musllinux_1_2_armv7l.whl",hashes = {sha256 = "fbecb9709111be913ae6879b07bafd4b0785b44c1eb5cac8ac76da048b3885a1"}}, + {name = "orjson-3.11.3-cp312-cp312-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/f8/90/7bbabafeb2ce65915e9247f14a56b29c9334003536009ef5b122783fe67e/orjson-3.11.3-cp312-cp312-musllinux_1_2_i686.whl",hashes = {sha256 = "9dba358d55aee552bd868de348f4736ca5a4086d9a62e2bfbbeeb5629fe8b0cc"}}, + {name = "orjson-3.11.3-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/27/b3/2d703946447da8b093350570644a663df69448c9d9330e5f1d9cce997f20/orjson-3.11.3-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "eabcf2e84f1d7105f84580e03012270c7e97ecb1fb1618bda395061b2a84a049"}}, + {name = "orjson-3.11.3-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/38/70/b14dcfae7aff0e379b0119c8a812f8396678919c431efccc8e8a0263e4d9/orjson-3.11.3-cp312-cp312-win32.whl",hashes = {sha256 = "3782d2c60b8116772aea8d9b7905221437fdf53e7277282e8d8b07c220f96cca"}}, + {name = "orjson-3.11.3-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/35/b8/9e3127d65de7fff243f7f3e53f59a531bf6bb295ebe5db024c2503cc0726/orjson-3.11.3-cp312-cp312-win_amd64.whl",hashes = {sha256 = "79b44319268af2eaa3e315b92298de9a0067ade6e6003ddaef72f8e0bedb94f1"}}, + {name = "orjson-3.11.3-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/51/92/a946e737d4d8a7fd84a606aba96220043dcc7d6988b9e7551f7f6d5ba5ad/orjson-3.11.3-cp312-cp312-win_arm64.whl",hashes = {sha256 = "0e92a4e83341ef79d835ca21b8bd13e27c859e4e9e4d7b63defc6e58462a3710"}}, +] +marker = "\"perf\" in extras or \"recommended\" in extras" [packages.tool.pdm] dependencies = [] @@ -1126,6 +1205,47 @@ dependencies = [ "setuptools>=70.1.0", ] +[[packages]] +name = "torch" +version = "2.9.0+cpu" +requires-python = ">=3.10" +wheels = [ + {name = "torch-2.9.0+cpu-cp314-cp314-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp314-cp314-manylinux_2_28_aarch64.whl",hashes = {sha256 = "44aadb735774d4a99525d2ec29126b23016c44a07b02ce6c237dfa61a223dd52"}}, + {name = "torch-2.9.0+cpu-cp314-cp314-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp314-cp314-manylinux_2_28_x86_64.whl",hashes = {sha256 = "b355e07b7f0c369cb031adfcbff5c37a609abcea091b918a39886412afd2e07d"}}, + {name = "torch-2.9.0+cpu-cp314-cp314-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp314-cp314-win_amd64.whl",hashes = {sha256 = "c2698999361d73c2d25d7cc8a787130188d49b183abb18b554228daa102e1594"}}, + {name = "torch-2.9.0+cpu-cp314-cp314t-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp314-cp314t-manylinux_2_28_aarch64.whl",hashes = {sha256 = "fa0d1373d04b30ff8f12d542135d292f1a1ddb7c0d852a3d487a320360e5dab9"}}, + {name = "torch-2.9.0+cpu-cp314-cp314t-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp314-cp314t-manylinux_2_28_x86_64.whl",hashes = {sha256 = "2f49bb57a5fe0dc7f8e73ea9e5d36ebda2ea25b8a714a788f0fc2fc47d20a830"}}, + {name = "torch-2.9.0+cpu-cp314-cp314t-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp314-cp314t-win_amd64.whl",hashes = {sha256 = "3a60d1ecf27a9cce839b3aa665b26f0af1b1007b9c9f1e7f597f6b7bdf107617"}}, + {name = "torch-2.9.0-cp314-cp314-macosx_11_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "d8e2ab7f86010330bdcc39c8b2c795590cc75e37df4823cdaee2c98d6e3ff4a3"}}, + {name = "torch-2.9.0-cp314-cp314t-macosx_11_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0-cp314-cp314t-macosx_11_0_arm64.whl",hashes = {sha256 = "a3e859039c985d8e3ea60d7a54ca7e97ea2ae15e31beced4f3260128a161bb01"}}, + {name = "torch-2.9.0+cpu-cp313-cp313-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl",hashes = {sha256 = "be4438d8dad7f0d5a5e54f0feef8a893446894ec87f102bb1d82dcc4518542e4"}}, + {name = "torch-2.9.0+cpu-cp313-cp313-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl",hashes = {sha256 = "6c9b217584400963d5b4daddb3711ec7a3778eab211e18654fba076cce3b8682"}}, + {name = "torch-2.9.0+cpu-cp313-cp313-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp313-cp313-win_amd64.whl",hashes = {sha256 = "728372e3f58c5826445f677746e5311c1935c1a7c59599f73a49ded850e038e8"}}, + {name = "torch-2.9.0+cpu-cp313-cp313-win_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp313-cp313-win_arm64.whl",hashes = {sha256 = "95e56c26f919fbb98f16e7a0b87af494b893f9da9a65a020f17a01c13e520a81"}}, + {name = "torch-2.9.0+cpu-cp313-cp313t-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl",hashes = {sha256 = "6c777160288b08555820781ae0f3a2c67a59bd24b065e88ca1ec20e2f9dc8ac7"}}, + {name = "torch-2.9.0+cpu-cp313-cp313t-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl",hashes = {sha256 = "528fd338311f31c9fb18038cafd00e6eae0bf5ad5577521701acb62510753d18"}}, + {name = "torch-2.9.0+cpu-cp313-cp313t-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp313-cp313t-win_amd64.whl",hashes = {sha256 = "d572863990e7d2762b547735ef589f6350d9eb4e441d38753a1c33636698cf4c"}}, + {name = "torch-2.9.0-cp313-cp313t-macosx_11_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0-cp313-cp313t-macosx_11_0_arm64.whl",hashes = {sha256 = "259548471194ab63d7ea273873053a6e3cc23530c1510f01e9d7ad259187bbd0"}}, + {name = "torch-2.9.0-cp313-none-macosx_11_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0-cp313-none-macosx_11_0_arm64.whl",hashes = {sha256 = "e24836d968b54ef4dfb05594001a61958711ac9224026291e4e3f92f83a6fd7f"}}, + {name = "torch-2.9.0+cpu-cp312-cp312-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl",hashes = {sha256 = "3a651434ae1248b0568c12b5f9e3acc8942eb28378d9d04a79302938b68c6f24"}}, + {name = "torch-2.9.0+cpu-cp312-cp312-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl",hashes = {sha256 = "28f6eb31b08180a5c5e98d5bc14eef6909c9f5a1dbff9632c3e02a8773449349"}}, + {name = "torch-2.9.0+cpu-cp312-cp312-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp312-cp312-win_amd64.whl",hashes = {sha256 = "e438061b87ec7dd6018fca9f975219889aa0a3f6cdc3ea10dd0ae2bc7f1c47ce"}}, + {name = "torch-2.9.0+cpu-cp312-cp312-win_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp312-cp312-win_arm64.whl",hashes = {sha256 = "eb13ff1c34e338d722e76a4fd83b8d282782505bd1b99af4b3c32da66eba6eb4"}}, + {name = "torch-2.9.0-cp312-none-macosx_11_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0-cp312-none-macosx_11_0_arm64.whl",hashes = {sha256 = "4de0ed8cbc457a506dbca40376e206a29efee10756a00f1f3404bf67ad737d04"}}, +] +marker = "\"default\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [ + "filelock", + "typing-extensions>=4.10.0", + "setuptools; python_version >= \"3.12\"", + "sympy>=1.13.3", + "networkx>=2.5.1", + "jinja2", + "fsspec>=0.8.5", +] + [[packages]] name = "transformers" version = "4.57.0" @@ -1412,7 +1532,7 @@ sdist = {name = "requests-2.32.5.tar.gz", url = "https://files.pythonhosted.org/ wheels = [ {name = "requests-2.32.5-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl",hashes = {sha256 = "2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras or \"recommended\" in extras" +marker = "\"default\" in dependency_groups or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras" [packages.tool.pdm] dependencies = [ @@ -1430,7 +1550,7 @@ sdist = {name = "urllib3-2.5.0.tar.gz", url = "https://files.pythonhosted.org/pa wheels = [ {name = "urllib3-2.5.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl",hashes = {sha256 = "e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras or \"recommended\" in extras" +marker = "\"default\" in dependency_groups or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras" [packages.tool.pdm] dependencies = [] @@ -1526,7 +1646,7 @@ wheels = [ {name = "charset_normalizer-3.4.3-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/2a/91/26c3036e62dfe8de8061182d33be5025e2424002125c9500faff74a6735e/charset_normalizer-3.4.3-cp310-cp310-win32.whl",hashes = {sha256 = "d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f"}}, {name = "charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/e2/c6/f05db471f81af1fa01839d44ae2a8bfeec8d2a8b4590f16c4e7393afd323/charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl",hashes = {sha256 = "c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras or \"recommended\" in extras" +marker = "\"default\" in dependency_groups or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras" [packages.tool.pdm] dependencies = [] @@ -1565,7 +1685,7 @@ sdist = {name = "filelock-3.20.0.tar.gz", url = "https://files.pythonhosted.org/ wheels = [ {name = "filelock-3.20.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl",hashes = {sha256 = "339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras or \"recommended\" in extras" +marker = "\"default\" in dependency_groups or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras" [packages.tool.pdm] dependencies = [] @@ -1916,7 +2036,7 @@ sdist = {name = "idna-3.10.tar.gz", url = "https://files.pythonhosted.org/packag wheels = [ {name = "idna-3.10-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl",hashes = {sha256 = "946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras or \"recommended\" in extras" +marker = "\"default\" in dependency_groups or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras" [packages.tool.pdm] dependencies = [] @@ -2042,7 +2162,7 @@ wheels = [ {name = "regex-2025.9.18-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/59/39/aeb11a4ae68faaec2498512cadae09f2d8a91f1f65730fe62b9bffeea150/regex-2025.9.18-cp310-cp310-win_amd64.whl",hashes = {sha256 = "47acd811589301298c49db2c56bde4f9308d6396da92daf99cba781fa74aa450"}}, {name = "regex-2025.9.18-cp310-cp310-win_arm64.whl",url = "https://files.pythonhosted.org/packages/29/04/37f2d3fc334a1031fc2767c9d89cec13c2e72207c7e7f6feae8a47f4e149/regex-2025.9.18-cp310-cp310-win_arm64.whl",hashes = {sha256 = "16bd2944e77522275e5ee36f867e19995bcaa533dcb516753a26726ac7285442"}}, ] -marker = "\"default\" in dependency_groups or \"recommended\" in extras" +marker = "\"default\" in dependency_groups or \"openai\" in extras or \"recommended\" in extras" [packages.tool.pdm] dependencies = [] @@ -2627,7 +2747,7 @@ sdist = {name = "certifi-2025.10.5.tar.gz", url = "https://files.pythonhosted.or wheels = [ {name = "certifi-2025.10.5-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl",hashes = {sha256 = "0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras or \"recommended\" in extras" +marker = "\"default\" in dependency_groups or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras" [packages.tool.pdm] dependencies = [] @@ -2861,7 +2981,7 @@ sdist = {name = "jinja2-3.1.6.tar.gz", url = "https://files.pythonhosted.org/pac wheels = [ {name = "jinja2-3.1.6-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl",hashes = {sha256 = "85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}}, ] -marker = "\"dev\" in extras" +marker = "\"default\" in dependency_groups or \"dev\" in extras" [packages.tool.pdm] dependencies = [ @@ -2991,7 +3111,7 @@ wheels = [ {name = "lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/77/a2/585a28fe3e67daa1cf2f06f34490d556d121c25d500b10082a7db96e3bcd/lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "2613e67de13d619fd283d58bda40bff0ee07739f624ffee8b13b631abf33083d"}}, {name = "lxml-6.0.2-pp310-pypy310_pp73-win_amd64.whl",url = "https://files.pythonhosted.org/packages/7b/d9/a57dd8bcebd7c69386c20263830d4fa72d27e6b72a229ef7a48e88952d9a/lxml-6.0.2-pp310-pypy310_pp73-win_amd64.whl",hashes = {sha256 = "24a8e756c982c001ca8d59e87c80c4d9dcd4d9b44a4cbeb8d9be4482c514d41d"}}, ] -marker = "\"recommended\" in extras" +marker = "\"openai\" in extras or \"recommended\" in extras" [packages.tool.pdm] dependencies = [] @@ -3080,7 +3200,7 @@ wheels = [ {name = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl",hashes = {sha256 = "c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"}}, {name = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl",url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl",hashes = {sha256 = "e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"}}, ] -marker = "\"dev\" in extras" +marker = "\"default\" in dependency_groups or \"dev\" in extras" [packages.tool.pdm] dependencies = [] @@ -3146,6 +3266,19 @@ marker = "\"dev\" in extras" [packages.tool.pdm] dependencies = [] +[[packages]] +name = "networkx" +version = "3.5" +requires-python = ">=3.11" +sdist = {name = "networkx-3.5.tar.gz", url = "https://files.pythonhosted.org/packages/6c/4f/ccdb8ad3a38e583f214547fd2f7ff1fc160c43a75af88e6aec213404b96a/networkx-3.5.tar.gz", hashes = {sha256 = "d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037"}} +wheels = [ + {name = "networkx-3.5-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl",hashes = {sha256 = "0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec"}}, +] +marker = "python_version ~= \"3.12\" and \"default\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [] + [[packages]] name = "nodeenv" version = "1.9.1" @@ -3240,7 +3373,7 @@ wheels = [ {name = "pycryptodomex-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/6e/c3/1de4f7631fea8a992a44ba632aa40e0008764c0fb9bf2854b0acf78c2cf2/pycryptodomex-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "fdfac7cda115bca3a5abb2f9e43bc2fb66c2b65ab074913643803ca7083a79ea"}}, {name = "pycryptodomex-3.23.0-pp310-pypy310_pp73-win_amd64.whl",url = "https://files.pythonhosted.org/packages/f2/5f/af7da8e6f1e42b52f44a24d08b8e4c726207434e2593732d39e7af5e7256/pycryptodomex-3.23.0-pp310-pypy310_pp73-win_amd64.whl",hashes = {sha256 = "14c37aaece158d0ace436f76a7bb19093db3b4deade9797abfc39ec6cd6cc2fe"}}, ] -marker = "\"recommended\" in extras" +marker = "\"openai\" in extras or \"recommended\" in extras" [packages.tool.pdm] dependencies = [] @@ -3351,6 +3484,33 @@ marker = "\"dev\" in extras" [packages.tool.pdm] dependencies = [] +[[packages]] +name = "sympy" +version = "1.14.0" +requires-python = ">=3.9" +sdist = {name = "sympy-1.14.0.tar.gz", url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hashes = {sha256 = "d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517"}} +wheels = [ + {name = "sympy-1.14.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl",hashes = {sha256 = "e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5"}}, +] +marker = "\"default\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [ + "mpmath<1.4,>=1.1.0", +] + +[[packages]] +name = "mpmath" +version = "1.3.0" +sdist = {name = "mpmath-1.3.0.tar.gz", url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hashes = {sha256 = "7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f"}} +wheels = [ + {name = "mpmath-1.3.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl",hashes = {sha256 = "a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"}}, +] +marker = "\"default\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [] + [[packages]] name = "tracerite" version = "1.1.3" @@ -4060,38 +4220,6 @@ marker = "\"default\" in dependency_groups" [packages.tool.pdm] dependencies = [] -[[packages]] -name = "scipy" -version = "1.15.3" -requires-python = ">=3.10" -sdist = {name = "scipy-1.15.3.tar.gz", url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hashes = {sha256 = "eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf"}} -wheels = [ - {name = "scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl",hashes = {sha256 = "993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b"}}, - {name = "scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl",url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl",hashes = {sha256 = "34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba"}}, - {name = "scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl",hashes = {sha256 = "3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65"}}, - {name = "scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl",hashes = {sha256 = "6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1"}}, - {name = "scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889"}}, - {name = "scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982"}}, - {name = "scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9"}}, - {name = "scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594"}}, - {name = "scipy-1.15.3-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl",hashes = {sha256 = "ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb"}}, - {name = "scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl",hashes = {sha256 = "a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c"}}, - {name = "scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl",url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl",hashes = {sha256 = "ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253"}}, - {name = "scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl",hashes = {sha256 = "aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f"}}, - {name = "scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl",hashes = {sha256 = "1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92"}}, - {name = "scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82"}}, - {name = "scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40"}}, - {name = "scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e"}}, - {name = "scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c"}}, - {name = "scipy-1.15.3-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl",hashes = {sha256 = "9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13"}}, -] -marker = "python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"dev\" in extras" - -[packages.tool.pdm] -dependencies = [ - "numpy<2.5,>=1.23.5", -] - [[packages]] name = "numpy" version = "2.2.6" @@ -4128,6 +4256,38 @@ marker = "python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \" [packages.tool.pdm] dependencies = [] +[[packages]] +name = "scipy" +version = "1.15.3" +requires-python = ">=3.10" +sdist = {name = "scipy-1.15.3.tar.gz", url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hashes = {sha256 = "eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf"}} +wheels = [ + {name = "scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl",hashes = {sha256 = "993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b"}}, + {name = "scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl",url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl",hashes = {sha256 = "34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba"}}, + {name = "scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl",hashes = {sha256 = "3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65"}}, + {name = "scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl",hashes = {sha256 = "6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1"}}, + {name = "scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889"}}, + {name = "scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982"}}, + {name = "scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9"}}, + {name = "scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594"}}, + {name = "scipy-1.15.3-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl",hashes = {sha256 = "ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb"}}, + {name = "scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl",hashes = {sha256 = "a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c"}}, + {name = "scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl",url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl",hashes = {sha256 = "ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253"}}, + {name = "scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl",hashes = {sha256 = "aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f"}}, + {name = "scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl",hashes = {sha256 = "1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92"}}, + {name = "scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82"}}, + {name = "scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40"}}, + {name = "scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e"}}, + {name = "scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c"}}, + {name = "scipy-1.15.3-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl",hashes = {sha256 = "9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13"}}, +] +marker = "python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"dev\" in extras" + +[packages.tool.pdm] +dependencies = [ + "numpy<2.5,>=1.23.5", +] + [[packages]] name = "tomli" version = "2.3.0" @@ -4206,6 +4366,19 @@ dependencies = [ "typing-extensions>=3.6.4; python_version < \"3.8\"", ] +[[packages]] +name = "networkx" +version = "3.4.2" +requires-python = ">=3.10" +sdist = {name = "networkx-3.4.2.tar.gz", url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hashes = {sha256 = "307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1"}} +wheels = [ + {name = "networkx-3.4.2-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl",hashes = {sha256 = "df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f"}}, +] +marker = "python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"default\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [] + [[packages]] name = "zipp" version = "3.23.0" @@ -4220,11 +4393,11 @@ marker = "python_full_version >= \"3.10.0\" and python_full_version < \"3.10.2\" dependencies = [] [tool.pdm] -hashes = {sha256 = "624646aafaf5561776673cdfb44330f8a295ed590670600bc9000c6dcdd8019b"} +hashes = {sha256 = "02a6fea936a5d6d81a623959518d50540f47a5756f7eb12720848d60a333e3a6"} strategy = ["inherit_metadata", "static_urls"] [[tool.pdm.targets]] requires_python = "~=3.12" [[tool.pdm.targets]] -requires_python = ">=3.10.0,<3.12" \ No newline at end of file +requires_python = ">=3.10.0,<3.12" diff --git a/pyproject.toml b/pyproject.toml index 5135edad..9e8cb813 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,24 +62,23 @@ dependencies = [ "httpx[http2]<1.0.0", "loguru", "msgpack", - "numpy<2.0.0", + "numpy>=2.0.0", "pillow", "protobuf", "pydantic>=2.11.7", "pydantic-settings>=2.0.0", - "pydub", "pyyaml>=6.0.0", "rich", "sanic", "transformers", "uvloop>=0.18", - "librosa>=0.11.0", "torch", ] [project.optional-dependencies] perf = ["orjson", "msgpack", "msgspec", "uvloop"] openai = ["tiktoken>=0.11.0", "blobfile>=3.1.0"] +multimodal = ["datasets[audio,vision]>=4.1.0", "torchcodec"] recommended = ["guidellm[perf,openai]"] dev = [ # build From 23d65ed5defde92124bb8670127b0932b243bd7c Mon Sep 17 00:00:00 2001 From: Samuel Monson Date: Tue, 14 Oct 2025 16:48:35 -0400 Subject: [PATCH 38/57] Add all group for extras Signed-off-by: Samuel Monson --- pylock.toml | 178 ++++++++++++++++++++++++------------------------- pyproject.toml | 9 ++- 2 files changed, 97 insertions(+), 90 deletions(-) diff --git a/pylock.toml b/pylock.toml index 779ab1c8..e81719ce 100644 --- a/pylock.toml +++ b/pylock.toml @@ -6,11 +6,29 @@ environments = [ "python_version ~= \"3.12\"", "python_full_version >= \"3.10.0\" and python_version < \"3.12\"", ] -extras = ["dev", "openai", "perf", "recommended"] +extras = ["all", "dev", "openai", "perf", "recommended"] dependency-groups = ["default"] default-groups = ["default"] created-by = "pdm" +[[packages]] +name = "blobfile" +version = "3.1.0" +requires-python = ">=3.8.0" +sdist = {name = "blobfile-3.1.0.tar.gz", url = "https://files.pythonhosted.org/packages/f0/6d/2e7567da75ddbb24fe979f52284b708da349d67a41042635af36071a5a6b/blobfile-3.1.0.tar.gz", hashes = {sha256 = "d45b6b1fa3b0920732314c23ddbdb4f494ca12f787c2b6eb6bba6faa51382671"}} +wheels = [ + {name = "blobfile-3.1.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/77/a7/51af11120d75af2828f8eede0b13a4caff650d708ac50e62d000aefe1ffb/blobfile-3.1.0-py3-none-any.whl",hashes = {sha256 = "2b4c5e766ebb7dfa20e4990cf6ec3d2106bdc91d632fb9377f170a234c5a5c6a"}}, +] +marker = "\"all\" in extras or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras" + +[packages.tool.pdm] +dependencies = [ + "pycryptodomex>=3.8", + "urllib3<3,>=1.25.3", + "lxml>=4.9", + "filelock>=3.0", +] + [[packages]] name = "click" version = "8.1.8" @@ -45,21 +63,67 @@ dependencies = [ ] [[packages]] -name = "blobfile" -version = "3.1.0" -requires-python = ">=3.8.0" -sdist = {name = "blobfile-3.1.0.tar.gz", url = "https://files.pythonhosted.org/packages/f0/6d/2e7567da75ddbb24fe979f52284b708da349d67a41042635af36071a5a6b/blobfile-3.1.0.tar.gz", hashes = {sha256 = "d45b6b1fa3b0920732314c23ddbdb4f494ca12f787c2b6eb6bba6faa51382671"}} +name = "tiktoken" +version = "0.12.0" +requires-python = ">=3.9" +sdist = {name = "tiktoken-0.12.0.tar.gz", url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hashes = {sha256 = "b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931"}} wheels = [ - {name = "blobfile-3.1.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/77/a7/51af11120d75af2828f8eede0b13a4caff650d708ac50e62d000aefe1ffb/blobfile-3.1.0-py3-none-any.whl",hashes = {sha256 = "2b4c5e766ebb7dfa20e4990cf6ec3d2106bdc91d632fb9377f170a234c5a5c6a"}}, + {name = "tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl",hashes = {sha256 = "a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646"}}, + {name = "tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88"}}, + {name = "tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl",hashes = {sha256 = "285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff"}}, + {name = "tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl",hashes = {sha256 = "d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830"}}, + {name = "tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl",hashes = {sha256 = "604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b"}}, + {name = "tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl",hashes = {sha256 = "8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b"}}, + {name = "tiktoken-0.12.0-cp314-cp314-win_amd64.whl",url = "https://files.pythonhosted.org/packages/80/57/ce64fd16ac390fafde001268c364d559447ba09b509181b2808622420eec/tiktoken-0.12.0-cp314-cp314-win_amd64.whl",hashes = {sha256 = "399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3"}}, + {name = "tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl",hashes = {sha256 = "c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365"}}, + {name = "tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl",hashes = {sha256 = "cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e"}}, + {name = "tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl",hashes = {sha256 = "dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63"}}, + {name = "tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl",hashes = {sha256 = "584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0"}}, + {name = "tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a"}}, + {name = "tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0"}}, + {name = "tiktoken-0.12.0-cp314-cp314t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl",hashes = {sha256 = "f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71"}}, + {name = "tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl",hashes = {sha256 = "775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3"}}, + {name = "tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160"}}, + {name = "tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl",hashes = {sha256 = "01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa"}}, + {name = "tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl",hashes = {sha256 = "4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be"}}, + {name = "tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a"}}, + {name = "tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3"}}, + {name = "tiktoken-0.12.0-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl",hashes = {sha256 = "b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697"}}, + {name = "tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl",hashes = {sha256 = "d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16"}}, + {name = "tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl",hashes = {sha256 = "b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a"}}, + {name = "tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl",hashes = {sha256 = "fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27"}}, + {name = "tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl",hashes = {sha256 = "06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb"}}, + {name = "tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e"}}, + {name = "tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25"}}, + {name = "tiktoken-0.12.0-cp313-cp313t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl",hashes = {sha256 = "dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f"}}, + {name = "tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/a4/85/be65d39d6b647c79800fd9d29241d081d4eeb06271f383bb87200d74cf76/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl",hashes = {sha256 = "b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8"}}, + {name = "tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b"}}, + {name = "tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl",hashes = {sha256 = "65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37"}}, + {name = "tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl",hashes = {sha256 = "edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad"}}, + {name = "tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5"}}, + {name = "tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3"}}, + {name = "tiktoken-0.12.0-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/81/10/b8523105c590c5b8349f2587e2fdfe51a69544bd5a76295fc20f2374f470/tiktoken-0.12.0-cp312-cp312-win_amd64.whl",hashes = {sha256 = "ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd"}}, + {name = "tiktoken-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/de/46/21ea696b21f1d6d1efec8639c204bdf20fde8bafb351e1355c72c5d7de52/tiktoken-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl",hashes = {sha256 = "6e227c7f96925003487c33b1b32265fad2fbcec2b7cf4817afb76d416f40f6bb"}}, + {name = "tiktoken-0.12.0-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/c9/d9/35c5d2d9e22bb2a5f74ba48266fb56c63d76ae6f66e02feb628671c0283e/tiktoken-0.12.0-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "c06cf0fcc24c2cb2adb5e185c7082a82cba29c17575e828518c2f11a01f445aa"}}, + {name = "tiktoken-0.12.0-cp311-cp311-manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/01/84/961106c37b8e49b9fdcf33fe007bb3a8fdcc380c528b20cc7fbba80578b8/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_aarch64.whl",hashes = {sha256 = "f18f249b041851954217e9fd8e5c00b024ab2315ffda5ed77665a05fa91f42dc"}}, + {name = "tiktoken-0.12.0-cp311-cp311-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/6a/d0/3d9275198e067f8b65076a68894bb52fd253875f3644f0a321a720277b8a/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_x86_64.whl",hashes = {sha256 = "47a5bc270b8c3db00bb46ece01ef34ad050e364b51d406b6f9730b64ac28eded"}}, + {name = "tiktoken-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/78/db/a58e09687c1698a7c592e1038e01c206569b86a0377828d51635561f8ebf/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "508fa71810c0efdcd1b898fda574889ee62852989f7c1667414736bcb2b9a4bd"}}, + {name = "tiktoken-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/9e/1b/a9e4d2bf91d515c0f74afc526fd773a812232dd6cda33ebea7f531202325/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "a1af81a6c44f008cba48494089dd98cccb8b313f55e961a52f5b222d1e507967"}}, + {name = "tiktoken-0.12.0-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/9d/15/963819345f1b1fb0809070a79e9dd96938d4ca41297367d471733e79c76c/tiktoken-0.12.0-cp311-cp311-win_amd64.whl",hashes = {sha256 = "3e68e3e593637b53e56f7237be560f7a394451cb8c11079755e80ae64b9e6def"}}, + {name = "tiktoken-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/89/b3/2cb7c17b6c4cf8ca983204255d3f1d95eda7213e247e6947a0ee2c747a2c/tiktoken-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl",hashes = {sha256 = "3de02f5a491cfd179aec916eddb70331814bd6bf764075d39e21d5862e533970"}}, + {name = "tiktoken-0.12.0-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/27/0f/df139f1df5f6167194ee5ab24634582ba9a1b62c6b996472b0277ec80f66/tiktoken-0.12.0-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "b6cfb6d9b7b54d20af21a912bfe63a2727d9cfa8fbda642fd8322c70340aad16"}}, + {name = "tiktoken-0.12.0-cp310-cp310-manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/ef/5d/26a691f28ab220d5edc09b9b787399b130f24327ef824de15e5d85ef21aa/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_aarch64.whl",hashes = {sha256 = "cde24cdb1b8a08368f709124f15b36ab5524aac5fa830cc3fdce9c03d4fb8030"}}, + {name = "tiktoken-0.12.0-cp310-cp310-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/b2/94/443fab3d4e5ebecac895712abd3849b8da93b7b7dec61c7db5c9c7ebe40c/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_x86_64.whl",hashes = {sha256 = "6de0da39f605992649b9cfa6f84071e3f9ef2cec458d08c5feb1b6f0ff62e134"}}, + {name = "tiktoken-0.12.0-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/54/35/388f941251b2521c70dd4c5958e598ea6d2c88e28445d2fb8189eecc1dfc/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "6faa0534e0eefbcafaccb75927a4a380463a2eaa7e26000f0173b920e98b720a"}}, + {name = "tiktoken-0.12.0-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/f8/00/c6681c7f833dd410576183715a530437a9873fa910265817081f65f9105f/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "82991e04fc860afb933efb63957affc7ad54f83e2216fe7d319007dab1ba5892"}}, + {name = "tiktoken-0.12.0-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/5f/d2/82e795a6a9bafa034bf26a58e68fe9a89eeaaa610d51dbeb22106ba04f0a/tiktoken-0.12.0-cp310-cp310-win_amd64.whl",hashes = {sha256 = "6fb2995b487c2e31acf0a9e17647e3b242235a20832642bb7a9d1a181c0c1bb1"}}, ] -marker = "\"openai\" in extras or \"recommended\" in extras" +marker = "\"all\" in extras or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras" [packages.tool.pdm] dependencies = [ - "pycryptodomex>=3.8", - "urllib3<3,>=1.25.3", - "lxml>=4.9", - "filelock>=3.0", + "regex>=2022.1.18", + "requests>=2.26.0", ] [[packages]] @@ -672,70 +736,6 @@ dependencies = [ "colorama>=0.4.5; sys_platform == \"win32\"", ] -[[packages]] -name = "tiktoken" -version = "0.12.0" -requires-python = ">=3.9" -sdist = {name = "tiktoken-0.12.0.tar.gz", url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hashes = {sha256 = "b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931"}} -wheels = [ - {name = "tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl",hashes = {sha256 = "a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646"}}, - {name = "tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88"}}, - {name = "tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl",hashes = {sha256 = "285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff"}}, - {name = "tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl",hashes = {sha256 = "d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830"}}, - {name = "tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl",hashes = {sha256 = "604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b"}}, - {name = "tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl",hashes = {sha256 = "8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b"}}, - {name = "tiktoken-0.12.0-cp314-cp314-win_amd64.whl",url = "https://files.pythonhosted.org/packages/80/57/ce64fd16ac390fafde001268c364d559447ba09b509181b2808622420eec/tiktoken-0.12.0-cp314-cp314-win_amd64.whl",hashes = {sha256 = "399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3"}}, - {name = "tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl",hashes = {sha256 = "c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365"}}, - {name = "tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl",hashes = {sha256 = "cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e"}}, - {name = "tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl",hashes = {sha256 = "dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63"}}, - {name = "tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl",hashes = {sha256 = "584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0"}}, - {name = "tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a"}}, - {name = "tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0"}}, - {name = "tiktoken-0.12.0-cp314-cp314t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl",hashes = {sha256 = "f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71"}}, - {name = "tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl",hashes = {sha256 = "775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3"}}, - {name = "tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160"}}, - {name = "tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl",hashes = {sha256 = "01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa"}}, - {name = "tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl",hashes = {sha256 = "4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be"}}, - {name = "tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a"}}, - {name = "tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3"}}, - {name = "tiktoken-0.12.0-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl",hashes = {sha256 = "b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697"}}, - {name = "tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl",hashes = {sha256 = "d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16"}}, - {name = "tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl",hashes = {sha256 = "b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a"}}, - {name = "tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl",hashes = {sha256 = "fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27"}}, - {name = "tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl",hashes = {sha256 = "06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb"}}, - {name = "tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e"}}, - {name = "tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25"}}, - {name = "tiktoken-0.12.0-cp313-cp313t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl",hashes = {sha256 = "dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f"}}, - {name = "tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/a4/85/be65d39d6b647c79800fd9d29241d081d4eeb06271f383bb87200d74cf76/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl",hashes = {sha256 = "b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8"}}, - {name = "tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b"}}, - {name = "tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl",hashes = {sha256 = "65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37"}}, - {name = "tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl",hashes = {sha256 = "edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad"}}, - {name = "tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5"}}, - {name = "tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3"}}, - {name = "tiktoken-0.12.0-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/81/10/b8523105c590c5b8349f2587e2fdfe51a69544bd5a76295fc20f2374f470/tiktoken-0.12.0-cp312-cp312-win_amd64.whl",hashes = {sha256 = "ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd"}}, - {name = "tiktoken-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/de/46/21ea696b21f1d6d1efec8639c204bdf20fde8bafb351e1355c72c5d7de52/tiktoken-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl",hashes = {sha256 = "6e227c7f96925003487c33b1b32265fad2fbcec2b7cf4817afb76d416f40f6bb"}}, - {name = "tiktoken-0.12.0-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/c9/d9/35c5d2d9e22bb2a5f74ba48266fb56c63d76ae6f66e02feb628671c0283e/tiktoken-0.12.0-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "c06cf0fcc24c2cb2adb5e185c7082a82cba29c17575e828518c2f11a01f445aa"}}, - {name = "tiktoken-0.12.0-cp311-cp311-manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/01/84/961106c37b8e49b9fdcf33fe007bb3a8fdcc380c528b20cc7fbba80578b8/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_aarch64.whl",hashes = {sha256 = "f18f249b041851954217e9fd8e5c00b024ab2315ffda5ed77665a05fa91f42dc"}}, - {name = "tiktoken-0.12.0-cp311-cp311-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/6a/d0/3d9275198e067f8b65076a68894bb52fd253875f3644f0a321a720277b8a/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_x86_64.whl",hashes = {sha256 = "47a5bc270b8c3db00bb46ece01ef34ad050e364b51d406b6f9730b64ac28eded"}}, - {name = "tiktoken-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/78/db/a58e09687c1698a7c592e1038e01c206569b86a0377828d51635561f8ebf/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "508fa71810c0efdcd1b898fda574889ee62852989f7c1667414736bcb2b9a4bd"}}, - {name = "tiktoken-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/9e/1b/a9e4d2bf91d515c0f74afc526fd773a812232dd6cda33ebea7f531202325/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "a1af81a6c44f008cba48494089dd98cccb8b313f55e961a52f5b222d1e507967"}}, - {name = "tiktoken-0.12.0-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/9d/15/963819345f1b1fb0809070a79e9dd96938d4ca41297367d471733e79c76c/tiktoken-0.12.0-cp311-cp311-win_amd64.whl",hashes = {sha256 = "3e68e3e593637b53e56f7237be560f7a394451cb8c11079755e80ae64b9e6def"}}, - {name = "tiktoken-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/89/b3/2cb7c17b6c4cf8ca983204255d3f1d95eda7213e247e6947a0ee2c747a2c/tiktoken-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl",hashes = {sha256 = "3de02f5a491cfd179aec916eddb70331814bd6bf764075d39e21d5862e533970"}}, - {name = "tiktoken-0.12.0-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/27/0f/df139f1df5f6167194ee5ab24634582ba9a1b62c6b996472b0277ec80f66/tiktoken-0.12.0-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "b6cfb6d9b7b54d20af21a912bfe63a2727d9cfa8fbda642fd8322c70340aad16"}}, - {name = "tiktoken-0.12.0-cp310-cp310-manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/ef/5d/26a691f28ab220d5edc09b9b787399b130f24327ef824de15e5d85ef21aa/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_aarch64.whl",hashes = {sha256 = "cde24cdb1b8a08368f709124f15b36ab5524aac5fa830cc3fdce9c03d4fb8030"}}, - {name = "tiktoken-0.12.0-cp310-cp310-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/b2/94/443fab3d4e5ebecac895712abd3849b8da93b7b7dec61c7db5c9c7ebe40c/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_x86_64.whl",hashes = {sha256 = "6de0da39f605992649b9cfa6f84071e3f9ef2cec458d08c5feb1b6f0ff62e134"}}, - {name = "tiktoken-0.12.0-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/54/35/388f941251b2521c70dd4c5958e598ea6d2c88e28445d2fb8189eecc1dfc/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "6faa0534e0eefbcafaccb75927a4a380463a2eaa7e26000f0173b920e98b720a"}}, - {name = "tiktoken-0.12.0-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/f8/00/c6681c7f833dd410576183715a530437a9873fa910265817081f65f9105f/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "82991e04fc860afb933efb63957affc7ad54f83e2216fe7d319007dab1ba5892"}}, - {name = "tiktoken-0.12.0-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/5f/d2/82e795a6a9bafa034bf26a58e68fe9a89eeaaa610d51dbeb22106ba04f0a/tiktoken-0.12.0-cp310-cp310-win_amd64.whl",hashes = {sha256 = "6fb2995b487c2e31acf0a9e17647e3b242235a20832642bb7a9d1a181c0c1bb1"}}, -] -marker = "\"openai\" in extras or \"recommended\" in extras" - -[packages.tool.pdm] -dependencies = [ - "regex>=2022.1.18", - "requests>=2.26.0", -] - [[packages]] name = "tox" version = "4.16.0" @@ -819,7 +819,7 @@ wheels = [ {name = "uvloop-0.21.0-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/bf/fe/5e94a977d058a54a19df95f12f7161ab6e323ad49f4dabc28822eb2df7ea/uvloop-0.21.0-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f"}}, {name = "uvloop-0.21.0-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/26/dd/c7179618e46092a77e036650c1f056041a028a35c4d76945089fcfc38af8/uvloop-0.21.0-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c"}}, ] -marker = "\"default\" in dependency_groups or \"perf\" in extras or \"recommended\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"perf\" in extras or \"recommended\" in extras" [packages.tool.pdm] dependencies = [] @@ -956,7 +956,7 @@ wheels = [ {name = "msgpack-1.1.2-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/4b/4f/05fcebd3b4977cb3d840f7ef6b77c51f8582086de5e642f3fefee35c86fc/msgpack-1.1.2-cp310-cp310-win32.whl",hashes = {sha256 = "e64c8d2f5e5d5fda7b842f55dec6133260ea8f53c4257d64494c534f306bf7a9"}}, {name = "msgpack-1.1.2-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/d0/3e/b4547e3a34210956382eed1c85935fff7e0f9b98be3106b3745d7dec9c5e/msgpack-1.1.2-cp310-cp310-win_amd64.whl",hashes = {sha256 = "db6192777d943bdaaafb6ba66d44bf65aa0e9c5616fa1d2da9bb08828c6b39aa"}}, ] -marker = "\"default\" in dependency_groups or \"perf\" in extras or \"recommended\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"perf\" in extras or \"recommended\" in extras" [packages.tool.pdm] dependencies = [] @@ -982,7 +982,7 @@ wheels = [ {name = "msgspec-0.19.0-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/9d/e6/8ad51bdc806aac1dc501e8fe43f759f9ed7284043d722b53323ea421c360/msgspec-0.19.0-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "5f0f65f29b45e2816d8bded36e6b837a4bf5fb60ec4bc3c625fa2c6da4124537"}}, {name = "msgspec-0.19.0-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/b1/ef/27dd35a7049c9a4f4211c6cd6a8c9db0a50647546f003a5867827ec45391/msgspec-0.19.0-cp312-cp312-win_amd64.whl",hashes = {sha256 = "067f0de1c33cfa0b6a8206562efdf6be5985b988b53dd244a8e06f993f27c8c0"}}, ] -marker = "\"perf\" in extras or \"recommended\" in extras" +marker = "\"all\" in extras or \"dev\" in extras or \"perf\" in extras or \"recommended\" in extras" [packages.tool.pdm] dependencies = [] @@ -1035,7 +1035,7 @@ wheels = [ {name = "orjson-3.11.3-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/35/b8/9e3127d65de7fff243f7f3e53f59a531bf6bb295ebe5db024c2503cc0726/orjson-3.11.3-cp312-cp312-win_amd64.whl",hashes = {sha256 = "79b44319268af2eaa3e315b92298de9a0067ade6e6003ddaef72f8e0bedb94f1"}}, {name = "orjson-3.11.3-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/51/92/a946e737d4d8a7fd84a606aba96220043dcc7d6988b9e7551f7f6d5ba5ad/orjson-3.11.3-cp312-cp312-win_arm64.whl",hashes = {sha256 = "0e92a4e83341ef79d835ca21b8bd13e27c859e4e9e4d7b63defc6e58462a3710"}}, ] -marker = "\"perf\" in extras or \"recommended\" in extras" +marker = "\"all\" in extras or \"dev\" in extras or \"perf\" in extras or \"recommended\" in extras" [packages.tool.pdm] dependencies = [] @@ -1532,7 +1532,7 @@ sdist = {name = "requests-2.32.5.tar.gz", url = "https://files.pythonhosted.org/ wheels = [ {name = "requests-2.32.5-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl",hashes = {sha256 = "2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras" [packages.tool.pdm] dependencies = [ @@ -1550,7 +1550,7 @@ sdist = {name = "urllib3-2.5.0.tar.gz", url = "https://files.pythonhosted.org/pa wheels = [ {name = "urllib3-2.5.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl",hashes = {sha256 = "e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras" [packages.tool.pdm] dependencies = [] @@ -1646,7 +1646,7 @@ wheels = [ {name = "charset_normalizer-3.4.3-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/2a/91/26c3036e62dfe8de8061182d33be5025e2424002125c9500faff74a6735e/charset_normalizer-3.4.3-cp310-cp310-win32.whl",hashes = {sha256 = "d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f"}}, {name = "charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/e2/c6/f05db471f81af1fa01839d44ae2a8bfeec8d2a8b4590f16c4e7393afd323/charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl",hashes = {sha256 = "c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras" [packages.tool.pdm] dependencies = [] @@ -1685,7 +1685,7 @@ sdist = {name = "filelock-3.20.0.tar.gz", url = "https://files.pythonhosted.org/ wheels = [ {name = "filelock-3.20.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl",hashes = {sha256 = "339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras" [packages.tool.pdm] dependencies = [] @@ -2036,7 +2036,7 @@ sdist = {name = "idna-3.10.tar.gz", url = "https://files.pythonhosted.org/packag wheels = [ {name = "idna-3.10-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl",hashes = {sha256 = "946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras" [packages.tool.pdm] dependencies = [] @@ -2162,7 +2162,7 @@ wheels = [ {name = "regex-2025.9.18-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/59/39/aeb11a4ae68faaec2498512cadae09f2d8a91f1f65730fe62b9bffeea150/regex-2025.9.18-cp310-cp310-win_amd64.whl",hashes = {sha256 = "47acd811589301298c49db2c56bde4f9308d6396da92daf99cba781fa74aa450"}}, {name = "regex-2025.9.18-cp310-cp310-win_arm64.whl",url = "https://files.pythonhosted.org/packages/29/04/37f2d3fc334a1031fc2767c9d89cec13c2e72207c7e7f6feae8a47f4e149/regex-2025.9.18-cp310-cp310-win_arm64.whl",hashes = {sha256 = "16bd2944e77522275e5ee36f867e19995bcaa533dcb516753a26726ac7285442"}}, ] -marker = "\"default\" in dependency_groups or \"openai\" in extras or \"recommended\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras" [packages.tool.pdm] dependencies = [] @@ -2747,7 +2747,7 @@ sdist = {name = "certifi-2025.10.5.tar.gz", url = "https://files.pythonhosted.or wheels = [ {name = "certifi-2025.10.5-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl",hashes = {sha256 = "0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras" [packages.tool.pdm] dependencies = [] @@ -3111,7 +3111,7 @@ wheels = [ {name = "lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/77/a2/585a28fe3e67daa1cf2f06f34490d556d121c25d500b10082a7db96e3bcd/lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "2613e67de13d619fd283d58bda40bff0ee07739f624ffee8b13b631abf33083d"}}, {name = "lxml-6.0.2-pp310-pypy310_pp73-win_amd64.whl",url = "https://files.pythonhosted.org/packages/7b/d9/a57dd8bcebd7c69386c20263830d4fa72d27e6b72a229ef7a48e88952d9a/lxml-6.0.2-pp310-pypy310_pp73-win_amd64.whl",hashes = {sha256 = "24a8e756c982c001ca8d59e87c80c4d9dcd4d9b44a4cbeb8d9be4482c514d41d"}}, ] -marker = "\"openai\" in extras or \"recommended\" in extras" +marker = "\"all\" in extras or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras" [packages.tool.pdm] dependencies = [] @@ -3373,7 +3373,7 @@ wheels = [ {name = "pycryptodomex-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/6e/c3/1de4f7631fea8a992a44ba632aa40e0008764c0fb9bf2854b0acf78c2cf2/pycryptodomex-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "fdfac7cda115bca3a5abb2f9e43bc2fb66c2b65ab074913643803ca7083a79ea"}}, {name = "pycryptodomex-3.23.0-pp310-pypy310_pp73-win_amd64.whl",url = "https://files.pythonhosted.org/packages/f2/5f/af7da8e6f1e42b52f44a24d08b8e4c726207434e2593732d39e7af5e7256/pycryptodomex-3.23.0-pp310-pypy310_pp73-win_amd64.whl",hashes = {sha256 = "14c37aaece158d0ace436f76a7bb19093db3b4deade9797abfc39ec6cd6cc2fe"}}, ] -marker = "\"openai\" in extras or \"recommended\" in extras" +marker = "\"all\" in extras or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras" [packages.tool.pdm] dependencies = [] @@ -4393,7 +4393,7 @@ marker = "python_full_version >= \"3.10.0\" and python_full_version < \"3.10.2\" dependencies = [] [tool.pdm] -hashes = {sha256 = "02a6fea936a5d6d81a623959518d50540f47a5756f7eb12720848d60a333e3a6"} +hashes = {sha256 = "42547540cdbfb38c3bf9f67e7159db84b94a9abd466da78f7c1c481c41bea687"} strategy = ["inherit_metadata", "static_urls"] [[tool.pdm.targets]] diff --git a/pyproject.toml b/pyproject.toml index 9e8cb813..f5420b6a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,11 +76,18 @@ dependencies = [ ] [project.optional-dependencies] +# Meta Extras +all = ["guidellm[perf,openai,multimodal]"] +recommended = ["guidellm[perf,openai]"] +# Feature Extras perf = ["orjson", "msgpack", "msgspec", "uvloop"] openai = ["tiktoken>=0.11.0", "blobfile>=3.1.0"] multimodal = ["datasets[audio,vision]>=4.1.0", "torchcodec"] -recommended = ["guidellm[perf,openai]"] +# Dev Tooling dev = [ + # Install all optional dependencies + "guidellm[all]", + # build "build>=1.0.0", "setuptools>=61.0", From 5a768f8756ff286b94cdf75530155d4fc66c7105 Mon Sep 17 00:00:00 2001 From: Samuel Monson Date: Tue, 14 Oct 2025 17:14:04 -0400 Subject: [PATCH 39/57] Fix lock Signed-off-by: Samuel Monson --- pylock.toml | 1321 ++++++++++++++++++++++++++++----------------------- 1 file changed, 716 insertions(+), 605 deletions(-) diff --git a/pylock.toml b/pylock.toml index e81719ce..8bd5e1d0 100644 --- a/pylock.toml +++ b/pylock.toml @@ -6,7 +6,7 @@ environments = [ "python_version ~= \"3.12\"", "python_full_version >= \"3.10.0\" and python_version < \"3.12\"", ] -extras = ["all", "dev", "openai", "perf", "recommended"] +extras = ["all", "dev", "multimodal", "openai", "perf", "recommended"] dependency-groups = ["default"] default-groups = ["default"] created-by = "pdm" @@ -29,39 +29,6 @@ dependencies = [ "filelock>=3.0", ] -[[packages]] -name = "click" -version = "8.1.8" -requires-python = ">=3.7" -sdist = {name = "click-8.1.8.tar.gz", url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hashes = {sha256 = "ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}} -wheels = [ - {name = "click-8.1.8-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl",hashes = {sha256 = "63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}}, -] -marker = "\"default\" in dependency_groups" - -[packages.tool.pdm] -dependencies = [ - "colorama; platform_system == \"Windows\"", - "importlib-metadata; python_version < \"3.8\"", -] - -[[packages]] -name = "setuptools-git-versioning" -version = "2.1.0" -requires-python = ">=3.7" -sdist = {name = "setuptools_git_versioning-2.1.0.tar.gz", url = "https://files.pythonhosted.org/packages/f0/72/507b0b459b1fdbf5705aecbc5330c32d62dd41560718d2720bb6d94607f5/setuptools_git_versioning-2.1.0.tar.gz", hashes = {sha256 = "6aef5b8bb1cfb953b6b343d27cbfc561d96cf2a2ee23c2e0dd3591042a059921"}} -wheels = [ - {name = "setuptools_git_versioning-2.1.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/c0/ba/daf16c2d1965bf6237fb696639e3e93645ac6801f7dcaf9ec694a74e9326/setuptools_git_versioning-2.1.0-py3-none-any.whl",hashes = {sha256 = "09a15cbb9a00884e91a3591a4c9ec1ff93c24b1b4a40de39a44815196beb7ebf"}}, -] -marker = "\"dev\" in extras" - -[packages.tool.pdm] -dependencies = [ - "packaging", - "setuptools", - "tomli>=2.0.1; python_version < \"3.11\"", -] - [[packages]] name = "tiktoken" version = "0.12.0" @@ -126,6 +93,202 @@ dependencies = [ "requests>=2.26.0", ] +[[packages]] +name = "click" +version = "8.1.8" +requires-python = ">=3.7" +sdist = {name = "click-8.1.8.tar.gz", url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hashes = {sha256 = "ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}} +wheels = [ + {name = "click-8.1.8-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl",hashes = {sha256 = "63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}}, +] +marker = "\"default\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [ + "colorama; platform_system == \"Windows\"", + "importlib-metadata; python_version < \"3.8\"", +] + +[[packages]] +name = "datasets" +version = "4.2.0" +requires-python = ">=3.9.0" +sdist = {name = "datasets-4.2.0.tar.gz", url = "https://files.pythonhosted.org/packages/70/48/0186fbc4b86a4f9ecaf04eb01e877e78b53bfa0b03be9c84b2298431ba33/datasets-4.2.0.tar.gz", hashes = {sha256 = "8333a7db9f3bb8044c1b819a35d4e3e2809596c837793b0921382efffdc36e78"}} +wheels = [ + {name = "datasets-4.2.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/91/9e/0bbbd09b116fd8ee2d3617e28e6598551d2f0f24d3a2ce99cc87ec85aeb0/datasets-4.2.0-py3-none-any.whl",hashes = {sha256 = "fdc43aaf4a73b31f64f80f72f195ab413a1141ed15555d675b2fd17926f8b026"}}, +] +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" + +[packages.tool.pdm] +dependencies = [ + "filelock", + "numpy>=1.17", + "pyarrow>=21.0.0", + "dill<0.4.1,>=0.3.0", + "pandas", + "requests>=2.32.2", + "httpx<1.0.0", + "tqdm>=4.66.3", + "xxhash", + "multiprocess<0.70.17", + "fsspec[http]<=2025.9.0,>=2023.1.0", + "huggingface-hub<2.0,>=0.25.0", + "packaging", + "pyyaml>=5.1", +] + +[[packages]] +name = "numpy" +version = "2.3.3" +requires-python = ">=3.11" +sdist = {name = "numpy-2.3.3.tar.gz", url = "https://files.pythonhosted.org/packages/d0/19/95b3d357407220ed24c139018d2518fab0a61a948e68286a25f1a4d049ff/numpy-2.3.3.tar.gz", hashes = {sha256 = "ddc7c39727ba62b80dfdbedf400d1c10ddfa8eefbd7ec8dcb118be8b56d31029"}} +wheels = [ + {name = "numpy-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/6b/01/342ad585ad82419b99bcf7cebe99e61da6bedb89e213c5fd71acc467faee/numpy-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl",hashes = {sha256 = "cd052f1fa6a78dee696b58a914b7229ecfa41f0a6d96dc663c1220a55e137593"}}, + {name = "numpy-2.3.3-cp314-cp314-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/ef/d8/204e0d73fc1b7a9ee80ab1fe1983dd33a4d64a4e30a05364b0208e9a241a/numpy-2.3.3-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "414a97499480067d305fcac9716c29cf4d0d76db6ebf0bf3cbce666677f12652"}}, + {name = "numpy-2.3.3-cp314-cp314-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/22/af/f11c916d08f3a18fb8ba81ab72b5b74a6e42ead4c2846d270eb19845bf74/numpy-2.3.3-cp314-cp314-macosx_14_0_arm64.whl",hashes = {sha256 = "50a5fe69f135f88a2be9b6ca0481a68a136f6febe1916e4920e12f1a34e708a7"}}, + {name = "numpy-2.3.3-cp314-cp314-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/fb/11/0ed919c8381ac9d2ffacd63fd1f0c34d27e99cab650f0eb6f110e6ae4858/numpy-2.3.3-cp314-cp314-macosx_14_0_x86_64.whl",hashes = {sha256 = "b912f2ed2b67a129e6a601e9d93d4fa37bef67e54cac442a2f588a54afe5c67a"}}, + {name = "numpy-2.3.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/ee/83/deb5f77cb0f7ba6cb52b91ed388b47f8f3c2e9930d4665c600408d9b90b9/numpy-2.3.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "9e318ee0596d76d4cb3d78535dc005fa60e5ea348cd131a51e99d0bdbe0b54fe"}}, + {name = "numpy-2.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/77/cc/70e59dcb84f2b005d4f306310ff0a892518cc0c8000a33d0e6faf7ca8d80/numpy-2.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "ce020080e4a52426202bdb6f7691c65bb55e49f261f31a8f506c9f6bc7450421"}}, + {name = "numpy-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/b6/5a/b2ab6c18b4257e099587d5b7f903317bd7115333ad8d4ec4874278eafa61/numpy-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl",hashes = {sha256 = "e6687dc183aa55dae4a705b35f9c0f8cb178bcaa2f029b241ac5356221d5c021"}}, + {name = "numpy-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/b8/f1/8b3fdc44324a259298520dd82147ff648979bed085feeacc1250ef1656c0/numpy-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl",hashes = {sha256 = "d8f3b1080782469fdc1718c4ed1d22549b5fb12af0d57d35e992158a772a37cf"}}, + {name = "numpy-2.3.3-cp314-cp314-win32.whl",url = "https://files.pythonhosted.org/packages/f0/a1/b87a284fb15a42e9274e7fcea0dad259d12ddbf07c1595b26883151ca3b4/numpy-2.3.3-cp314-cp314-win32.whl",hashes = {sha256 = "cb248499b0bc3be66ebd6578b83e5acacf1d6cb2a77f2248ce0e40fbec5a76d0"}}, + {name = "numpy-2.3.3-cp314-cp314-win_amd64.whl",url = "https://files.pythonhosted.org/packages/70/5f/1816f4d08f3b8f66576d8433a66f8fa35a5acfb3bbd0bf6c31183b003f3d/numpy-2.3.3-cp314-cp314-win_amd64.whl",hashes = {sha256 = "691808c2b26b0f002a032c73255d0bd89751425f379f7bcd22d140db593a96e8"}}, + {name = "numpy-2.3.3-cp314-cp314-win_arm64.whl",url = "https://files.pythonhosted.org/packages/8c/de/072420342e46a8ea41c324a555fa90fcc11637583fb8df722936aed1736d/numpy-2.3.3-cp314-cp314-win_arm64.whl",hashes = {sha256 = "9ad12e976ca7b10f1774b03615a2a4bab8addce37ecc77394d8e986927dc0dfe"}}, + {name = "numpy-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/d5/df/ee2f1c0a9de7347f14da5dd3cd3c3b034d1b8607ccb6883d7dd5c035d631/numpy-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl",hashes = {sha256 = "9cc48e09feb11e1db00b320e9d30a4151f7369afb96bd0e48d942d09da3a0d00"}}, + {name = "numpy-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/d6/92/9453bdc5a4e9e69cf4358463f25e8260e2ffc126d52e10038b9077815989/numpy-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl",hashes = {sha256 = "901bf6123879b7f251d3631967fd574690734236075082078e0571977c6a8e6a"}}, + {name = "numpy-2.3.3-cp314-cp314t-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/13/77/1447b9eb500f028bb44253105bd67534af60499588a5149a94f18f2ca917/numpy-2.3.3-cp314-cp314t-macosx_14_0_arm64.whl",hashes = {sha256 = "7f025652034199c301049296b59fa7d52c7e625017cae4c75d8662e377bf487d"}}, + {name = "numpy-2.3.3-cp314-cp314t-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/3d/f9/d72221b6ca205f9736cb4b2ce3b002f6e45cd67cd6a6d1c8af11a2f0b649/numpy-2.3.3-cp314-cp314t-macosx_14_0_x86_64.whl",hashes = {sha256 = "533ca5f6d325c80b6007d4d7fb1984c303553534191024ec6a524a4c92a5935a"}}, + {name = "numpy-2.3.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/3c/5f/d12834711962ad9c46af72f79bb31e73e416ee49d17f4c797f72c96b6ca5/numpy-2.3.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "0edd58682a399824633b66885d699d7de982800053acf20be1eaa46d92009c54"}}, + {name = "numpy-2.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/a1/0d/fdbec6629d97fd1bebed56cd742884e4eead593611bbe1abc3eb40d304b2/numpy-2.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "367ad5d8fbec5d9296d18478804a530f1191e24ab4d75ab408346ae88045d25e"}}, + {name = "numpy-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/9b/09/0a35196dc5575adde1eb97ddfbc3e1687a814f905377621d18ca9bc2b7dd/numpy-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "8f6ac61a217437946a1fa48d24c47c91a0c4f725237871117dea264982128097"}}, + {name = "numpy-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/7a/ca/c9de3ea397d576f1b6753eaa906d4cdef1bf97589a6d9825a349b4729cc2/numpy-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "179a42101b845a816d464b6fe9a845dfaf308fdfc7925387195570789bb2c970"}}, + {name = "numpy-2.3.3-cp314-cp314t-win32.whl",url = "https://files.pythonhosted.org/packages/fd/c2/e5ed830e08cd0196351db55db82f65bc0ab05da6ef2b72a836dcf1936d2f/numpy-2.3.3-cp314-cp314t-win32.whl",hashes = {sha256 = "1250c5d3d2562ec4174bce2e3a1523041595f9b651065e4a4473f5f48a6bc8a5"}}, + {name = "numpy-2.3.3-cp314-cp314t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/47/c7/b0f6b5b67f6788a0725f744496badbb604d226bf233ba716683ebb47b570/numpy-2.3.3-cp314-cp314t-win_amd64.whl",hashes = {sha256 = "b37a0b2e5935409daebe82c1e42274d30d9dd355852529eab91dab8dcca7419f"}}, + {name = "numpy-2.3.3-cp314-cp314t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/06/b9/33bba5ff6fb679aa0b1f8a07e853f002a6b04b9394db3069a1270a7784ca/numpy-2.3.3-cp314-cp314t-win_arm64.whl",hashes = {sha256 = "78c9f6560dc7e6b3990e32df7ea1a50bbd0e2a111e05209963f5ddcab7073b0b"}}, + {name = "numpy-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/7d/b9/984c2b1ee61a8b803bf63582b4ac4242cf76e2dbd663efeafcb620cc0ccb/numpy-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl",hashes = {sha256 = "f5415fb78995644253370985342cd03572ef8620b934da27d77377a2285955bf"}}, + {name = "numpy-2.3.3-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/a6/e4/07970e3bed0b1384d22af1e9912527ecbeb47d3b26e9b6a3bced068b3bea/numpy-2.3.3-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "d00de139a3324e26ed5b95870ce63be7ec7352171bc69a4cf1f157a48e3eb6b7"}}, + {name = "numpy-2.3.3-cp313-cp313-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/35/c7/477a83887f9de61f1203bad89cf208b7c19cc9fef0cebef65d5a1a0619f2/numpy-2.3.3-cp313-cp313-macosx_14_0_arm64.whl",hashes = {sha256 = "9dc13c6a5829610cc07422bc74d3ac083bd8323f14e2827d992f9e52e22cd6a6"}}, + {name = "numpy-2.3.3-cp313-cp313-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/52/47/93b953bd5866a6f6986344d045a207d3f1cfbad99db29f534ea9cee5108c/numpy-2.3.3-cp313-cp313-macosx_14_0_x86_64.whl",hashes = {sha256 = "d79715d95f1894771eb4e60fb23f065663b2298f7d22945d66877aadf33d00c7"}}, + {name = "numpy-2.3.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/23/83/377f84aaeb800b64c0ef4de58b08769e782edcefa4fea712910b6f0afd3c/numpy-2.3.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "952cfd0748514ea7c3afc729a0fc639e61655ce4c55ab9acfab14bda4f402b4c"}}, + {name = "numpy-2.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/9a/a5/bf3db6e66c4b160d6ea10b534c381a1955dfab34cb1017ea93aa33c70ed3/numpy-2.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "5b83648633d46f77039c29078751f80da65aa64d5622a3cd62aaef9d835b6c93"}}, + {name = "numpy-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/a2/59/1287924242eb4fa3f9b3a2c30400f2e17eb2707020d1c5e3086fe7330717/numpy-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "b001bae8cea1c7dfdb2ae2b017ed0a6f2102d7a70059df1e338e307a4c78a8ae"}}, + {name = "numpy-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/e6/93/b3d47ed882027c35e94ac2320c37e452a549f582a5e801f2d34b56973c97/numpy-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "8e9aced64054739037d42fb84c54dd38b81ee238816c948c8f3ed134665dcd86"}}, + {name = "numpy-2.3.3-cp313-cp313-win32.whl",url = "https://files.pythonhosted.org/packages/20/d9/487a2bccbf7cc9d4bfc5f0f197761a5ef27ba870f1e3bbb9afc4bbe3fcc2/numpy-2.3.3-cp313-cp313-win32.whl",hashes = {sha256 = "9591e1221db3f37751e6442850429b3aabf7026d3b05542d102944ca7f00c8a8"}}, + {name = "numpy-2.3.3-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/1b/b5/263ebbbbcede85028f30047eab3d58028d7ebe389d6493fc95ae66c636ab/numpy-2.3.3-cp313-cp313-win_amd64.whl",hashes = {sha256 = "f0dadeb302887f07431910f67a14d57209ed91130be0adea2f9793f1a4f817cf"}}, + {name = "numpy-2.3.3-cp313-cp313-win_arm64.whl",url = "https://files.pythonhosted.org/packages/fa/75/67b8ca554bbeaaeb3fac2e8bce46967a5a06544c9108ec0cf5cece559b6c/numpy-2.3.3-cp313-cp313-win_arm64.whl",hashes = {sha256 = "3c7cf302ac6e0b76a64c4aecf1a09e51abd9b01fc7feee80f6c43e3ab1b1dbc5"}}, + {name = "numpy-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/11/d0/0d1ddec56b162042ddfafeeb293bac672de9b0cfd688383590090963720a/numpy-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl",hashes = {sha256 = "eda59e44957d272846bb407aad19f89dc6f58fecf3504bd144f4c5cf81a7eacc"}}, + {name = "numpy-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/36/9e/1996ca6b6d00415b6acbdd3c42f7f03ea256e2c3f158f80bd7436a8a19f3/numpy-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl",hashes = {sha256 = "823d04112bc85ef5c4fda73ba24e6096c8f869931405a80aa8b0e604510a26bc"}}, + {name = "numpy-2.3.3-cp313-cp313t-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/05/24/43da09aa764c68694b76e84b3d3f0c44cb7c18cdc1ba80e48b0ac1d2cd39/numpy-2.3.3-cp313-cp313t-macosx_14_0_arm64.whl",hashes = {sha256 = "40051003e03db4041aa325da2a0971ba41cf65714e65d296397cc0e32de6018b"}}, + {name = "numpy-2.3.3-cp313-cp313t-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/bc/14/50ffb0f22f7218ef8af28dd089f79f68289a7a05a208db9a2c5dcbe123c1/numpy-2.3.3-cp313-cp313t-macosx_14_0_x86_64.whl",hashes = {sha256 = "6ee9086235dd6ab7ae75aba5662f582a81ced49f0f1c6de4260a78d8f2d91a19"}}, + {name = "numpy-2.3.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/55/52/af46ac0795e09657d45a7f4db961917314377edecf66db0e39fa7ab5c3d3/numpy-2.3.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "94fcaa68757c3e2e668ddadeaa86ab05499a70725811e582b6a9858dd472fb30"}}, + {name = "numpy-2.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/a7/b1/dc226b4c90eb9f07a3fff95c2f0db3268e2e54e5cce97c4ac91518aee71b/numpy-2.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "da1a74b90e7483d6ce5244053399a614b1d6b7bc30a60d2f570e5071f8959d3e"}}, + {name = "numpy-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/9d/9d/9d8d358f2eb5eced14dba99f110d83b5cd9a4460895230f3b396ad19a323/numpy-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "2990adf06d1ecee3b3dcbb4977dfab6e9f09807598d647f04d385d29e7a3c3d3"}}, + {name = "numpy-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/b6/27/b3922660c45513f9377b3fb42240bec63f203c71416093476ec9aa0719dc/numpy-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "ed635ff692483b8e3f0fcaa8e7eb8a75ee71aa6d975388224f70821421800cea"}}, + {name = "numpy-2.3.3-cp313-cp313t-win32.whl",url = "https://files.pythonhosted.org/packages/5b/8e/3ab61a730bdbbc201bb245a71102aa609f0008b9ed15255500a99cd7f780/numpy-2.3.3-cp313-cp313t-win32.whl",hashes = {sha256 = "a333b4ed33d8dc2b373cc955ca57babc00cd6f9009991d9edc5ddbc1bac36bcd"}}, + {name = "numpy-2.3.3-cp313-cp313t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/1c/3a/e22b766b11f6030dc2decdeff5c2fb1610768055603f9f3be88b6d192fb2/numpy-2.3.3-cp313-cp313t-win_amd64.whl",hashes = {sha256 = "4384a169c4d8f97195980815d6fcad04933a7e1ab3b530921c3fef7a1c63426d"}}, + {name = "numpy-2.3.3-cp313-cp313t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/7b/42/c2e2bc48c5e9b2a83423f99733950fbefd86f165b468a3d85d52b30bf782/numpy-2.3.3-cp313-cp313t-win_arm64.whl",hashes = {sha256 = "75370986cc0bc66f4ce5110ad35aae6d182cc4ce6433c40ad151f53690130bf1"}}, + {name = "numpy-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/51/5d/bb7fc075b762c96329147799e1bcc9176ab07ca6375ea976c475482ad5b3/numpy-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl",hashes = {sha256 = "cfdd09f9c84a1a934cde1eec2267f0a43a7cd44b2cca4ff95b7c0d14d144b0bf"}}, + {name = "numpy-2.3.3-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/6b/0e/c6211bb92af26517acd52125a237a92afe9c3124c6a68d3b9f81b62a0568/numpy-2.3.3-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "cb32e3cf0f762aee47ad1ddc6672988f7f27045b0783c887190545baba73aa25"}}, + {name = "numpy-2.3.3-cp312-cp312-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/22/f2/07bb754eb2ede9073f4054f7c0286b0d9d2e23982e090a80d478b26d35ca/numpy-2.3.3-cp312-cp312-macosx_14_0_arm64.whl",hashes = {sha256 = "396b254daeb0a57b1fe0ecb5e3cff6fa79a380fa97c8f7781a6d08cd429418fe"}}, + {name = "numpy-2.3.3-cp312-cp312-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/81/0a/afa51697e9fb74642f231ea36aca80fa17c8fb89f7a82abd5174023c3960/numpy-2.3.3-cp312-cp312-macosx_14_0_x86_64.whl",hashes = {sha256 = "067e3d7159a5d8f8a0b46ee11148fc35ca9b21f61e3c49fbd0a027450e65a33b"}}, + {name = "numpy-2.3.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/5d/f5/122d9cdb3f51c520d150fef6e87df9279e33d19a9611a87c0d2cf78a89f4/numpy-2.3.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "1c02d0629d25d426585fb2e45a66154081b9fa677bc92a881ff1d216bc9919a8"}}, + {name = "numpy-2.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/51/64/7de3c91e821a2debf77c92962ea3fe6ac2bc45d0778c1cbe15d4fce2fd94/numpy-2.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "d9192da52b9745f7f0766531dcfa978b7763916f158bb63bdb8a1eca0068ab20"}}, + {name = "numpy-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/30/e4/961a5fa681502cd0d68907818b69f67542695b74e3ceaa513918103b7e80/numpy-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "cd7de500a5b66319db419dc3c345244404a164beae0d0937283b907d8152e6ea"}}, + {name = "numpy-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/99/26/92c912b966e47fbbdf2ad556cb17e3a3088e2e1292b9833be1dfa5361a1a/numpy-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "93d4962d8f82af58f0b2eb85daaf1b3ca23fe0a85d0be8f1f2b7bb46034e56d7"}}, + {name = "numpy-2.3.3-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/17/b6/fc8f82cb3520768718834f310c37d96380d9dc61bfdaf05fe5c0b7653e01/numpy-2.3.3-cp312-cp312-win32.whl",hashes = {sha256 = "5534ed6b92f9b7dca6c0a19d6df12d41c68b991cef051d108f6dbff3babc4ebf"}}, + {name = "numpy-2.3.3-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/32/ee/de999f2625b80d043d6d2d628c07d0d5555a677a3cf78fdf868d409b8766/numpy-2.3.3-cp312-cp312-win_amd64.whl",hashes = {sha256 = "497d7cad08e7092dba36e3d296fe4c97708c93daf26643a1ae4b03f6294d30eb"}}, + {name = "numpy-2.3.3-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/49/6e/b479032f8a43559c383acb20816644f5f91c88f633d9271ee84f3b3a996c/numpy-2.3.3-cp312-cp312-win_arm64.whl",hashes = {sha256 = "ca0309a18d4dfea6fc6262a66d06c26cfe4640c3926ceec90e57791a82b6eee5"}}, +] +marker = "\"default\" in dependency_groups and python_version ~= \"3.12\" or \"all\" in extras and python_version ~= \"3.12\" or \"dev\" in extras and python_version ~= \"3.12\" or \"multimodal\" in extras and python_version ~= \"3.12\"" + +[packages.tool.pdm] +dependencies = [] + +[[packages]] +name = "pyyaml" +version = "6.0.3" +requires-python = ">=3.8" +sdist = {name = "pyyaml-6.0.3.tar.gz", url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hashes = {sha256 = "d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}} +wheels = [ + {name = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl",hashes = {sha256 = "8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"}}, + {name = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"}}, + {name = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"}}, + {name = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"}}, + {name = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"}}, + {name = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl",hashes = {sha256 = "7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"}}, + {name = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl",hashes = {sha256 = "5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"}}, + {name = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl",url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl",hashes = {sha256 = "4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"}}, + {name = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl",url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl",hashes = {sha256 = "93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"}}, + {name = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl",hashes = {sha256 = "02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"}}, + {name = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl",hashes = {sha256 = "c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"}}, + {name = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"}}, + {name = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"}}, + {name = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"}}, + {name = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"}}, + {name = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"}}, + {name = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl",hashes = {sha256 = "4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"}}, + {name = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl",hashes = {sha256 = "ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"}}, + {name = "pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl",hashes = {sha256 = "8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8"}}, + {name = "pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1"}}, + {name = "pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c"}}, + {name = "pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5"}}, + {name = "pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6"}}, + {name = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6"}}, + {name = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be"}}, + {name = "pyyaml-6.0.3-cp313-cp313-win32.whl",url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl",hashes = {sha256 = "d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26"}}, + {name = "pyyaml-6.0.3-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl",hashes = {sha256 = "79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c"}}, + {name = "pyyaml-6.0.3-cp313-cp313-win_arm64.whl",url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl",hashes = {sha256 = "5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb"}}, + {name = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl",hashes = {sha256 = "7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"}}, + {name = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"}}, + {name = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"}}, + {name = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"}}, + {name = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"}}, + {name = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"}}, + {name = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"}}, + {name = "pyyaml-6.0.3-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl",hashes = {sha256 = "96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"}}, + {name = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl",hashes = {sha256 = "5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"}}, + {name = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl",hashes = {sha256 = "64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"}}, + {name = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl",hashes = {sha256 = "44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"}}, + {name = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"}}, + {name = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"}}, + {name = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"}}, + {name = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"}}, + {name = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"}}, + {name = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"}}, + {name = "pyyaml-6.0.3-cp311-cp311-win32.whl",url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl",hashes = {sha256 = "8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"}}, + {name = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl",hashes = {sha256 = "9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"}}, + {name = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl",hashes = {sha256 = "214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"}}, + {name = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"}}, + {name = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"}}, + {name = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"}}, + {name = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"}}, + {name = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"}}, + {name = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"}}, + {name = "pyyaml-6.0.3-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl",hashes = {sha256 = "28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"}}, + {name = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl",hashes = {sha256 = "bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"}}, +] +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" + +[packages.tool.pdm] +dependencies = [] + +[[packages]] +name = "setuptools-git-versioning" +version = "2.1.0" +requires-python = ">=3.7" +sdist = {name = "setuptools_git_versioning-2.1.0.tar.gz", url = "https://files.pythonhosted.org/packages/f0/72/507b0b459b1fdbf5705aecbc5330c32d62dd41560718d2720bb6d94607f5/setuptools_git_versioning-2.1.0.tar.gz", hashes = {sha256 = "6aef5b8bb1cfb953b6b343d27cbfc561d96cf2a2ee23c2e0dd3591042a059921"}} +wheels = [ + {name = "setuptools_git_versioning-2.1.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/c0/ba/daf16c2d1965bf6237fb696639e3e93645ac6801f7dcaf9ec694a74e9326/setuptools_git_versioning-2.1.0-py3-none-any.whl",hashes = {sha256 = "09a15cbb9a00884e91a3591a4c9ec1ff93c24b1b4a40de39a44815196beb7ebf"}}, +] +marker = "\"dev\" in extras" + +[packages.tool.pdm] +dependencies = [ + "packaging", + "setuptools", + "tomli>=2.0.1; python_version < \"3.11\"", +] + [[packages]] name = "build" version = "1.3.0" @@ -312,68 +475,107 @@ dependencies = [ ] [[packages]] -name = "numpy" -version = "2.3.3" -requires-python = ">=3.11" -sdist = {name = "numpy-2.3.3.tar.gz", url = "https://files.pythonhosted.org/packages/d0/19/95b3d357407220ed24c139018d2518fab0a61a948e68286a25f1a4d049ff/numpy-2.3.3.tar.gz", hashes = {sha256 = "ddc7c39727ba62b80dfdbedf400d1c10ddfa8eefbd7ec8dcb118be8b56d31029"}} +name = "pillow" +version = "11.3.0" +requires-python = ">=3.9" +sdist = {name = "pillow-11.3.0.tar.gz", url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hashes = {sha256 = "3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523"}} wheels = [ - {name = "numpy-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/6b/01/342ad585ad82419b99bcf7cebe99e61da6bedb89e213c5fd71acc467faee/numpy-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl",hashes = {sha256 = "cd052f1fa6a78dee696b58a914b7229ecfa41f0a6d96dc663c1220a55e137593"}}, - {name = "numpy-2.3.3-cp314-cp314-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/ef/d8/204e0d73fc1b7a9ee80ab1fe1983dd33a4d64a4e30a05364b0208e9a241a/numpy-2.3.3-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "414a97499480067d305fcac9716c29cf4d0d76db6ebf0bf3cbce666677f12652"}}, - {name = "numpy-2.3.3-cp314-cp314-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/22/af/f11c916d08f3a18fb8ba81ab72b5b74a6e42ead4c2846d270eb19845bf74/numpy-2.3.3-cp314-cp314-macosx_14_0_arm64.whl",hashes = {sha256 = "50a5fe69f135f88a2be9b6ca0481a68a136f6febe1916e4920e12f1a34e708a7"}}, - {name = "numpy-2.3.3-cp314-cp314-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/fb/11/0ed919c8381ac9d2ffacd63fd1f0c34d27e99cab650f0eb6f110e6ae4858/numpy-2.3.3-cp314-cp314-macosx_14_0_x86_64.whl",hashes = {sha256 = "b912f2ed2b67a129e6a601e9d93d4fa37bef67e54cac442a2f588a54afe5c67a"}}, - {name = "numpy-2.3.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/ee/83/deb5f77cb0f7ba6cb52b91ed388b47f8f3c2e9930d4665c600408d9b90b9/numpy-2.3.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "9e318ee0596d76d4cb3d78535dc005fa60e5ea348cd131a51e99d0bdbe0b54fe"}}, - {name = "numpy-2.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/77/cc/70e59dcb84f2b005d4f306310ff0a892518cc0c8000a33d0e6faf7ca8d80/numpy-2.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "ce020080e4a52426202bdb6f7691c65bb55e49f261f31a8f506c9f6bc7450421"}}, - {name = "numpy-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/b6/5a/b2ab6c18b4257e099587d5b7f903317bd7115333ad8d4ec4874278eafa61/numpy-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl",hashes = {sha256 = "e6687dc183aa55dae4a705b35f9c0f8cb178bcaa2f029b241ac5356221d5c021"}}, - {name = "numpy-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/b8/f1/8b3fdc44324a259298520dd82147ff648979bed085feeacc1250ef1656c0/numpy-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl",hashes = {sha256 = "d8f3b1080782469fdc1718c4ed1d22549b5fb12af0d57d35e992158a772a37cf"}}, - {name = "numpy-2.3.3-cp314-cp314-win32.whl",url = "https://files.pythonhosted.org/packages/f0/a1/b87a284fb15a42e9274e7fcea0dad259d12ddbf07c1595b26883151ca3b4/numpy-2.3.3-cp314-cp314-win32.whl",hashes = {sha256 = "cb248499b0bc3be66ebd6578b83e5acacf1d6cb2a77f2248ce0e40fbec5a76d0"}}, - {name = "numpy-2.3.3-cp314-cp314-win_amd64.whl",url = "https://files.pythonhosted.org/packages/70/5f/1816f4d08f3b8f66576d8433a66f8fa35a5acfb3bbd0bf6c31183b003f3d/numpy-2.3.3-cp314-cp314-win_amd64.whl",hashes = {sha256 = "691808c2b26b0f002a032c73255d0bd89751425f379f7bcd22d140db593a96e8"}}, - {name = "numpy-2.3.3-cp314-cp314-win_arm64.whl",url = "https://files.pythonhosted.org/packages/8c/de/072420342e46a8ea41c324a555fa90fcc11637583fb8df722936aed1736d/numpy-2.3.3-cp314-cp314-win_arm64.whl",hashes = {sha256 = "9ad12e976ca7b10f1774b03615a2a4bab8addce37ecc77394d8e986927dc0dfe"}}, - {name = "numpy-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/d5/df/ee2f1c0a9de7347f14da5dd3cd3c3b034d1b8607ccb6883d7dd5c035d631/numpy-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl",hashes = {sha256 = "9cc48e09feb11e1db00b320e9d30a4151f7369afb96bd0e48d942d09da3a0d00"}}, - {name = "numpy-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/d6/92/9453bdc5a4e9e69cf4358463f25e8260e2ffc126d52e10038b9077815989/numpy-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl",hashes = {sha256 = "901bf6123879b7f251d3631967fd574690734236075082078e0571977c6a8e6a"}}, - {name = "numpy-2.3.3-cp314-cp314t-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/13/77/1447b9eb500f028bb44253105bd67534af60499588a5149a94f18f2ca917/numpy-2.3.3-cp314-cp314t-macosx_14_0_arm64.whl",hashes = {sha256 = "7f025652034199c301049296b59fa7d52c7e625017cae4c75d8662e377bf487d"}}, - {name = "numpy-2.3.3-cp314-cp314t-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/3d/f9/d72221b6ca205f9736cb4b2ce3b002f6e45cd67cd6a6d1c8af11a2f0b649/numpy-2.3.3-cp314-cp314t-macosx_14_0_x86_64.whl",hashes = {sha256 = "533ca5f6d325c80b6007d4d7fb1984c303553534191024ec6a524a4c92a5935a"}}, - {name = "numpy-2.3.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/3c/5f/d12834711962ad9c46af72f79bb31e73e416ee49d17f4c797f72c96b6ca5/numpy-2.3.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "0edd58682a399824633b66885d699d7de982800053acf20be1eaa46d92009c54"}}, - {name = "numpy-2.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/a1/0d/fdbec6629d97fd1bebed56cd742884e4eead593611bbe1abc3eb40d304b2/numpy-2.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "367ad5d8fbec5d9296d18478804a530f1191e24ab4d75ab408346ae88045d25e"}}, - {name = "numpy-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/9b/09/0a35196dc5575adde1eb97ddfbc3e1687a814f905377621d18ca9bc2b7dd/numpy-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "8f6ac61a217437946a1fa48d24c47c91a0c4f725237871117dea264982128097"}}, - {name = "numpy-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/7a/ca/c9de3ea397d576f1b6753eaa906d4cdef1bf97589a6d9825a349b4729cc2/numpy-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "179a42101b845a816d464b6fe9a845dfaf308fdfc7925387195570789bb2c970"}}, - {name = "numpy-2.3.3-cp314-cp314t-win32.whl",url = "https://files.pythonhosted.org/packages/fd/c2/e5ed830e08cd0196351db55db82f65bc0ab05da6ef2b72a836dcf1936d2f/numpy-2.3.3-cp314-cp314t-win32.whl",hashes = {sha256 = "1250c5d3d2562ec4174bce2e3a1523041595f9b651065e4a4473f5f48a6bc8a5"}}, - {name = "numpy-2.3.3-cp314-cp314t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/47/c7/b0f6b5b67f6788a0725f744496badbb604d226bf233ba716683ebb47b570/numpy-2.3.3-cp314-cp314t-win_amd64.whl",hashes = {sha256 = "b37a0b2e5935409daebe82c1e42274d30d9dd355852529eab91dab8dcca7419f"}}, - {name = "numpy-2.3.3-cp314-cp314t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/06/b9/33bba5ff6fb679aa0b1f8a07e853f002a6b04b9394db3069a1270a7784ca/numpy-2.3.3-cp314-cp314t-win_arm64.whl",hashes = {sha256 = "78c9f6560dc7e6b3990e32df7ea1a50bbd0e2a111e05209963f5ddcab7073b0b"}}, - {name = "numpy-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/7d/b9/984c2b1ee61a8b803bf63582b4ac4242cf76e2dbd663efeafcb620cc0ccb/numpy-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl",hashes = {sha256 = "f5415fb78995644253370985342cd03572ef8620b934da27d77377a2285955bf"}}, - {name = "numpy-2.3.3-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/a6/e4/07970e3bed0b1384d22af1e9912527ecbeb47d3b26e9b6a3bced068b3bea/numpy-2.3.3-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "d00de139a3324e26ed5b95870ce63be7ec7352171bc69a4cf1f157a48e3eb6b7"}}, - {name = "numpy-2.3.3-cp313-cp313-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/35/c7/477a83887f9de61f1203bad89cf208b7c19cc9fef0cebef65d5a1a0619f2/numpy-2.3.3-cp313-cp313-macosx_14_0_arm64.whl",hashes = {sha256 = "9dc13c6a5829610cc07422bc74d3ac083bd8323f14e2827d992f9e52e22cd6a6"}}, - {name = "numpy-2.3.3-cp313-cp313-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/52/47/93b953bd5866a6f6986344d045a207d3f1cfbad99db29f534ea9cee5108c/numpy-2.3.3-cp313-cp313-macosx_14_0_x86_64.whl",hashes = {sha256 = "d79715d95f1894771eb4e60fb23f065663b2298f7d22945d66877aadf33d00c7"}}, - {name = "numpy-2.3.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/23/83/377f84aaeb800b64c0ef4de58b08769e782edcefa4fea712910b6f0afd3c/numpy-2.3.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "952cfd0748514ea7c3afc729a0fc639e61655ce4c55ab9acfab14bda4f402b4c"}}, - {name = "numpy-2.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/9a/a5/bf3db6e66c4b160d6ea10b534c381a1955dfab34cb1017ea93aa33c70ed3/numpy-2.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "5b83648633d46f77039c29078751f80da65aa64d5622a3cd62aaef9d835b6c93"}}, - {name = "numpy-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/a2/59/1287924242eb4fa3f9b3a2c30400f2e17eb2707020d1c5e3086fe7330717/numpy-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "b001bae8cea1c7dfdb2ae2b017ed0a6f2102d7a70059df1e338e307a4c78a8ae"}}, - {name = "numpy-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/e6/93/b3d47ed882027c35e94ac2320c37e452a549f582a5e801f2d34b56973c97/numpy-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "8e9aced64054739037d42fb84c54dd38b81ee238816c948c8f3ed134665dcd86"}}, - {name = "numpy-2.3.3-cp313-cp313-win32.whl",url = "https://files.pythonhosted.org/packages/20/d9/487a2bccbf7cc9d4bfc5f0f197761a5ef27ba870f1e3bbb9afc4bbe3fcc2/numpy-2.3.3-cp313-cp313-win32.whl",hashes = {sha256 = "9591e1221db3f37751e6442850429b3aabf7026d3b05542d102944ca7f00c8a8"}}, - {name = "numpy-2.3.3-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/1b/b5/263ebbbbcede85028f30047eab3d58028d7ebe389d6493fc95ae66c636ab/numpy-2.3.3-cp313-cp313-win_amd64.whl",hashes = {sha256 = "f0dadeb302887f07431910f67a14d57209ed91130be0adea2f9793f1a4f817cf"}}, - {name = "numpy-2.3.3-cp313-cp313-win_arm64.whl",url = "https://files.pythonhosted.org/packages/fa/75/67b8ca554bbeaaeb3fac2e8bce46967a5a06544c9108ec0cf5cece559b6c/numpy-2.3.3-cp313-cp313-win_arm64.whl",hashes = {sha256 = "3c7cf302ac6e0b76a64c4aecf1a09e51abd9b01fc7feee80f6c43e3ab1b1dbc5"}}, - {name = "numpy-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/11/d0/0d1ddec56b162042ddfafeeb293bac672de9b0cfd688383590090963720a/numpy-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl",hashes = {sha256 = "eda59e44957d272846bb407aad19f89dc6f58fecf3504bd144f4c5cf81a7eacc"}}, - {name = "numpy-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/36/9e/1996ca6b6d00415b6acbdd3c42f7f03ea256e2c3f158f80bd7436a8a19f3/numpy-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl",hashes = {sha256 = "823d04112bc85ef5c4fda73ba24e6096c8f869931405a80aa8b0e604510a26bc"}}, - {name = "numpy-2.3.3-cp313-cp313t-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/05/24/43da09aa764c68694b76e84b3d3f0c44cb7c18cdc1ba80e48b0ac1d2cd39/numpy-2.3.3-cp313-cp313t-macosx_14_0_arm64.whl",hashes = {sha256 = "40051003e03db4041aa325da2a0971ba41cf65714e65d296397cc0e32de6018b"}}, - {name = "numpy-2.3.3-cp313-cp313t-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/bc/14/50ffb0f22f7218ef8af28dd089f79f68289a7a05a208db9a2c5dcbe123c1/numpy-2.3.3-cp313-cp313t-macosx_14_0_x86_64.whl",hashes = {sha256 = "6ee9086235dd6ab7ae75aba5662f582a81ced49f0f1c6de4260a78d8f2d91a19"}}, - {name = "numpy-2.3.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/55/52/af46ac0795e09657d45a7f4db961917314377edecf66db0e39fa7ab5c3d3/numpy-2.3.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "94fcaa68757c3e2e668ddadeaa86ab05499a70725811e582b6a9858dd472fb30"}}, - {name = "numpy-2.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/a7/b1/dc226b4c90eb9f07a3fff95c2f0db3268e2e54e5cce97c4ac91518aee71b/numpy-2.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "da1a74b90e7483d6ce5244053399a614b1d6b7bc30a60d2f570e5071f8959d3e"}}, - {name = "numpy-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/9d/9d/9d8d358f2eb5eced14dba99f110d83b5cd9a4460895230f3b396ad19a323/numpy-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "2990adf06d1ecee3b3dcbb4977dfab6e9f09807598d647f04d385d29e7a3c3d3"}}, - {name = "numpy-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/b6/27/b3922660c45513f9377b3fb42240bec63f203c71416093476ec9aa0719dc/numpy-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "ed635ff692483b8e3f0fcaa8e7eb8a75ee71aa6d975388224f70821421800cea"}}, - {name = "numpy-2.3.3-cp313-cp313t-win32.whl",url = "https://files.pythonhosted.org/packages/5b/8e/3ab61a730bdbbc201bb245a71102aa609f0008b9ed15255500a99cd7f780/numpy-2.3.3-cp313-cp313t-win32.whl",hashes = {sha256 = "a333b4ed33d8dc2b373cc955ca57babc00cd6f9009991d9edc5ddbc1bac36bcd"}}, - {name = "numpy-2.3.3-cp313-cp313t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/1c/3a/e22b766b11f6030dc2decdeff5c2fb1610768055603f9f3be88b6d192fb2/numpy-2.3.3-cp313-cp313t-win_amd64.whl",hashes = {sha256 = "4384a169c4d8f97195980815d6fcad04933a7e1ab3b530921c3fef7a1c63426d"}}, - {name = "numpy-2.3.3-cp313-cp313t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/7b/42/c2e2bc48c5e9b2a83423f99733950fbefd86f165b468a3d85d52b30bf782/numpy-2.3.3-cp313-cp313t-win_arm64.whl",hashes = {sha256 = "75370986cc0bc66f4ce5110ad35aae6d182cc4ce6433c40ad151f53690130bf1"}}, - {name = "numpy-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/51/5d/bb7fc075b762c96329147799e1bcc9176ab07ca6375ea976c475482ad5b3/numpy-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl",hashes = {sha256 = "cfdd09f9c84a1a934cde1eec2267f0a43a7cd44b2cca4ff95b7c0d14d144b0bf"}}, - {name = "numpy-2.3.3-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/6b/0e/c6211bb92af26517acd52125a237a92afe9c3124c6a68d3b9f81b62a0568/numpy-2.3.3-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "cb32e3cf0f762aee47ad1ddc6672988f7f27045b0783c887190545baba73aa25"}}, - {name = "numpy-2.3.3-cp312-cp312-macosx_14_0_arm64.whl",url = "https://files.pythonhosted.org/packages/22/f2/07bb754eb2ede9073f4054f7c0286b0d9d2e23982e090a80d478b26d35ca/numpy-2.3.3-cp312-cp312-macosx_14_0_arm64.whl",hashes = {sha256 = "396b254daeb0a57b1fe0ecb5e3cff6fa79a380fa97c8f7781a6d08cd429418fe"}}, - {name = "numpy-2.3.3-cp312-cp312-macosx_14_0_x86_64.whl",url = "https://files.pythonhosted.org/packages/81/0a/afa51697e9fb74642f231ea36aca80fa17c8fb89f7a82abd5174023c3960/numpy-2.3.3-cp312-cp312-macosx_14_0_x86_64.whl",hashes = {sha256 = "067e3d7159a5d8f8a0b46ee11148fc35ca9b21f61e3c49fbd0a027450e65a33b"}}, - {name = "numpy-2.3.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/5d/f5/122d9cdb3f51c520d150fef6e87df9279e33d19a9611a87c0d2cf78a89f4/numpy-2.3.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "1c02d0629d25d426585fb2e45a66154081b9fa677bc92a881ff1d216bc9919a8"}}, - {name = "numpy-2.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/51/64/7de3c91e821a2debf77c92962ea3fe6ac2bc45d0778c1cbe15d4fce2fd94/numpy-2.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "d9192da52b9745f7f0766531dcfa978b7763916f158bb63bdb8a1eca0068ab20"}}, - {name = "numpy-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/30/e4/961a5fa681502cd0d68907818b69f67542695b74e3ceaa513918103b7e80/numpy-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "cd7de500a5b66319db419dc3c345244404a164beae0d0937283b907d8152e6ea"}}, - {name = "numpy-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/99/26/92c912b966e47fbbdf2ad556cb17e3a3088e2e1292b9833be1dfa5361a1a/numpy-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "93d4962d8f82af58f0b2eb85daaf1b3ca23fe0a85d0be8f1f2b7bb46034e56d7"}}, - {name = "numpy-2.3.3-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/17/b6/fc8f82cb3520768718834f310c37d96380d9dc61bfdaf05fe5c0b7653e01/numpy-2.3.3-cp312-cp312-win32.whl",hashes = {sha256 = "5534ed6b92f9b7dca6c0a19d6df12d41c68b991cef051d108f6dbff3babc4ebf"}}, - {name = "numpy-2.3.3-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/32/ee/de999f2625b80d043d6d2d628c07d0d5555a677a3cf78fdf868d409b8766/numpy-2.3.3-cp312-cp312-win_amd64.whl",hashes = {sha256 = "497d7cad08e7092dba36e3d296fe4c97708c93daf26643a1ae4b03f6294d30eb"}}, - {name = "numpy-2.3.3-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/49/6e/b479032f8a43559c383acb20816644f5f91c88f633d9271ee84f3b3a996c/numpy-2.3.3-cp312-cp312-win_arm64.whl",hashes = {sha256 = "ca0309a18d4dfea6fc6262a66d06c26cfe4640c3926ceec90e57791a82b6eee5"}}, + {name = "pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl",hashes = {sha256 = "d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12"}}, + {name = "pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a"}}, + {name = "pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632"}}, + {name = "pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673"}}, + {name = "pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027"}}, + {name = "pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77"}}, + {name = "pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl",hashes = {sha256 = "7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874"}}, + {name = "pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl",hashes = {sha256 = "98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a"}}, + {name = "pillow-11.3.0-cp314-cp314-win32.whl",url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl",hashes = {sha256 = "02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214"}}, + {name = "pillow-11.3.0-cp314-cp314-win_amd64.whl",url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl",hashes = {sha256 = "a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635"}}, + {name = "pillow-11.3.0-cp314-cp314-win_arm64.whl",url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl",hashes = {sha256 = "155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6"}}, + {name = "pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl",hashes = {sha256 = "59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae"}}, + {name = "pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl",hashes = {sha256 = "f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653"}}, + {name = "pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6"}}, + {name = "pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36"}}, + {name = "pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b"}}, + {name = "pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477"}}, + {name = "pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50"}}, + {name = "pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b"}}, + {name = "pillow-11.3.0-cp314-cp314t-win32.whl",url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl",hashes = {sha256 = "118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12"}}, + {name = "pillow-11.3.0-cp314-cp314t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl",hashes = {sha256 = "8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db"}}, + {name = "pillow-11.3.0-cp314-cp314t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl",hashes = {sha256 = "79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa"}}, + {name = "pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl",url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl",hashes = {sha256 = "1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd"}}, + {name = "pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl",url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl",hashes = {sha256 = "30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8"}}, + {name = "pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl",url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl",hashes = {sha256 = "7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f"}}, + {name = "pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl",hashes = {sha256 = "ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c"}}, + {name = "pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd"}}, + {name = "pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e"}}, + {name = "pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1"}}, + {name = "pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805"}}, + {name = "pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8"}}, + {name = "pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2"}}, + {name = "pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b"}}, + {name = "pillow-11.3.0-cp313-cp313-win32.whl",url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl",hashes = {sha256 = "a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3"}}, + {name = "pillow-11.3.0-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl",hashes = {sha256 = "0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51"}}, + {name = "pillow-11.3.0-cp313-cp313-win_arm64.whl",url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl",hashes = {sha256 = "1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580"}}, + {name = "pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl",hashes = {sha256 = "4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e"}}, + {name = "pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl",hashes = {sha256 = "5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d"}}, + {name = "pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced"}}, + {name = "pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c"}}, + {name = "pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8"}}, + {name = "pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59"}}, + {name = "pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe"}}, + {name = "pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c"}}, + {name = "pillow-11.3.0-cp313-cp313t-win32.whl",url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl",hashes = {sha256 = "2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788"}}, + {name = "pillow-11.3.0-cp313-cp313t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl",hashes = {sha256 = "857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31"}}, + {name = "pillow-11.3.0-cp313-cp313t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl",hashes = {sha256 = "8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e"}}, + {name = "pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl",hashes = {sha256 = "fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4"}}, + {name = "pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69"}}, + {name = "pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d"}}, + {name = "pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6"}}, + {name = "pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7"}}, + {name = "pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024"}}, + {name = "pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809"}}, + {name = "pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d"}}, + {name = "pillow-11.3.0-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl",hashes = {sha256 = "7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149"}}, + {name = "pillow-11.3.0-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl",hashes = {sha256 = "a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d"}}, + {name = "pillow-11.3.0-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl",hashes = {sha256 = "2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542"}}, + {name = "pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl",url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl",hashes = {sha256 = "1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722"}}, + {name = "pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288"}}, + {name = "pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d"}}, + {name = "pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494"}}, + {name = "pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58"}}, + {name = "pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f"}}, + {name = "pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e"}}, + {name = "pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94"}}, + {name = "pillow-11.3.0-cp311-cp311-win32.whl",url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl",hashes = {sha256 = "b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0"}}, + {name = "pillow-11.3.0-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl",hashes = {sha256 = "1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac"}}, + {name = "pillow-11.3.0-cp311-cp311-win_arm64.whl",url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl",hashes = {sha256 = "30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd"}}, + {name = "pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl",url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl",hashes = {sha256 = "7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6"}}, + {name = "pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl",hashes = {sha256 = "9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438"}}, + {name = "pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3"}}, + {name = "pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c"}}, + {name = "pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361"}}, + {name = "pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7"}}, + {name = "pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl",url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl",hashes = {sha256 = "c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8"}}, + {name = "pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl",url = "https://files.pythonhosted.org/packages/4c/5d/45a3553a253ac8763f3561371432a90bdbe6000fbdcf1397ffe502aa206c/pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl",hashes = {sha256 = "1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860"}}, + {name = "pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/7c/c8/67c12ab069ef586a25a4a79ced553586748fad100c77c0ce59bb4983ac98/pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad"}}, + {name = "pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/2f/bd/6741ebd56263390b382ae4c5de02979af7f8bd9807346d068700dd6d5cf9/pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0"}}, + {name = "pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/ca/0b/c412a9e27e1e6a829e6ab6c2dca52dd563efbedf4c9c6aa453d9a9b77359/pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b"}}, + {name = "pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/59/9d/9b7076aaf30f5dd17e5e5589b2d2f5a5d7e30ff67a171eb686e4eecc2adf/pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50"}}, + {name = "pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/f0/16/1a6bf01fb622fb9cf5c91683823f073f053005c849b1f52ed613afcf8dae/pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae"}}, + {name = "pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/7b/e6/6ff7077077eb47fde78739e7d570bdcd7c10495666b6afcd23ab56b19a43/pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9"}}, + {name = "pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/c3/3a/b13f36832ea6d279a697231658199e0a03cd87ef12048016bdcc84131601/pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e"}}, + {name = "pillow-11.3.0-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/6c/e4/61b2e1a7528740efbc70b3d581f33937e38e98ef3d50b05007267a55bcb2/pillow-11.3.0-cp310-cp310-win32.whl",hashes = {sha256 = "89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6"}}, + {name = "pillow-11.3.0-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/a9/d3/60c781c83a785d6afbd6a326ed4d759d141de43aa7365725cbcd65ce5e54/pillow-11.3.0-cp310-cp310-win_amd64.whl",hashes = {sha256 = "19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f"}}, + {name = "pillow-11.3.0-cp310-cp310-win_arm64.whl",url = "https://files.pythonhosted.org/packages/9f/28/4f4a0203165eefb3763939c6789ba31013a2e90adffb456610f30f613850/pillow-11.3.0-cp310-cp310-win_arm64.whl",hashes = {sha256 = "819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f"}}, + {name = "pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl",url = "https://files.pythonhosted.org/packages/6f/8b/209bd6b62ce8367f47e68a218bffac88888fdf2c9fcf1ecadc6c3ec1ebc7/pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl",hashes = {sha256 = "3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967"}}, + {name = "pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/2e/e6/231a0b76070c2cfd9e260a7a5b504fb72da0a95279410fa7afd99d9751d6/pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl",hashes = {sha256 = "b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe"}}, + {name = "pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/13/f4/10cf94fda33cb12765f2397fc285fa6d8eb9c29de7f3185165b702fc7386/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c"}}, + {name = "pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/72/c9/583821097dc691880c92892e8e2d41fe0a5a3d6021f4963371d2f6d57250/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25"}}, + {name = "pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/3b/8e/5c9d410f9217b12320efc7c413e72693f48468979a013ad17fd690397b9a/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27"}}, + {name = "pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/62/bb/78347dbe13219991877ffb3a91bf09da8317fbfcd4b5f9140aeae020ad71/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a"}}, + {name = "pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl",url = "https://files.pythonhosted.org/packages/d9/28/1000353d5e61498aaeaaf7f1e4b49ddb05f2c6575f9d4f9f914a3538b6e1/pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl",hashes = {sha256 = "8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f"}}, ] -marker = "\"default\" in dependency_groups and python_version ~= \"3.12\" or \"dev\" in extras and python_version ~= \"3.12\"" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" [packages.tool.pdm] dependencies = [] @@ -397,88 +599,20 @@ dependencies = [ "virtualenv>=20.10.0", ] -[[packages]] -name = "pyyaml" -version = "6.0.3" -requires-python = ">=3.8" -sdist = {name = "pyyaml-6.0.3.tar.gz", url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hashes = {sha256 = "d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}} -wheels = [ - {name = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl",hashes = {sha256 = "8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"}}, - {name = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"}}, - {name = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"}}, - {name = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"}}, - {name = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"}}, - {name = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl",hashes = {sha256 = "7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"}}, - {name = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl",hashes = {sha256 = "5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"}}, - {name = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl",url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl",hashes = {sha256 = "4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"}}, - {name = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl",url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl",hashes = {sha256 = "93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"}}, - {name = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl",hashes = {sha256 = "02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"}}, - {name = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl",hashes = {sha256 = "c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"}}, - {name = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"}}, - {name = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"}}, - {name = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"}}, - {name = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"}}, - {name = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"}}, - {name = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl",hashes = {sha256 = "4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"}}, - {name = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl",hashes = {sha256 = "ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"}}, - {name = "pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl",hashes = {sha256 = "8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8"}}, - {name = "pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1"}}, - {name = "pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c"}}, - {name = "pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5"}}, - {name = "pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6"}}, - {name = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6"}}, - {name = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be"}}, - {name = "pyyaml-6.0.3-cp313-cp313-win32.whl",url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl",hashes = {sha256 = "d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26"}}, - {name = "pyyaml-6.0.3-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl",hashes = {sha256 = "79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c"}}, - {name = "pyyaml-6.0.3-cp313-cp313-win_arm64.whl",url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl",hashes = {sha256 = "5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb"}}, - {name = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl",hashes = {sha256 = "7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"}}, - {name = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"}}, - {name = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"}}, - {name = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"}}, - {name = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"}}, - {name = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"}}, - {name = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"}}, - {name = "pyyaml-6.0.3-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl",hashes = {sha256 = "96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"}}, - {name = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl",hashes = {sha256 = "5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"}}, - {name = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl",hashes = {sha256 = "64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"}}, - {name = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl",hashes = {sha256 = "44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"}}, - {name = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"}}, - {name = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"}}, - {name = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"}}, - {name = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"}}, - {name = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"}}, - {name = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"}}, - {name = "pyyaml-6.0.3-cp311-cp311-win32.whl",url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl",hashes = {sha256 = "8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"}}, - {name = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl",hashes = {sha256 = "9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"}}, - {name = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl",hashes = {sha256 = "214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"}}, - {name = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"}}, - {name = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"}}, - {name = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"}}, - {name = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"}}, - {name = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"}}, - {name = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"}}, - {name = "pyyaml-6.0.3-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl",hashes = {sha256 = "28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"}}, - {name = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl",hashes = {sha256 = "bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"}}, -] -marker = "\"default\" in dependency_groups or \"dev\" in extras" - -[packages.tool.pdm] -dependencies = [] - [[packages]] name = "pydantic" -version = "2.12.0" +version = "2.12.2" requires-python = ">=3.9" -sdist = {name = "pydantic-2.12.0.tar.gz", url = "https://files.pythonhosted.org/packages/c3/da/b8a7ee04378a53f6fefefc0c5e05570a3ebfdfa0523a878bcd3b475683ee/pydantic-2.12.0.tar.gz", hashes = {sha256 = "c1a077e6270dbfb37bfd8b498b3981e2bb18f68103720e51fa6c306a5a9af563"}} +sdist = {name = "pydantic-2.12.2.tar.gz", url = "https://files.pythonhosted.org/packages/8d/35/d319ed522433215526689bad428a94058b6dd12190ce7ddd78618ac14b28/pydantic-2.12.2.tar.gz", hashes = {sha256 = "7b8fa15b831a4bbde9d5b84028641ac3080a4ca2cbd4a621a661687e741624fd"}} wheels = [ - {name = "pydantic-2.12.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/f4/9d/d5c855424e2e5b6b626fbc6ec514d8e655a600377ce283008b115abb7445/pydantic-2.12.0-py3-none-any.whl",hashes = {sha256 = "f6a1da352d42790537e95e83a8bdfb91c7efbae63ffd0b86fa823899e807116f"}}, + {name = "pydantic-2.12.2-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/6c/98/468cb649f208a6f1279448e6e5247b37ae79cf5e4041186f1e2ef3d16345/pydantic-2.12.2-py3-none-any.whl",hashes = {sha256 = "25ff718ee909acd82f1ff9b1a4acfd781bb23ab3739adaa7144f19a6a4e231ae"}}, ] marker = "\"default\" in dependency_groups" [packages.tool.pdm] dependencies = [ "annotated-types>=0.6.0", - "pydantic-core==2.41.1", + "pydantic-core==2.41.4", "typing-extensions>=4.14.1", "typing-inspection>=0.4.2", ] @@ -700,7 +834,7 @@ sdist = {name = "setuptools-80.9.0.tar.gz", url = "https://files.pythonhosted.or wheels = [ {name = "setuptools-80.9.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl",hashes = {sha256 = "062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" [packages.tool.pdm] dependencies = [] @@ -736,6 +870,79 @@ dependencies = [ "colorama>=0.4.5; sys_platform == \"win32\"", ] +[[packages]] +name = "torch" +version = "2.9.0+cpu" +requires-python = ">=3.10" +wheels = [ + {name = "torch-2.9.0+cpu-cp314-cp314-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp314-cp314-manylinux_2_28_aarch64.whl",hashes = {sha256 = "44aadb735774d4a99525d2ec29126b23016c44a07b02ce6c237dfa61a223dd52"}}, + {name = "torch-2.9.0+cpu-cp314-cp314-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp314-cp314-manylinux_2_28_x86_64.whl",hashes = {sha256 = "b355e07b7f0c369cb031adfcbff5c37a609abcea091b918a39886412afd2e07d"}}, + {name = "torch-2.9.0+cpu-cp314-cp314-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp314-cp314-win_amd64.whl",hashes = {sha256 = "c2698999361d73c2d25d7cc8a787130188d49b183abb18b554228daa102e1594"}}, + {name = "torch-2.9.0+cpu-cp314-cp314t-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp314-cp314t-manylinux_2_28_aarch64.whl",hashes = {sha256 = "fa0d1373d04b30ff8f12d542135d292f1a1ddb7c0d852a3d487a320360e5dab9"}}, + {name = "torch-2.9.0+cpu-cp314-cp314t-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp314-cp314t-manylinux_2_28_x86_64.whl",hashes = {sha256 = "2f49bb57a5fe0dc7f8e73ea9e5d36ebda2ea25b8a714a788f0fc2fc47d20a830"}}, + {name = "torch-2.9.0+cpu-cp314-cp314t-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp314-cp314t-win_amd64.whl",hashes = {sha256 = "3a60d1ecf27a9cce839b3aa665b26f0af1b1007b9c9f1e7f597f6b7bdf107617"}}, + {name = "torch-2.9.0-cp314-cp314-macosx_11_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "d8e2ab7f86010330bdcc39c8b2c795590cc75e37df4823cdaee2c98d6e3ff4a3"}}, + {name = "torch-2.9.0-cp314-cp314t-macosx_11_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0-cp314-cp314t-macosx_11_0_arm64.whl",hashes = {sha256 = "a3e859039c985d8e3ea60d7a54ca7e97ea2ae15e31beced4f3260128a161bb01"}}, + {name = "torch-2.9.0+cpu-cp313-cp313-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl",hashes = {sha256 = "be4438d8dad7f0d5a5e54f0feef8a893446894ec87f102bb1d82dcc4518542e4"}}, + {name = "torch-2.9.0+cpu-cp313-cp313-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl",hashes = {sha256 = "6c9b217584400963d5b4daddb3711ec7a3778eab211e18654fba076cce3b8682"}}, + {name = "torch-2.9.0+cpu-cp313-cp313-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp313-cp313-win_amd64.whl",hashes = {sha256 = "728372e3f58c5826445f677746e5311c1935c1a7c59599f73a49ded850e038e8"}}, + {name = "torch-2.9.0+cpu-cp313-cp313-win_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp313-cp313-win_arm64.whl",hashes = {sha256 = "95e56c26f919fbb98f16e7a0b87af494b893f9da9a65a020f17a01c13e520a81"}}, + {name = "torch-2.9.0+cpu-cp313-cp313t-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl",hashes = {sha256 = "6c777160288b08555820781ae0f3a2c67a59bd24b065e88ca1ec20e2f9dc8ac7"}}, + {name = "torch-2.9.0+cpu-cp313-cp313t-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl",hashes = {sha256 = "528fd338311f31c9fb18038cafd00e6eae0bf5ad5577521701acb62510753d18"}}, + {name = "torch-2.9.0+cpu-cp313-cp313t-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp313-cp313t-win_amd64.whl",hashes = {sha256 = "d572863990e7d2762b547735ef589f6350d9eb4e441d38753a1c33636698cf4c"}}, + {name = "torch-2.9.0-cp313-cp313t-macosx_11_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0-cp313-cp313t-macosx_11_0_arm64.whl",hashes = {sha256 = "259548471194ab63d7ea273873053a6e3cc23530c1510f01e9d7ad259187bbd0"}}, + {name = "torch-2.9.0-cp313-none-macosx_11_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0-cp313-none-macosx_11_0_arm64.whl",hashes = {sha256 = "e24836d968b54ef4dfb05594001a61958711ac9224026291e4e3f92f83a6fd7f"}}, + {name = "torch-2.9.0+cpu-cp312-cp312-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl",hashes = {sha256 = "3a651434ae1248b0568c12b5f9e3acc8942eb28378d9d04a79302938b68c6f24"}}, + {name = "torch-2.9.0+cpu-cp312-cp312-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl",hashes = {sha256 = "28f6eb31b08180a5c5e98d5bc14eef6909c9f5a1dbff9632c3e02a8773449349"}}, + {name = "torch-2.9.0+cpu-cp312-cp312-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp312-cp312-win_amd64.whl",hashes = {sha256 = "e438061b87ec7dd6018fca9f975219889aa0a3f6cdc3ea10dd0ae2bc7f1c47ce"}}, + {name = "torch-2.9.0+cpu-cp312-cp312-win_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp312-cp312-win_arm64.whl",hashes = {sha256 = "eb13ff1c34e338d722e76a4fd83b8d282782505bd1b99af4b3c32da66eba6eb4"}}, + {name = "torch-2.9.0-cp312-none-macosx_11_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0-cp312-none-macosx_11_0_arm64.whl",hashes = {sha256 = "4de0ed8cbc457a506dbca40376e206a29efee10756a00f1f3404bf67ad737d04"}}, + {name = "torch-2.9.0+cpu-cp311-cp311-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp311-cp311-manylinux_2_28_aarch64.whl",hashes = {sha256 = "da77341ccaba31762d9238b0942c165c4582a26818f3045b052b39cebdd7ad9d"}}, + {name = "torch-2.9.0+cpu-cp311-cp311-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp311-cp311-manylinux_2_28_x86_64.whl",hashes = {sha256 = "add3e93ecc1eeaa6853f6a973ce60ffb3cb14ed2e80f5055e139b09385dce0a7"}}, + {name = "torch-2.9.0+cpu-cp311-cp311-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp311-cp311-win_amd64.whl",hashes = {sha256 = "389e1e0b8083fd355f7caf5ba82356b5e01c318998bd575dbf2285a0d8137089"}}, + {name = "torch-2.9.0+cpu-cp311-cp311-win_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp311-cp311-win_arm64.whl",hashes = {sha256 = "5ce3d01aef91dc078fbb121814e556d55bc886d303efaf42c4fe67e411f5f9ad"}}, + {name = "torch-2.9.0-cp311-none-macosx_11_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0-cp311-none-macosx_11_0_arm64.whl",hashes = {sha256 = "aa4483602586cc9a35d1cf33771a9977f05f642b9161518a289e36548a0b77c2"}}, + {name = "torch-2.9.0+cpu-cp310-cp310-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp310-cp310-manylinux_2_28_aarch64.whl",hashes = {sha256 = "b224792ea567b52c7f1ce1d789567f6920e06fd3b339fa1e1b05948845f783ad"}}, + {name = "torch-2.9.0+cpu-cp310-cp310-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp310-cp310-manylinux_2_28_x86_64.whl",hashes = {sha256 = "bd2a257e670ede9fc01c6d76dccdc473040913b8e9328169bf177dbdc38e2484"}}, + {name = "torch-2.9.0+cpu-cp310-cp310-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp310-cp310-win_amd64.whl",hashes = {sha256 = "96f3f7aa4eb9e7fc5af8a722eaf1e5e32e3039dbafe817178d7b90a8566be32d"}}, + {name = "torch-2.9.0-cp310-none-macosx_11_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0-cp310-none-macosx_11_0_arm64.whl",hashes = {sha256 = "59484193b01299bf669520505a72b29d59a0028ae4c6d95f492938f186592208"}}, +] +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" + +[packages.tool.pdm] +dependencies = [ + "filelock", + "typing-extensions>=4.10.0", + "setuptools; python_version >= \"3.12\"", + "sympy>=1.13.3", + "networkx>=2.5.1", + "jinja2", + "fsspec>=0.8.5", +] + +[[packages]] +name = "torchcodec" +version = "0.7.0" +requires-python = ">=3.8" +wheels = [ + {name = "torchcodec-0.7.0-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/3c/82/7c7691d538f67704b2b2444deb0e234ae564f9329bc9becf66d69998bc9b/torchcodec-0.7.0-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "32a0115035a7f0a77fa451f67c101e0273a3a37d33b69e1bcd777f00aceb7340"}}, + {name = "torchcodec-0.7.0-cp313-cp313-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/01/25/177ea01d138598ab68d5e3b000789e8617bf97874bd8f761d89093f419ba/torchcodec-0.7.0-cp313-cp313-manylinux_2_28_x86_64.whl",hashes = {sha256 = "9c70f910f9f48e6625aacaed534f766e13d447b895dc7299e96d4db9a93f1514"}}, + {name = "torchcodec-0.7.0-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/5c/a9/e2b6301fbf4590d352e183bef64927f74ef4d4f660cca3ed7a32dda60484/torchcodec-0.7.0-cp313-cp313-win_amd64.whl",hashes = {sha256 = "31b402c9ae3c6e9f33c41fddf7058f9492c443ad55d02f022395f8fa196b58f6"}}, + {name = "torchcodec-0.7.0-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/c7/b2/6d3e190fcd18c65b35f6da734d4415c72b42c8a72ffc2494d998bad8caf3/torchcodec-0.7.0-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "d9d082bbb599f4f7715bfc3b1afa5bc16d8fb9d852e68084c63f1973cc78a1cb"}}, + {name = "torchcodec-0.7.0-cp312-cp312-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/41/10/4a1a8407d0fad37cb43d1f749e7b422e5a0f6def17f3b90ab9ab9a105e32/torchcodec-0.7.0-cp312-cp312-manylinux_2_28_x86_64.whl",hashes = {sha256 = "3cd23c3296c9b071d56bb2c534a6a98275d65c1a6a7213cdb72a26ec9f9d2fd8"}}, + {name = "torchcodec-0.7.0-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/9b/e7/a2fa7ed9c81d7d683d37ca7204007b421c0537132364a9cfd8d577f19a96/torchcodec-0.7.0-cp312-cp312-win_amd64.whl",hashes = {sha256 = "ac942831bff02e6041d8718b71c6f63e4e37c05dd95e72863725c9dbef0d4a7b"}}, + {name = "torchcodec-0.7.0-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/6a/f1/bb2b5ab929ef3f092cb6508673510ffc2aafd8324493c94a2d41f1c8a683/torchcodec-0.7.0-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "967a14b31e04721901ddbf965f9e9f733f328c5e98a51e22f414e25ac32e20ba"}}, + {name = "torchcodec-0.7.0-cp311-cp311-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/06/14/8ff28247988365fc47e8471e28cdfd8d037232fcf73abb67ee815ac80f1d/torchcodec-0.7.0-cp311-cp311-manylinux_2_28_x86_64.whl",hashes = {sha256 = "afb1c48b52bd4ee8f485f5a427bb4e82380590255a26b8e9e3fe099e0779287f"}}, + {name = "torchcodec-0.7.0-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/1f/80/04f23dff2c7ac406d2d6b24a52be7654a946d2fdfe158b19341a524dae20/torchcodec-0.7.0-cp311-cp311-win_amd64.whl",hashes = {sha256 = "a68765cd29159da3cf36eb5716481c617ad9d168fe06418bcde2a9360cc7eb5e"}}, + {name = "torchcodec-0.7.0-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/d8/23/ca6bd1bc5e22786596e25d1dd62a6a4e733802940b54726a54fcf5a8795b/torchcodec-0.7.0-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "6ae0b7acbc0c1a755ae817a8843715131f24be7807ddc46092d8b49a0fc970eb"}}, + {name = "torchcodec-0.7.0-cp310-cp310-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/0d/81/cff42793544b7d3e2ff9a4912542c6d1c7a617aabe8404f8fd3d52453f20/torchcodec-0.7.0-cp310-cp310-manylinux_2_28_x86_64.whl",hashes = {sha256 = "a0071096724e8ded6a171457ce4680f646499b4a4d285cdb46e130983f965ce4"}}, + {name = "torchcodec-0.7.0-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/ad/b9/7f03bf7d42e0f7ab5598d400cb1133d3f227b52aad15d88b2ab9c97fe1ff/torchcodec-0.7.0-cp310-cp310-win_amd64.whl",hashes = {sha256 = "737da9212594bf2f205582512a7a4f56d39591b357bf5a30e72e858cfcedc2ac"}}, +] +marker = "\"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" + +[packages.tool.pdm] +dependencies = [] + [[packages]] name = "tox" version = "4.16.0" @@ -824,33 +1031,6 @@ marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in ex [packages.tool.pdm] dependencies = [] -[[packages]] -name = "datasets" -version = "4.1.1" -requires-python = ">=3.9.0" -sdist = {name = "datasets-4.1.1.tar.gz", url = "https://files.pythonhosted.org/packages/91/a4/73f8e6ef52c535e1d20d5b2ca83bfe6de399d8b8b8a61ccc8d63d60735aa/datasets-4.1.1.tar.gz", hashes = {sha256 = "7d8d5ba8b12861d2c44bfff9c83484ebfafff1ff553371e5901a8d3aab5450e2"}} -wheels = [ - {name = "datasets-4.1.1-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/f4/c8/09012ac195a0aab58755800d2efdc0e7d5905053509f12cb5d136c911cda/datasets-4.1.1-py3-none-any.whl",hashes = {sha256 = "62e4f6899a36be9ec74a7e759a6951253cc85b3fcfa0a759b0efa8353b149dac"}}, -] -marker = "\"default\" in dependency_groups" - -[packages.tool.pdm] -dependencies = [ - "filelock", - "numpy>=1.17", - "pyarrow>=21.0.0", - "dill<0.4.1,>=0.3.0", - "pandas", - "requests>=2.32.2", - "tqdm>=4.66.3", - "xxhash", - "multiprocess<0.70.17", - "fsspec[http]<=2025.9.0,>=2023.1.0", - "huggingface-hub>=0.24.0", - "packaging", - "pyyaml>=5.1", -] - [[packages]] name = "eval-type-backport" version = "0.2.2" @@ -981,6 +1161,20 @@ wheels = [ {name = "msgspec-0.19.0-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/19/2b/4137bc2ed45660444842d042be2cf5b18aa06efd2cda107cff18253b9653/msgspec-0.19.0-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "757b501fa57e24896cf40a831442b19a864f56d253679f34f260dcb002524a6c"}}, {name = "msgspec-0.19.0-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/9d/e6/8ad51bdc806aac1dc501e8fe43f759f9ed7284043d722b53323ea421c360/msgspec-0.19.0-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "5f0f65f29b45e2816d8bded36e6b837a4bf5fb60ec4bc3c625fa2c6da4124537"}}, {name = "msgspec-0.19.0-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/b1/ef/27dd35a7049c9a4f4211c6cd6a8c9db0a50647546f003a5867827ec45391/msgspec-0.19.0-cp312-cp312-win_amd64.whl",hashes = {sha256 = "067f0de1c33cfa0b6a8206562efdf6be5985b988b53dd244a8e06f993f27c8c0"}}, + {name = "msgspec-0.19.0-cp311-cp311-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/24/d4/2ec2567ac30dab072cce3e91fb17803c52f0a37aab6b0c24375d2b20a581/msgspec-0.19.0-cp311-cp311-macosx_10_9_x86_64.whl",hashes = {sha256 = "aa77046904db764b0462036bc63ef71f02b75b8f72e9c9dd4c447d6da1ed8f8e"}}, + {name = "msgspec-0.19.0-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/2b/c0/18226e4328897f4f19875cb62bb9259fe47e901eade9d9376ab5f251a929/msgspec-0.19.0-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "047cfa8675eb3bad68722cfe95c60e7afabf84d1bd8938979dd2b92e9e4a9551"}}, + {name = "msgspec-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/81/25/3a4b24d468203d8af90d1d351b77ea3cffb96b29492855cf83078f16bfe4/msgspec-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "e78f46ff39a427e10b4a61614a2777ad69559cc8d603a7c05681f5a595ea98f7"}}, + {name = "msgspec-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/85/2e/db7e189b57901955239f7689b5dcd6ae9458637a9c66747326726c650523/msgspec-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "6c7adf191e4bd3be0e9231c3b6dc20cf1199ada2af523885efc2ed218eafd011"}}, + {name = "msgspec-0.19.0-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/03/97/7c8895c9074a97052d7e4a1cc1230b7b6e2ca2486714eb12c3f08bb9d284/msgspec-0.19.0-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "f04cad4385e20be7c7176bb8ae3dca54a08e9756cfc97bcdb4f18560c3042063"}}, + {name = "msgspec-0.19.0-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/61/61/e892997bcaa289559b4d5869f066a8021b79f4bf8e955f831b095f47a4cd/msgspec-0.19.0-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "45c8fb410670b3b7eb884d44a75589377c341ec1392b778311acdbfa55187716"}}, + {name = "msgspec-0.19.0-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/ce/3d/71b2dffd3a1c743ffe13296ff701ee503feaebc3f04d0e75613b6563c374/msgspec-0.19.0-cp311-cp311-win_amd64.whl",hashes = {sha256 = "70eaef4934b87193a27d802534dc466778ad8d536e296ae2f9334e182ac27b6c"}}, + {name = "msgspec-0.19.0-cp310-cp310-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/13/40/817282b42f58399762267b30deb8ac011d8db373f8da0c212c85fbe62b8f/msgspec-0.19.0-cp310-cp310-macosx_10_9_x86_64.whl",hashes = {sha256 = "d8dd848ee7ca7c8153462557655570156c2be94e79acec3561cf379581343259"}}, + {name = "msgspec-0.19.0-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/92/99/bd7ed738c00f223a8119928661167a89124140792af18af513e6519b0d54/msgspec-0.19.0-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "0553bbc77662e5708fe66aa75e7bd3e4b0f209709c48b299afd791d711a93c36"}}, + {name = "msgspec-0.19.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/e5/27/322badde18eb234e36d4a14122b89edd4e2973cdbc3da61ca7edf40a1ccd/msgspec-0.19.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "fe2c4bf29bf4e89790b3117470dea2c20b59932772483082c468b990d45fb947"}}, + {name = "msgspec-0.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/c6/65/080509c5774a1592b2779d902a70b5fe008532759927e011f068145a16cb/msgspec-0.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "00e87ecfa9795ee5214861eab8326b0e75475c2e68a384002aa135ea2a27d909"}}, + {name = "msgspec-0.19.0-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/6f/2e/1c23c6b4ca6f4285c30a39def1054e2bee281389e4b681b5e3711bd5a8c9/msgspec-0.19.0-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "3c4ec642689da44618f68c90855a10edbc6ac3ff7c1d94395446c65a776e712a"}}, + {name = "msgspec-0.19.0-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/83/fe/95f9654518879f3359d1e76bc41189113aa9102452170ab7c9a9a4ee52f6/msgspec-0.19.0-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "2719647625320b60e2d8af06b35f5b12d4f4d281db30a15a1df22adb2295f633"}}, + {name = "msgspec-0.19.0-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/79/f6/71ca7e87a1fb34dfe5efea8156c9ef59dd55613aeda2ca562f122cd22012/msgspec-0.19.0-cp310-cp310-win_amd64.whl",hashes = {sha256 = "695b832d0091edd86eeb535cd39e45f3919f48d997685f7ac31acb15e0a2ed90"}}, ] marker = "\"all\" in extras or \"dev\" in extras or \"perf\" in extras or \"recommended\" in extras" @@ -1034,118 +1228,40 @@ wheels = [ {name = "orjson-3.11.3-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/38/70/b14dcfae7aff0e379b0119c8a812f8396678919c431efccc8e8a0263e4d9/orjson-3.11.3-cp312-cp312-win32.whl",hashes = {sha256 = "3782d2c60b8116772aea8d9b7905221437fdf53e7277282e8d8b07c220f96cca"}}, {name = "orjson-3.11.3-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/35/b8/9e3127d65de7fff243f7f3e53f59a531bf6bb295ebe5db024c2503cc0726/orjson-3.11.3-cp312-cp312-win_amd64.whl",hashes = {sha256 = "79b44319268af2eaa3e315b92298de9a0067ade6e6003ddaef72f8e0bedb94f1"}}, {name = "orjson-3.11.3-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/51/92/a946e737d4d8a7fd84a606aba96220043dcc7d6988b9e7551f7f6d5ba5ad/orjson-3.11.3-cp312-cp312-win_arm64.whl",hashes = {sha256 = "0e92a4e83341ef79d835ca21b8bd13e27c859e4e9e4d7b63defc6e58462a3710"}}, + {name = "orjson-3.11.3-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl",url = "https://files.pythonhosted.org/packages/cd/8b/360674cd817faef32e49276187922a946468579fcaf37afdfb6c07046e92/orjson-3.11.3-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl",hashes = {sha256 = "9d2ae0cc6aeb669633e0124531f342a17d8e97ea999e42f12a5ad4adaa304c5f"}}, + {name = "orjson-3.11.3-cp311-cp311-macosx_15_0_arm64.whl",url = "https://files.pythonhosted.org/packages/05/3d/5fa9ea4b34c1a13be7d9046ba98d06e6feb1d8853718992954ab59d16625/orjson-3.11.3-cp311-cp311-macosx_15_0_arm64.whl",hashes = {sha256 = "ba21dbb2493e9c653eaffdc38819b004b7b1b246fb77bfc93dc016fe664eac91"}}, + {name = "orjson-3.11.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/e5/5f/e18367823925e00b1feec867ff5f040055892fc474bf5f7875649ecfa586/orjson-3.11.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "00f1a271e56d511d1569937c0447d7dce5a99a33ea0dec76673706360a051904"}}, + {name = "orjson-3.11.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",url = "https://files.pythonhosted.org/packages/0f/bd/3c66b91c4564759cf9f473251ac1650e446c7ba92a7c0f9f56ed54f9f0e6/orjson-3.11.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",hashes = {sha256 = "b67e71e47caa6680d1b6f075a396d04fa6ca8ca09aafb428731da9b3ea32a5a6"}}, + {name = "orjson-3.11.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/82/b5/dc8dcd609db4766e2967a85f63296c59d4722b39503e5b0bf7fd340d387f/orjson-3.11.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "d7d012ebddffcce8c85734a6d9e5f08180cd3857c5f5a3ac70185b43775d043d"}}, + {name = "orjson-3.11.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/48/c2/d58ec5fd1270b2aa44c862171891adc2e1241bd7dab26c8f46eb97c6c6f1/orjson-3.11.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "dd759f75d6b8d1b62012b7f5ef9461d03c804f94d539a5515b454ba3a6588038"}}, + {name = "orjson-3.11.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/73/87/0ef7e22eb8dd1ef940bfe3b9e441db519e692d62ed1aae365406a16d23d0/orjson-3.11.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "6890ace0809627b0dff19cfad92d69d0fa3f089d3e359a2a532507bb6ba34efb"}}, + {name = "orjson-3.11.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/bb/6a/e5bf7b70883f374710ad74faf99bacfc4b5b5a7797c1d5e130350e0e28a3/orjson-3.11.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "f9d4a5e041ae435b815e568537755773d05dac031fee6a57b4ba70897a44d9d2"}}, + {name = "orjson-3.11.3-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/bd/0c/4577fd860b6386ffaa56440e792af01c7882b56d2766f55384b5b0e9d39b/orjson-3.11.3-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "2d68bf97a771836687107abfca089743885fb664b90138d8761cce61d5625d55"}}, + {name = "orjson-3.11.3-cp311-cp311-musllinux_1_2_armv7l.whl",url = "https://files.pythonhosted.org/packages/66/4b/83e92b2d67e86d1c33f2ea9411742a714a26de63641b082bdbf3d8e481af/orjson-3.11.3-cp311-cp311-musllinux_1_2_armv7l.whl",hashes = {sha256 = "bfc27516ec46f4520b18ef645864cee168d2a027dbf32c5537cb1f3e3c22dac1"}}, + {name = "orjson-3.11.3-cp311-cp311-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/6d/e5/9eea6a14e9b5ceb4a271a1fd2e1dec5f2f686755c0fab6673dc6ff3433f4/orjson-3.11.3-cp311-cp311-musllinux_1_2_i686.whl",hashes = {sha256 = "f66b001332a017d7945e177e282a40b6997056394e3ed7ddb41fb1813b83e824"}}, + {name = "orjson-3.11.3-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/45/78/8d4f5ad0c80ba9bf8ac4d0fc71f93a7d0dc0844989e645e2074af376c307/orjson-3.11.3-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "212e67806525d2561efbfe9e799633b17eb668b8964abed6b5319b2f1cfbae1f"}}, + {name = "orjson-3.11.3-cp311-cp311-win32.whl",url = "https://files.pythonhosted.org/packages/0b/5f/16386970370178d7a9b438517ea3d704efcf163d286422bae3b37b88dbb5/orjson-3.11.3-cp311-cp311-win32.whl",hashes = {sha256 = "6e8e0c3b85575a32f2ffa59de455f85ce002b8bdc0662d6b9c2ed6d80ab5d204"}}, + {name = "orjson-3.11.3-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/09/60/db16c6f7a41dd8ac9fb651f66701ff2aeb499ad9ebc15853a26c7c152448/orjson-3.11.3-cp311-cp311-win_amd64.whl",hashes = {sha256 = "6be2f1b5d3dc99a5ce5ce162fc741c22ba9f3443d3dd586e6a1211b7bc87bc7b"}}, + {name = "orjson-3.11.3-cp311-cp311-win_arm64.whl",url = "https://files.pythonhosted.org/packages/3e/2a/bb811ad336667041dea9b8565c7c9faf2f59b47eb5ab680315eea612ef2e/orjson-3.11.3-cp311-cp311-win_arm64.whl",hashes = {sha256 = "fafb1a99d740523d964b15c8db4eabbfc86ff29f84898262bf6e3e4c9e97e43e"}}, + {name = "orjson-3.11.3-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl",url = "https://files.pythonhosted.org/packages/9b/64/4a3cef001c6cd9c64256348d4c13a7b09b857e3e1cbb5185917df67d8ced/orjson-3.11.3-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl",hashes = {sha256 = "29cb1f1b008d936803e2da3d7cba726fc47232c45df531b29edf0b232dd737e7"}}, + {name = "orjson-3.11.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/10/ce/0c8c87f54f79d051485903dc46226c4d3220b691a151769156054df4562b/orjson-3.11.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "97dceed87ed9139884a55db8722428e27bd8452817fbf1869c58b49fecab1120"}}, + {name = "orjson-3.11.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",url = "https://files.pythonhosted.org/packages/ef/d0/249497e861f2d438f45b3ab7b7b361484237414945169aa285608f9f7019/orjson-3.11.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",hashes = {sha256 = "58533f9e8266cb0ac298e259ed7b4d42ed3fa0b78ce76860626164de49e0d467"}}, + {name = "orjson-3.11.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/e5/64/00485702f640a0fd56144042a1ea196469f4a3ae93681871564bf74fa996/orjson-3.11.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "0c212cfdd90512fe722fa9bd620de4d46cda691415be86b2e02243242ae81873"}}, + {name = "orjson-3.11.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/64/81/110d68dba3909171bf3f05619ad0cf187b430e64045ae4e0aa7ccfe25b15/orjson-3.11.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "5ff835b5d3e67d9207343effb03760c00335f8b5285bfceefd4dc967b0e48f6a"}}, + {name = "orjson-3.11.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/79/92/dba25c22b0ddfafa1e6516a780a00abac28d49f49e7202eb433a53c3e94e/orjson-3.11.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "f5aa4682912a450c2db89cbd92d356fef47e115dffba07992555542f344d301b"}}, + {name = "orjson-3.11.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/44/1d/ca2230fd55edbd87b58a43a19032d63a4b180389a97520cc62c535b726f9/orjson-3.11.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "d7d18dd34ea2e860553a579df02041845dee0af8985dff7f8661306f95504ddf"}}, + {name = "orjson-3.11.3-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/6e/b9/96bbc8ed3e47e52b487d504bd6861798977445fbc410da6e87e302dc632d/orjson-3.11.3-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "d8b11701bc43be92ea42bd454910437b355dfb63696c06fe953ffb40b5f763b4"}}, + {name = "orjson-3.11.3-cp310-cp310-musllinux_1_2_armv7l.whl",url = "https://files.pythonhosted.org/packages/c4/3c/418fbd93d94b0df71cddf96b7fe5894d64a5d890b453ac365120daec30f7/orjson-3.11.3-cp310-cp310-musllinux_1_2_armv7l.whl",hashes = {sha256 = "90368277087d4af32d38bd55f9da2ff466d25325bf6167c8f382d8ee40cb2bbc"}}, + {name = "orjson-3.11.3-cp310-cp310-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/5b/a9/2bfd58817d736c2f63608dec0c34857339d423eeed30099b126562822191/orjson-3.11.3-cp310-cp310-musllinux_1_2_i686.whl",hashes = {sha256 = "fd7ff459fb393358d3a155d25b275c60b07a2c83dcd7ea962b1923f5a1134569"}}, + {name = "orjson-3.11.3-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/33/ba/29023771f334096f564e48d82ed855a0ed3320389d6748a9c949e25be734/orjson-3.11.3-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "f8d902867b699bcd09c176a280b1acdab57f924489033e53d0afe79817da37e6"}}, + {name = "orjson-3.11.3-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/39/62/b5a1eca83f54cb3aa11a9645b8a22f08d97dbd13f27f83aae7c6666a0a05/orjson-3.11.3-cp310-cp310-win32.whl",hashes = {sha256 = "bb93562146120bb51e6b154962d3dadc678ed0fce96513fa6bc06599bb6f6edc"}}, + {name = "orjson-3.11.3-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/e3/c0/7ebfaa327d9a9ed982adc0d9420dbce9a3fec45b60ab32c6308f731333fa/orjson-3.11.3-cp310-cp310-win_amd64.whl",hashes = {sha256 = "976c6f1975032cc327161c65d4194c549f2589d88b105a5e3499429a54479770"}}, ] marker = "\"all\" in extras or \"dev\" in extras or \"perf\" in extras or \"recommended\" in extras" [packages.tool.pdm] dependencies = [] -[[packages]] -name = "pillow" -version = "11.3.0" -requires-python = ">=3.9" -sdist = {name = "pillow-11.3.0.tar.gz", url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hashes = {sha256 = "3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523"}} -wheels = [ - {name = "pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl",hashes = {sha256 = "d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12"}}, - {name = "pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a"}}, - {name = "pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632"}}, - {name = "pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673"}}, - {name = "pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027"}}, - {name = "pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77"}}, - {name = "pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl",hashes = {sha256 = "7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874"}}, - {name = "pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl",hashes = {sha256 = "98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a"}}, - {name = "pillow-11.3.0-cp314-cp314-win32.whl",url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl",hashes = {sha256 = "02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214"}}, - {name = "pillow-11.3.0-cp314-cp314-win_amd64.whl",url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl",hashes = {sha256 = "a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635"}}, - {name = "pillow-11.3.0-cp314-cp314-win_arm64.whl",url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl",hashes = {sha256 = "155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6"}}, - {name = "pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl",hashes = {sha256 = "59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae"}}, - {name = "pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl",hashes = {sha256 = "f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653"}}, - {name = "pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6"}}, - {name = "pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36"}}, - {name = "pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b"}}, - {name = "pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477"}}, - {name = "pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50"}}, - {name = "pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b"}}, - {name = "pillow-11.3.0-cp314-cp314t-win32.whl",url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl",hashes = {sha256 = "118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12"}}, - {name = "pillow-11.3.0-cp314-cp314t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl",hashes = {sha256 = "8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db"}}, - {name = "pillow-11.3.0-cp314-cp314t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl",hashes = {sha256 = "79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa"}}, - {name = "pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl",url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl",hashes = {sha256 = "1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd"}}, - {name = "pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl",url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl",hashes = {sha256 = "30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8"}}, - {name = "pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl",url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl",hashes = {sha256 = "7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f"}}, - {name = "pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl",hashes = {sha256 = "ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c"}}, - {name = "pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd"}}, - {name = "pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e"}}, - {name = "pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1"}}, - {name = "pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805"}}, - {name = "pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8"}}, - {name = "pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2"}}, - {name = "pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b"}}, - {name = "pillow-11.3.0-cp313-cp313-win32.whl",url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl",hashes = {sha256 = "a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3"}}, - {name = "pillow-11.3.0-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl",hashes = {sha256 = "0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51"}}, - {name = "pillow-11.3.0-cp313-cp313-win_arm64.whl",url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl",hashes = {sha256 = "1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580"}}, - {name = "pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl",hashes = {sha256 = "4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e"}}, - {name = "pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl",hashes = {sha256 = "5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d"}}, - {name = "pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced"}}, - {name = "pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c"}}, - {name = "pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8"}}, - {name = "pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59"}}, - {name = "pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe"}}, - {name = "pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c"}}, - {name = "pillow-11.3.0-cp313-cp313t-win32.whl",url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl",hashes = {sha256 = "2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788"}}, - {name = "pillow-11.3.0-cp313-cp313t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl",hashes = {sha256 = "857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31"}}, - {name = "pillow-11.3.0-cp313-cp313t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl",hashes = {sha256 = "8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e"}}, - {name = "pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl",hashes = {sha256 = "fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4"}}, - {name = "pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69"}}, - {name = "pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d"}}, - {name = "pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6"}}, - {name = "pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7"}}, - {name = "pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024"}}, - {name = "pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809"}}, - {name = "pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d"}}, - {name = "pillow-11.3.0-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl",hashes = {sha256 = "7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149"}}, - {name = "pillow-11.3.0-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl",hashes = {sha256 = "a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d"}}, - {name = "pillow-11.3.0-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl",hashes = {sha256 = "2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542"}}, - {name = "pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl",url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl",hashes = {sha256 = "1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722"}}, - {name = "pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288"}}, - {name = "pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d"}}, - {name = "pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494"}}, - {name = "pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58"}}, - {name = "pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f"}}, - {name = "pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e"}}, - {name = "pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94"}}, - {name = "pillow-11.3.0-cp311-cp311-win32.whl",url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl",hashes = {sha256 = "b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0"}}, - {name = "pillow-11.3.0-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl",hashes = {sha256 = "1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac"}}, - {name = "pillow-11.3.0-cp311-cp311-win_arm64.whl",url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl",hashes = {sha256 = "30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd"}}, - {name = "pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl",url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl",hashes = {sha256 = "7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6"}}, - {name = "pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl",hashes = {sha256 = "9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438"}}, - {name = "pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3"}}, - {name = "pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c"}}, - {name = "pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361"}}, - {name = "pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7"}}, - {name = "pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl",url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl",hashes = {sha256 = "c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8"}}, - {name = "pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl",url = "https://files.pythonhosted.org/packages/4c/5d/45a3553a253ac8763f3561371432a90bdbe6000fbdcf1397ffe502aa206c/pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl",hashes = {sha256 = "1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860"}}, - {name = "pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/7c/c8/67c12ab069ef586a25a4a79ced553586748fad100c77c0ce59bb4983ac98/pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad"}}, - {name = "pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/2f/bd/6741ebd56263390b382ae4c5de02979af7f8bd9807346d068700dd6d5cf9/pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0"}}, - {name = "pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/ca/0b/c412a9e27e1e6a829e6ab6c2dca52dd563efbedf4c9c6aa453d9a9b77359/pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b"}}, - {name = "pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/59/9d/9b7076aaf30f5dd17e5e5589b2d2f5a5d7e30ff67a171eb686e4eecc2adf/pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50"}}, - {name = "pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/f0/16/1a6bf01fb622fb9cf5c91683823f073f053005c849b1f52ed613afcf8dae/pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae"}}, - {name = "pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/7b/e6/6ff7077077eb47fde78739e7d570bdcd7c10495666b6afcd23ab56b19a43/pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9"}}, - {name = "pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/c3/3a/b13f36832ea6d279a697231658199e0a03cd87ef12048016bdcc84131601/pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e"}}, - {name = "pillow-11.3.0-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/6c/e4/61b2e1a7528740efbc70b3d581f33937e38e98ef3d50b05007267a55bcb2/pillow-11.3.0-cp310-cp310-win32.whl",hashes = {sha256 = "89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6"}}, - {name = "pillow-11.3.0-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/a9/d3/60c781c83a785d6afbd6a326ed4d759d141de43aa7365725cbcd65ce5e54/pillow-11.3.0-cp310-cp310-win_amd64.whl",hashes = {sha256 = "19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f"}}, - {name = "pillow-11.3.0-cp310-cp310-win_arm64.whl",url = "https://files.pythonhosted.org/packages/9f/28/4f4a0203165eefb3763939c6789ba31013a2e90adffb456610f30f613850/pillow-11.3.0-cp310-cp310-win_arm64.whl",hashes = {sha256 = "819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f"}}, - {name = "pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl",url = "https://files.pythonhosted.org/packages/6f/8b/209bd6b62ce8367f47e68a218bffac88888fdf2c9fcf1ecadc6c3ec1ebc7/pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl",hashes = {sha256 = "3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967"}}, - {name = "pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/2e/e6/231a0b76070c2cfd9e260a7a5b504fb72da0a95279410fa7afd99d9751d6/pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl",hashes = {sha256 = "b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe"}}, - {name = "pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/13/f4/10cf94fda33cb12765f2397fc285fa6d8eb9c29de7f3185165b702fc7386/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c"}}, - {name = "pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/72/c9/583821097dc691880c92892e8e2d41fe0a5a3d6021f4963371d2f6d57250/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25"}}, - {name = "pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/3b/8e/5c9d410f9217b12320efc7c413e72693f48468979a013ad17fd690397b9a/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27"}}, - {name = "pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/62/bb/78347dbe13219991877ffb3a91bf09da8317fbfcd4b5f9140aeae020ad71/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a"}}, - {name = "pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl",url = "https://files.pythonhosted.org/packages/d9/28/1000353d5e61498aaeaaf7f1e4b49ddb05f2c6575f9d4f9f914a3538b6e1/pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl",hashes = {sha256 = "8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f"}}, -] -marker = "\"default\" in dependency_groups" - -[packages.tool.pdm] -dependencies = [] - [[packages]] name = "protobuf" version = "6.32.1" @@ -1205,54 +1321,13 @@ dependencies = [ "setuptools>=70.1.0", ] -[[packages]] -name = "torch" -version = "2.9.0+cpu" -requires-python = ">=3.10" -wheels = [ - {name = "torch-2.9.0+cpu-cp314-cp314-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp314-cp314-manylinux_2_28_aarch64.whl",hashes = {sha256 = "44aadb735774d4a99525d2ec29126b23016c44a07b02ce6c237dfa61a223dd52"}}, - {name = "torch-2.9.0+cpu-cp314-cp314-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp314-cp314-manylinux_2_28_x86_64.whl",hashes = {sha256 = "b355e07b7f0c369cb031adfcbff5c37a609abcea091b918a39886412afd2e07d"}}, - {name = "torch-2.9.0+cpu-cp314-cp314-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp314-cp314-win_amd64.whl",hashes = {sha256 = "c2698999361d73c2d25d7cc8a787130188d49b183abb18b554228daa102e1594"}}, - {name = "torch-2.9.0+cpu-cp314-cp314t-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp314-cp314t-manylinux_2_28_aarch64.whl",hashes = {sha256 = "fa0d1373d04b30ff8f12d542135d292f1a1ddb7c0d852a3d487a320360e5dab9"}}, - {name = "torch-2.9.0+cpu-cp314-cp314t-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp314-cp314t-manylinux_2_28_x86_64.whl",hashes = {sha256 = "2f49bb57a5fe0dc7f8e73ea9e5d36ebda2ea25b8a714a788f0fc2fc47d20a830"}}, - {name = "torch-2.9.0+cpu-cp314-cp314t-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp314-cp314t-win_amd64.whl",hashes = {sha256 = "3a60d1ecf27a9cce839b3aa665b26f0af1b1007b9c9f1e7f597f6b7bdf107617"}}, - {name = "torch-2.9.0-cp314-cp314-macosx_11_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "d8e2ab7f86010330bdcc39c8b2c795590cc75e37df4823cdaee2c98d6e3ff4a3"}}, - {name = "torch-2.9.0-cp314-cp314t-macosx_11_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0-cp314-cp314t-macosx_11_0_arm64.whl",hashes = {sha256 = "a3e859039c985d8e3ea60d7a54ca7e97ea2ae15e31beced4f3260128a161bb01"}}, - {name = "torch-2.9.0+cpu-cp313-cp313-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl",hashes = {sha256 = "be4438d8dad7f0d5a5e54f0feef8a893446894ec87f102bb1d82dcc4518542e4"}}, - {name = "torch-2.9.0+cpu-cp313-cp313-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl",hashes = {sha256 = "6c9b217584400963d5b4daddb3711ec7a3778eab211e18654fba076cce3b8682"}}, - {name = "torch-2.9.0+cpu-cp313-cp313-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp313-cp313-win_amd64.whl",hashes = {sha256 = "728372e3f58c5826445f677746e5311c1935c1a7c59599f73a49ded850e038e8"}}, - {name = "torch-2.9.0+cpu-cp313-cp313-win_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp313-cp313-win_arm64.whl",hashes = {sha256 = "95e56c26f919fbb98f16e7a0b87af494b893f9da9a65a020f17a01c13e520a81"}}, - {name = "torch-2.9.0+cpu-cp313-cp313t-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl",hashes = {sha256 = "6c777160288b08555820781ae0f3a2c67a59bd24b065e88ca1ec20e2f9dc8ac7"}}, - {name = "torch-2.9.0+cpu-cp313-cp313t-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl",hashes = {sha256 = "528fd338311f31c9fb18038cafd00e6eae0bf5ad5577521701acb62510753d18"}}, - {name = "torch-2.9.0+cpu-cp313-cp313t-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp313-cp313t-win_amd64.whl",hashes = {sha256 = "d572863990e7d2762b547735ef589f6350d9eb4e441d38753a1c33636698cf4c"}}, - {name = "torch-2.9.0-cp313-cp313t-macosx_11_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0-cp313-cp313t-macosx_11_0_arm64.whl",hashes = {sha256 = "259548471194ab63d7ea273873053a6e3cc23530c1510f01e9d7ad259187bbd0"}}, - {name = "torch-2.9.0-cp313-none-macosx_11_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0-cp313-none-macosx_11_0_arm64.whl",hashes = {sha256 = "e24836d968b54ef4dfb05594001a61958711ac9224026291e4e3f92f83a6fd7f"}}, - {name = "torch-2.9.0+cpu-cp312-cp312-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl",hashes = {sha256 = "3a651434ae1248b0568c12b5f9e3acc8942eb28378d9d04a79302938b68c6f24"}}, - {name = "torch-2.9.0+cpu-cp312-cp312-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl",hashes = {sha256 = "28f6eb31b08180a5c5e98d5bc14eef6909c9f5a1dbff9632c3e02a8773449349"}}, - {name = "torch-2.9.0+cpu-cp312-cp312-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp312-cp312-win_amd64.whl",hashes = {sha256 = "e438061b87ec7dd6018fca9f975219889aa0a3f6cdc3ea10dd0ae2bc7f1c47ce"}}, - {name = "torch-2.9.0+cpu-cp312-cp312-win_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp312-cp312-win_arm64.whl",hashes = {sha256 = "eb13ff1c34e338d722e76a4fd83b8d282782505bd1b99af4b3c32da66eba6eb4"}}, - {name = "torch-2.9.0-cp312-none-macosx_11_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0-cp312-none-macosx_11_0_arm64.whl",hashes = {sha256 = "4de0ed8cbc457a506dbca40376e206a29efee10756a00f1f3404bf67ad737d04"}}, -] -marker = "\"default\" in dependency_groups" - -[packages.tool.pdm] -dependencies = [ - "filelock", - "typing-extensions>=4.10.0", - "setuptools; python_version >= \"3.12\"", - "sympy>=1.13.3", - "networkx>=2.5.1", - "jinja2", - "fsspec>=0.8.5", -] - [[packages]] name = "transformers" -version = "4.57.0" +version = "4.57.1" requires-python = ">=3.9.0" -sdist = {name = "transformers-4.57.0.tar.gz", url = "https://files.pythonhosted.org/packages/f3/5c/a22c39dac2687f3fe2a6b97e2c1ae516e91cd4d3976a7a2b7c24ff2fae48/transformers-4.57.0.tar.gz", hashes = {sha256 = "d045753f3d93f9216e693cdb168698dfd2e9d3aad1bb72579a5d60ebf1545a8b"}} +sdist = {name = "transformers-4.57.1.tar.gz", url = "https://files.pythonhosted.org/packages/d6/68/a39307bcc4116a30b2106f2e689130a48de8bd8a1e635b5e1030e46fcd9e/transformers-4.57.1.tar.gz", hashes = {sha256 = "f06c837959196c75039809636cd964b959f6604b75b8eeec6fdfc0440b89cc55"}} wheels = [ - {name = "transformers-4.57.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/e5/2b/4d2708ac1ff5cd708b6548f4c5812d0ae40d1c28591c4c1c762b6dbdef2d/transformers-4.57.0-py3-none-any.whl",hashes = {sha256 = "9d7c6d098c026e40d897e017ed1f481ab803cbac041021dbc6ae6100e4949b55"}}, + {name = "transformers-4.57.1-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/71/d3/c16c3b3cf7655a67db1144da94b021c200ac1303f82428f2beef6c2e72bb/transformers-4.57.1-py3-none-any.whl",hashes = {sha256 = "b10d05da8fa67dc41644dbbf9bc45a44cb86ae33da6f9295f5fbf5b7890bd267"}}, ] marker = "\"default\" in dependency_groups" @@ -1291,7 +1366,7 @@ sdist = {name = "httpx-0.28.1.tar.gz", url = "https://files.pythonhosted.org/pac wheels = [ {name = "httpx-0.28.1-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl",hashes = {sha256 = "d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" [packages.tool.pdm] dependencies = [ @@ -1309,7 +1384,7 @@ sdist = {name = "httpcore-1.0.9.tar.gz", url = "https://files.pythonhosted.org/p wheels = [ {name = "httpcore-1.0.9-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl",hashes = {sha256 = "2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" [packages.tool.pdm] dependencies = [ @@ -1319,101 +1394,105 @@ dependencies = [ [[packages]] name = "pydantic-core" -version = "2.41.1" +version = "2.41.4" requires-python = ">=3.9" -sdist = {name = "pydantic_core-2.41.1.tar.gz", url = "https://files.pythonhosted.org/packages/7d/14/12b4a0d2b0b10d8e1d9a24ad94e7bbb43335eaf29c0c4e57860e8a30734a/pydantic_core-2.41.1.tar.gz", hashes = {sha256 = "1ad375859a6d8c356b7704ec0f547a58e82ee80bb41baa811ad710e124bc8f2f"}} -wheels = [ - {name = "pydantic_core-2.41.1-cp314-cp314-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/41/12/cec246429ddfa2778d2d6301eca5362194dc8749ecb19e621f2f65b5090f/pydantic_core-2.41.1-cp314-cp314-macosx_10_12_x86_64.whl",hashes = {sha256 = "05226894a26f6f27e1deb735d7308f74ef5fa3a6de3e0135bb66cdcaee88f64b"}}, - {name = "pydantic_core-2.41.1-cp314-cp314-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/20/39/baba47f8d8b87081302498e610aefc37142ce6a1cc98b2ab6b931a162562/pydantic_core-2.41.1-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "85ff7911c6c3e2fd8d3779c50925f6406d770ea58ea6dde9c230d35b52b16b4a"}}, - {name = "pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/50/32/9a3d87cae2c75a5178334b10358d631bd094b916a00a5993382222dbfd92/pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "47f1f642a205687d59b52dc1a9a607f45e588f5a2e9eeae05edd80c7a8c47674"}}, - {name = "pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",url = "https://files.pythonhosted.org/packages/27/42/a96c9d793a04cf2a9773bff98003bb154087b94f5530a2ce6063ecfec583/pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",hashes = {sha256 = "df11c24e138876ace5ec6043e5cae925e34cf38af1a1b3d63589e8f7b5f5cdc4"}}, - {name = "pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/3e/8d/028c4b7d157a005b1f52c086e2d4b0067886b213c86220c1153398dbdf8f/pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "7f0bf7f5c8f7bf345c527e8a0d72d6b26eda99c1227b0c34e7e59e181260de31"}}, - {name = "pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/08/f7/ee64cda8fcc9ca3f4716e6357144f9ee71166775df582a1b6b738bf6da57/pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "82b887a711d341c2c47352375d73b029418f55b20bd7815446d175a70effa706"}}, - {name = "pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/13/c0/e8ec05f0f5ee7a3656973ad9cd3bc73204af99f6512c1a4562f6fb4b3f7d/pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "b5f1d5d6bbba484bdf220c72d8ecd0be460f4bd4c5e534a541bb2cd57589fb8b"}}, - {name = "pydantic_core-2.41.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl",url = "https://files.pythonhosted.org/packages/0a/25/d77a73ff24e2e4fcea64472f5e39b0402d836da9b08b5361a734d0153023/pydantic_core-2.41.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl",hashes = {sha256 = "2bf1917385ebe0f968dc5c6ab1375886d56992b93ddfe6bf52bff575d03662be"}}, - {name = "pydantic_core-2.41.1-cp314-cp314-musllinux_1_1_aarch64.whl",url = "https://files.pythonhosted.org/packages/66/45/4a4ebaaae12a740552278d06fe71418c0f2869537a369a89c0e6723b341d/pydantic_core-2.41.1-cp314-cp314-musllinux_1_1_aarch64.whl",hashes = {sha256 = "4f94f3ab188f44b9a73f7295663f3ecb8f2e2dd03a69c8f2ead50d37785ecb04"}}, - {name = "pydantic_core-2.41.1-cp314-cp314-musllinux_1_1_armv7l.whl",url = "https://files.pythonhosted.org/packages/da/6d/b727ce1022f143194a36593243ff244ed5a1eb3c9122296bf7e716aa37ba/pydantic_core-2.41.1-cp314-cp314-musllinux_1_1_armv7l.whl",hashes = {sha256 = "3925446673641d37c30bd84a9d597e49f72eacee8b43322c8999fa17d5ae5bc4"}}, - {name = "pydantic_core-2.41.1-cp314-cp314-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/6f/8c/02df9d8506c427787059f87c6c7253435c6895e12472a652d9616ee0fc95/pydantic_core-2.41.1-cp314-cp314-musllinux_1_1_x86_64.whl",hashes = {sha256 = "49bd51cc27adb980c7b97357ae036ce9b3c4d0bb406e84fbe16fb2d368b602a8"}}, - {name = "pydantic_core-2.41.1-cp314-cp314-win32.whl",url = "https://files.pythonhosted.org/packages/98/67/0cf429a7d6802536941f430e6e3243f6d4b68f41eeea4b242372f1901794/pydantic_core-2.41.1-cp314-cp314-win32.whl",hashes = {sha256 = "a31ca0cd0e4d12ea0df0077df2d487fc3eb9d7f96bbb13c3c5b88dcc21d05159"}}, - {name = "pydantic_core-2.41.1-cp314-cp314-win_amd64.whl",url = "https://files.pythonhosted.org/packages/38/60/742fef93de5d085022d2302a6317a2b34dbfe15258e9396a535c8a100ae7/pydantic_core-2.41.1-cp314-cp314-win_amd64.whl",hashes = {sha256 = "1b5c4374a152e10a22175d7790e644fbd8ff58418890e07e2073ff9d4414efae"}}, - {name = "pydantic_core-2.41.1-cp314-cp314-win_arm64.whl",url = "https://files.pythonhosted.org/packages/31/38/cdd8ccb8555ef7720bd7715899bd6cfbe3c29198332710e1b61b8f5dd8b8/pydantic_core-2.41.1-cp314-cp314-win_arm64.whl",hashes = {sha256 = "4fee76d757639b493eb600fba668f1e17475af34c17dd61db7a47e824d464ca9"}}, - {name = "pydantic_core-2.41.1-cp314-cp314t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/e7/7e/8ac10ccb047dc0221aa2530ec3c7c05ab4656d4d4bd984ee85da7f3d5525/pydantic_core-2.41.1-cp314-cp314t-macosx_11_0_arm64.whl",hashes = {sha256 = "f9b9c968cfe5cd576fdd7361f47f27adeb120517e637d1b189eea1c3ece573f4"}}, - {name = "pydantic_core-2.41.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/c3/e4/7d9791efeb9c7d97e7268f8d20e0da24d03438a7fa7163ab58f1073ba968/pydantic_core-2.41.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "f1ebc7ab67b856384aba09ed74e3e977dded40e693de18a4f197c67d0d4e6d8e"}}, - {name = "pydantic_core-2.41.1-cp314-cp314t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/2d/c3/3f6e6b2342ac11ac8cd5cb56e24c7b14afa27c010e82a765ffa5f771884a/pydantic_core-2.41.1-cp314-cp314t-win_amd64.whl",hashes = {sha256 = "8ae0dc57b62a762985bc7fbf636be3412394acc0ddb4ade07fe104230f1b9762"}}, - {name = "pydantic_core-2.41.1-cp313-cp313-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/27/8a/6d54198536a90a37807d31a156642aae7a8e1263ed9fe6fc6245defe9332/pydantic_core-2.41.1-cp313-cp313-macosx_10_12_x86_64.whl",hashes = {sha256 = "70e790fce5f05204ef4403159857bfcd587779da78627b0babb3654f75361ebf"}}, - {name = "pydantic_core-2.41.1-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/4f/2e/4784fd7b22ac9c8439db25bf98ffed6853d01e7e560a346e8af821776ccc/pydantic_core-2.41.1-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "9cebf1ca35f10930612d60bd0f78adfacee824c30a880e3534ba02c207cceceb"}}, - {name = "pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/f3/92/31eb0748059ba5bd0aa708fb4bab9fcb211461ddcf9e90702a6542f22d0d/pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "170406a37a5bc82c22c3274616bf6f17cc7df9c4a0a0a50449e559cb755db669"}}, - {name = "pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",url = "https://files.pythonhosted.org/packages/ab/91/946527792275b5c4c7dde4cfa3e81241bf6900e9fee74fb1ba43e0c0f1ab/pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",hashes = {sha256 = "12d4257fc9187a0ccd41b8b327d6a4e57281ab75e11dda66a9148ef2e1fb712f"}}, - {name = "pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/31/5d/a35c5d7b414e5c0749f1d9f0d159ee2ef4bab313f499692896b918014ee3/pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "a75a33b4db105dd1c8d57839e17ee12db8d5ad18209e792fa325dbb4baeb00f4"}}, - {name = "pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/21/4d/8713737c689afa57ecfefe38db78259d4484c97aa494979e6a9d19662584/pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "08a589f850803a74e0fcb16a72081cafb0d72a3cdda500106942b07e76b7bf62"}}, - {name = "pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/f6/ec/929f9a3a5ed5cda767081494bacd32f783e707a690ce6eeb5e0730ec4986/pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "7a97939d6ea44763c456bd8a617ceada2c9b96bb5b8ab3dfa0d0827df7619014"}}, - {name = "pydantic_core-2.41.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl",url = "https://files.pythonhosted.org/packages/26/55/a33f459d4f9cc8786d9db42795dbecc84fa724b290d7d71ddc3d7155d46a/pydantic_core-2.41.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl",hashes = {sha256 = "d2ae423c65c556f09569524b80ffd11babff61f33055ef9773d7c9fabc11ed8d"}}, - {name = "pydantic_core-2.41.1-cp313-cp313-musllinux_1_1_aarch64.whl",url = "https://files.pythonhosted.org/packages/77/af/d5c6959f8b089f2185760a2779079e3c2c411bfc70ea6111f58367851629/pydantic_core-2.41.1-cp313-cp313-musllinux_1_1_aarch64.whl",hashes = {sha256 = "4dc703015fbf8764d6a8001c327a87f1823b7328d40b47ce6000c65918ad2b4f"}}, - {name = "pydantic_core-2.41.1-cp313-cp313-musllinux_1_1_armv7l.whl",url = "https://files.pythonhosted.org/packages/58/e5/2c19bd2a14bffe7fabcf00efbfbd3ac430aaec5271b504a938ff019ac7be/pydantic_core-2.41.1-cp313-cp313-musllinux_1_1_armv7l.whl",hashes = {sha256 = "968e4ffdfd35698a5fe659e5e44c508b53664870a8e61c8f9d24d3d145d30257"}}, - {name = "pydantic_core-2.41.1-cp313-cp313-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/93/ef/e0870ccda798c54e6b100aff3c4d49df5458fd64217e860cb9c3b0a403f4/pydantic_core-2.41.1-cp313-cp313-musllinux_1_1_x86_64.whl",hashes = {sha256 = "fff2b76c8e172d34771cd4d4f0ade08072385310f214f823b5a6ad4006890d32"}}, - {name = "pydantic_core-2.41.1-cp313-cp313-win32.whl",url = "https://files.pythonhosted.org/packages/b1/4b/c3b991d95f5deb24d0bd52e47bcf716098fa1afe0ce2d4bd3125b38566ba/pydantic_core-2.41.1-cp313-cp313-win32.whl",hashes = {sha256 = "a38a5263185407ceb599f2f035faf4589d57e73c7146d64f10577f6449e8171d"}}, - {name = "pydantic_core-2.41.1-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/a7/ce/5c316fd62e01f8d6be1b7ee6b54273214e871772997dc2c95e204997a055/pydantic_core-2.41.1-cp313-cp313-win_amd64.whl",hashes = {sha256 = "b42ae7fd6760782c975897e1fdc810f483b021b32245b0105d40f6e7a3803e4b"}}, - {name = "pydantic_core-2.41.1-cp313-cp313-win_arm64.whl",url = "https://files.pythonhosted.org/packages/29/41/902640cfd6a6523194123e2c3373c60f19006447f2fb06f76de4e8466c5b/pydantic_core-2.41.1-cp313-cp313-win_arm64.whl",hashes = {sha256 = "ad4111acc63b7384e205c27a2f15e23ac0ee21a9d77ad6f2e9cb516ec90965fb"}}, - {name = "pydantic_core-2.41.1-cp313-cp313t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/04/04/28b040e88c1b89d851278478842f0bdf39c7a05da9e850333c6c8cbe7dfa/pydantic_core-2.41.1-cp313-cp313t-macosx_11_0_arm64.whl",hashes = {sha256 = "440d0df7415b50084a4ba9d870480c16c5f67c0d1d4d5119e3f70925533a0edc"}}, - {name = "pydantic_core-2.41.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/d6/58/b41dd3087505220bb58bc81be8c3e8cbc037f5710cd3c838f44f90bdd704/pydantic_core-2.41.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "71eaa38d342099405dae6484216dcf1e8e4b0bebd9b44a4e08c9b43db6a2ab67"}}, - {name = "pydantic_core-2.41.1-cp313-cp313t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/d7/b8/760f23754e40bf6c65b94a69b22c394c24058a0ef7e2aa471d2e39219c1a/pydantic_core-2.41.1-cp313-cp313t-win_amd64.whl",hashes = {sha256 = "555ecf7e50f1161d3f693bc49f23c82cf6cdeafc71fa37a06120772a09a38795"}}, - {name = "pydantic_core-2.41.1-cp312-cp312-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/ee/bc/5f520319ee1c9e25010412fac4154a72e0a40d0a19eb00281b1f200c0947/pydantic_core-2.41.1-cp312-cp312-macosx_10_12_x86_64.whl",hashes = {sha256 = "db2f82c0ccbce8f021ad304ce35cbe02aa2f95f215cac388eed542b03b4d5eb4"}}, - {name = "pydantic_core-2.41.1-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/31/14/010cd64c5c3814fb6064786837ec12604be0dd46df3327cf8474e38abbbd/pydantic_core-2.41.1-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "47694a31c710ced9205d5f1e7e8af3ca57cbb8a503d98cb9e33e27c97a501601"}}, - {name = "pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/8e/2e/23fc2a8a93efad52df302fdade0a60f471ecc0c7aac889801ac24b4c07d6/pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "93e9decce94daf47baf9e9d392f5f2557e783085f7c5e522011545d9d6858e00"}}, - {name = "pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",url = "https://files.pythonhosted.org/packages/b9/b6/6db08b2725b2432b9390844852e11d320281e5cea8a859c52c68001975fa/pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",hashes = {sha256 = "ab0adafdf2b89c8b84f847780a119437a0931eca469f7b44d356f2b426dd9741"}}, - {name = "pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/61/d9/4de44600f2d4514b44f3f3aeeda2e14931214b6b5bf52479339e801ce748/pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "5da98cc81873f39fd56882e1569c4677940fbc12bce6213fad1ead784192d7c8"}}, - {name = "pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/7a/ae/dbe51187a7f35fc21b283c5250571a94e36373eb557c1cba9f29a9806dcf/pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "209910e88afb01fd0fd403947b809ba8dba0e08a095e1f703294fda0a8fdca51"}}, - {name = "pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/b5/a7/975585147457c2e9fb951c7c8dab56deeb6aa313f3aa72c2fc0df3f74a49/pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "365109d1165d78d98e33c5bfd815a9b5d7d070f578caefaabcc5771825b4ecb5"}}, - {name = "pydantic_core-2.41.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl",url = "https://files.pythonhosted.org/packages/62/37/ea94d1d0c01dec1b7d236c7cec9103baab0021f42500975de3d42522104b/pydantic_core-2.41.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl",hashes = {sha256 = "706abf21e60a2857acdb09502bc853ee5bce732955e7b723b10311114f033115"}}, - {name = "pydantic_core-2.41.1-cp312-cp312-musllinux_1_1_aarch64.whl",url = "https://files.pythonhosted.org/packages/d3/fe/694cf9fdd3a777a618c3afd210dba7b414cb8a72b1bd29b199c2e5765fee/pydantic_core-2.41.1-cp312-cp312-musllinux_1_1_aarch64.whl",hashes = {sha256 = "bf0bd5417acf7f6a7ec3b53f2109f587be176cb35f9cf016da87e6017437a72d"}}, - {name = "pydantic_core-2.41.1-cp312-cp312-musllinux_1_1_armv7l.whl",url = "https://files.pythonhosted.org/packages/0f/ae/174aeabd89916fbd2988cc37b81a59e1186e952afd2a7ed92018c22f31ca/pydantic_core-2.41.1-cp312-cp312-musllinux_1_1_armv7l.whl",hashes = {sha256 = "2e71b1c6ceb9c78424ae9f63a07292fb769fb890a4e7efca5554c47f33a60ea5"}}, - {name = "pydantic_core-2.41.1-cp312-cp312-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/65/e8/e9aecafaebf53fc456314f72886068725d6fba66f11b013532dc21259343/pydantic_core-2.41.1-cp312-cp312-musllinux_1_1_x86_64.whl",hashes = {sha256 = "80745b9770b4a38c25015b517451c817799bfb9d6499b0d13d8227ec941cb513"}}, - {name = "pydantic_core-2.41.1-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/35/2f/1c2e71d2a052f9bb2f2df5a6a05464a0eb800f9e8d9dd800202fe31219e1/pydantic_core-2.41.1-cp312-cp312-win32.whl",hashes = {sha256 = "83b64d70520e7890453f1aa21d66fda44e7b35f1cfea95adf7b4289a51e2b479"}}, - {name = "pydantic_core-2.41.1-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/b1/78/562998301ff2588b9c6dcc5cb21f52fa919d6e1decc75a35055feb973594/pydantic_core-2.41.1-cp312-cp312-win_amd64.whl",hashes = {sha256 = "377defd66ee2003748ee93c52bcef2d14fde48fe28a0b156f88c3dbf9bc49a50"}}, - {name = "pydantic_core-2.41.1-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/b2/53/d95699ce5a5cdb44bb470bd818b848b9beadf51459fd4ea06667e8ede862/pydantic_core-2.41.1-cp312-cp312-win_arm64.whl",hashes = {sha256 = "c95caff279d49c1d6cdfe2996e6c2ad712571d3b9caaa209a404426c326c4bde"}}, - {name = "pydantic_core-2.41.1-cp311-cp311-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/f6/a9/ec440f02e57beabdfd804725ef1e38ac1ba00c49854d298447562e119513/pydantic_core-2.41.1-cp311-cp311-macosx_10_12_x86_64.whl",hashes = {sha256 = "4f276a6134fe1fc1daa692642a3eaa2b7b858599c49a7610816388f5e37566a1"}}, - {name = "pydantic_core-2.41.1-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/f0/f9/6bc15bacfd8dcfc073a1820a564516d9c12a435a9a332d4cbbfd48828ddd/pydantic_core-2.41.1-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "07588570a805296ece009c59d9a679dc08fab72fb337365afb4f3a14cfbfc176"}}, - {name = "pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/38/8a/d9edcdcdfe80bade17bed424284427c08bea892aaec11438fa52eaeaf79c/pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "28527e4b53400cd60ffbd9812ccb2b5135d042129716d71afd7e45bf42b855c0"}}, - {name = "pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",url = "https://files.pythonhosted.org/packages/d5/b3/ff225c6d49fba4279de04677c1c876fc3dc6562fd0c53e9bfd66f58c51a8/pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",hashes = {sha256 = "46a1c935c9228bad738c8a41de06478770927baedf581d172494ab36a6b96575"}}, - {name = "pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/47/ba/183e8c0be4321314af3fd1ae6bfc7eafdd7a49bdea5da81c56044a207316/pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "447ddf56e2b7d28d200d3e9eafa936fe40485744b5a824b67039937580b3cb20"}}, - {name = "pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/57/c5/aab61e94fd02f45c65f1f8c9ec38bb3b33fbf001a1837c74870e97462572/pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "63892ead40c1160ac860b5debcc95c95c5a0035e543a8b5a4eac70dd22e995f4"}}, - {name = "pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/e5/4f/3aaa3bd1ea420a15acc42d7d3ccb3b0bbc5444ae2f9dbc1959f8173e16b8/pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "f4a9543ca355e6df8fbe9c83e9faab707701e9103ae857ecb40f1c0cf8b0e94d"}}, - {name = "pydantic_core-2.41.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl",url = "https://files.pythonhosted.org/packages/58/bd/e3975cdebe03ec080ef881648de316c73f2a6be95c14fc4efb2f7bdd0d41/pydantic_core-2.41.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl",hashes = {sha256 = "f2611bdb694116c31e551ed82e20e39a90bea9b7ad9e54aaf2d045ad621aa7a1"}}, - {name = "pydantic_core-2.41.1-cp311-cp311-musllinux_1_1_aarch64.whl",url = "https://files.pythonhosted.org/packages/2b/b8/6b7e7217f147d3b3105b57fb1caec3c4f667581affdfaab6d1d277e1f749/pydantic_core-2.41.1-cp311-cp311-musllinux_1_1_aarch64.whl",hashes = {sha256 = "fecc130893a9b5f7bfe230be1bb8c61fe66a19db8ab704f808cb25a82aad0bc9"}}, - {name = "pydantic_core-2.41.1-cp311-cp311-musllinux_1_1_armv7l.whl",url = "https://files.pythonhosted.org/packages/fe/7b/239c2fe76bd8b7eef9ae2140d737368a3c6fea4fd27f8f6b4cde6baa3ce9/pydantic_core-2.41.1-cp311-cp311-musllinux_1_1_armv7l.whl",hashes = {sha256 = "1e2df5f8344c99b6ea5219f00fdc8950b8e6f2c422fbc1cc122ec8641fac85a1"}}, - {name = "pydantic_core-2.41.1-cp311-cp311-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/bd/2e/77a821a67ff0786f2f14856d6bd1348992f695ee90136a145d7a445c1ff6/pydantic_core-2.41.1-cp311-cp311-musllinux_1_1_x86_64.whl",hashes = {sha256 = "35291331e9d8ed94c257bab6be1cb3a380b5eee570a2784bffc055e18040a2ea"}}, - {name = "pydantic_core-2.41.1-cp311-cp311-win32.whl",url = "https://files.pythonhosted.org/packages/fd/9a/b54512bb9df7f64c586b369328c30481229b70ca6a5fcbb90b715e15facf/pydantic_core-2.41.1-cp311-cp311-win32.whl",hashes = {sha256 = "2876a095292668d753f1a868c4a57c4ac9f6acbd8edda8debe4218d5848cf42f"}}, - {name = "pydantic_core-2.41.1-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/9d/72/63c9a4f1a5c950e65dd522d7dd67f167681f9d4f6ece3b80085a0329f08f/pydantic_core-2.41.1-cp311-cp311-win_amd64.whl",hashes = {sha256 = "b92d6c628e9a338846a28dfe3fcdc1a3279388624597898b105e078cdfc59298"}}, - {name = "pydantic_core-2.41.1-cp311-cp311-win_arm64.whl",url = "https://files.pythonhosted.org/packages/d8/16/4e2706184209f61b50c231529257c12eb6bd9eb36e99ea1272e4815d2200/pydantic_core-2.41.1-cp311-cp311-win_arm64.whl",hashes = {sha256 = "7d82ae99409eb69d507a89835488fb657faa03ff9968a9379567b0d2e2e56bc5"}}, - {name = "pydantic_core-2.41.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/e6/6c/fa3e45c2b054a1e627a89a364917f12cbe3abc3e91b9004edaae16e7b3c5/pydantic_core-2.41.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl",hashes = {sha256 = "af2385d3f98243fb733862f806c5bb9122e5fba05b373e3af40e3c82d711cef1"}}, - {name = "pydantic_core-2.41.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/e5/17/7eebc38b4658cc8e6902d0befc26388e4c2a5f2e179c561eeb43e1922c7b/pydantic_core-2.41.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl",hashes = {sha256 = "6550617a0c2115be56f90c31a5370261d8ce9dbf051c3ed53b51172dd34da696"}}, - {name = "pydantic_core-2.41.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/2b/00/9fe640194a1717a464ab861d43595c268830f98cb1e2705aa134b3544b70/pydantic_core-2.41.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "dc17b6ecf4983d298686014c92ebc955a9f9baf9f57dad4065e7906e7bee6222"}}, - {name = "pydantic_core-2.41.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl",url = "https://files.pythonhosted.org/packages/b2/ad/f4cdfaf483b78ee65362363e73b6b40c48e067078d7b146e8816d5945ad6/pydantic_core-2.41.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl",hashes = {sha256 = "42ae9352cf211f08b04ea110563d6b1e415878eea5b4c70f6bdb17dca3b932d2"}}, - {name = "pydantic_core-2.41.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl",url = "https://files.pythonhosted.org/packages/cb/c1/18f416d40a10f44e9387497ba449f40fdb1478c61ba05c4b6bdb82300362/pydantic_core-2.41.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl",hashes = {sha256 = "e82947de92068b0a21681a13dd2102387197092fbe7defcfb8453e0913866506"}}, - {name = "pydantic_core-2.41.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl",url = "https://files.pythonhosted.org/packages/42/30/134c8a921630d8a88d6f905a562495a6421e959a23c19b0f49b660801d67/pydantic_core-2.41.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl",hashes = {sha256 = "e244c37d5471c9acdcd282890c6c4c83747b77238bfa19429b8473586c907656"}}, - {name = "pydantic_core-2.41.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/9c/48/a9263aeaebdec81e941198525b43edb3b44f27cfa4cb8005b8d3eb8dec72/pydantic_core-2.41.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl",hashes = {sha256 = "1e798b4b304a995110d41ec93653e57975620ccb2842ba9420037985e7d7284e"}}, - {name = "pydantic_core-2.41.1-pp311-pypy311_pp73-win_amd64.whl",url = "https://files.pythonhosted.org/packages/1d/62/755d2bd2593f701c5839fc084e9c2c5e2418f460383ad04e3b5d0befc3ca/pydantic_core-2.41.1-pp311-pypy311_pp73-win_amd64.whl",hashes = {sha256 = "f1fc716c0eb1663c59699b024428ad5ec2bcc6b928527b8fe28de6cb89f47efb"}}, - {name = "pydantic_core-2.41.1-cp310-cp310-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/b3/2c/a5c4640dc7132540109f67fe83b566fbc7512ccf2a068cfa22a243df70c7/pydantic_core-2.41.1-cp310-cp310-macosx_10_12_x86_64.whl",hashes = {sha256 = "e63036298322e9aea1c8b7c0a6c1204d615dbf6ec0668ce5b83ff27f07404a61"}}, - {name = "pydantic_core-2.41.1-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/e3/e7/a8694c3454a57842095d69c7a4ab3cf81c3c7b590f052738eabfdfc2e234/pydantic_core-2.41.1-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "241299ca91fc77ef64f11ed909d2d9220a01834e8e6f8de61275c4dd16b7c936"}}, - {name = "pydantic_core-2.41.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/9c/58/29f12e65b19c1877a0269eb4f23c5d2267eded6120a7d6762501ab843dc9/pydantic_core-2.41.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "1ab7e594a2a5c24ab8013a7dc8cfe5f2260e80e490685814122081705c2cf2b0"}}, - {name = "pydantic_core-2.41.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",url = "https://files.pythonhosted.org/packages/98/26/4e677f2b7ec3fbdd10be6b586a82a814c8ebe3e474024c8df2d4260e564e/pydantic_core-2.41.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",hashes = {sha256 = "b054ef1a78519cb934b58e9c90c09e93b837c935dcd907b891f2b265b129eb6e"}}, - {name = "pydantic_core-2.41.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/29/50/50614bd906089904d7ca1be3b9ecf08c00a327143d48f1decfdc21b3c302/pydantic_core-2.41.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "f2ab7d10d0ab2ed6da54c757233eb0f48ebfb4f86e9b88ccecb3f92bbd61a538"}}, - {name = "pydantic_core-2.41.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/ea/58/b1e640b4ca559273cca7c28e0fe8891d5d8e9a600f5ab4882670ec107549/pydantic_core-2.41.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "2757606b7948bb853a27e4040820306eaa0ccb9e8f9f8a0fa40cb674e170f350"}}, - {name = "pydantic_core-2.41.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/53/25/cd47df3bfb24350e03835f0950288d1054f1cc9a8023401dabe6d4ff2834/pydantic_core-2.41.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "cec0e75eb61f606bad0a32f2be87507087514e26e8c73db6cbdb8371ccd27917"}}, - {name = "pydantic_core-2.41.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl",url = "https://files.pythonhosted.org/packages/ec/b4/71b2c77e5df527fbbc1a03e72c3fd96c44cd10d4241a81befef8c12b9fc4/pydantic_core-2.41.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl",hashes = {sha256 = "0234236514f44a5bf552105cfe2543a12f48203397d9d0f866affa569345a5b5"}}, - {name = "pydantic_core-2.41.1-cp310-cp310-musllinux_1_1_aarch64.whl",url = "https://files.pythonhosted.org/packages/aa/08/4b8a50733005865efde284fec45da75fe16a258f706e16323c5ace4004eb/pydantic_core-2.41.1-cp310-cp310-musllinux_1_1_aarch64.whl",hashes = {sha256 = "1b974e41adfbb4ebb0f65fc4ca951347b17463d60893ba7d5f7b9bb087c83897"}}, - {name = "pydantic_core-2.41.1-cp310-cp310-musllinux_1_1_armv7l.whl",url = "https://files.pythonhosted.org/packages/83/c3/1037cb603ef2130c210150a51b1710d86825b5c28df54a55750099f91196/pydantic_core-2.41.1-cp310-cp310-musllinux_1_1_armv7l.whl",hashes = {sha256 = "248dafb3204136113c383e91a4d815269f51562b6659b756cf3df14eefc7d0bb"}}, - {name = "pydantic_core-2.41.1-cp310-cp310-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/56/4c/52d111869610e6b1a46e1f1035abcdc94d0655587e39104433a290e9f377/pydantic_core-2.41.1-cp310-cp310-musllinux_1_1_x86_64.whl",hashes = {sha256 = "678f9d76a91d6bcedd7568bbf6beb77ae8447f85d1aeebaab7e2f0829cfc3a13"}}, - {name = "pydantic_core-2.41.1-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/32/5d/4b435f0b52ab543967761aca66b84ad3f0026e491e57de47693d15d0a8db/pydantic_core-2.41.1-cp310-cp310-win32.whl",hashes = {sha256 = "dff5bee1d21ee58277900692a641925d2dddfde65182c972569b1a276d2ac8fb"}}, - {name = "pydantic_core-2.41.1-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/88/52/31b4deafc1d3cb96d0e7c0af70f0dc05454982d135d07f5117e6336153e8/pydantic_core-2.41.1-cp310-cp310-win_amd64.whl",hashes = {sha256 = "5042da12e5d97d215f91567110fdfa2e2595a25f17c19b9ff024f31c34f9b53e"}}, - {name = "pydantic_core-2.41.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/d4/31/f403d7ca8352e3e4df352ccacd200f5f7f7fe81cef8e458515f015091625/pydantic_core-2.41.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl",hashes = {sha256 = "fabcbdb12de6eada8d6e9a759097adb3c15440fafc675b3e94ae5c9cb8d678a0"}}, - {name = "pydantic_core-2.41.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/6e/b5/334473b6d2810df84db67f03d4f666acacfc538512c2d2a254074fee0889/pydantic_core-2.41.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl",hashes = {sha256 = "80e97ccfaf0aaf67d55de5085b0ed0d994f57747d9d03f2de5cc9847ca737b08"}}, - {name = "pydantic_core-2.41.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/ea/5e/45513e4dc621f47397cfa5fef12ba8fa5e8b1c4c07f2ff2a5fef8ff81b25/pydantic_core-2.41.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "34df1fe8fea5d332484a763702e8b6a54048a9d4fe6ccf41e34a128238e01f52"}}, - {name = "pydantic_core-2.41.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl",url = "https://files.pythonhosted.org/packages/22/e3/f1797c168e5f52b973bed1c585e99827a22d5e579d1ed57d51bc15b14633/pydantic_core-2.41.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl",hashes = {sha256 = "421b5595f845842fc093f7250e24ee395f54ca62d494fdde96f43ecf9228ae01"}}, - {name = "pydantic_core-2.41.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl",url = "https://files.pythonhosted.org/packages/bb/e1/24ef4c3b4ab91c21c3a09a966c7d2cffe101058a7bfe5cc8b2c7c7d574e2/pydantic_core-2.41.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl",hashes = {sha256 = "dce8b22663c134583aaad24827863306a933f576c79da450be3984924e2031d1"}}, - {name = "pydantic_core-2.41.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl",url = "https://files.pythonhosted.org/packages/35/74/70c1e225d67f7ef3fdba02c506d9011efaf734020914920b2aa3d1a45e61/pydantic_core-2.41.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl",hashes = {sha256 = "300a9c162fea9906cc5c103893ca2602afd84f0ec90d3be36f4cc360125d22e1"}}, - {name = "pydantic_core-2.41.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/c8/bf/dd4d21037c8bef0d8cce90a86a3f2dcb011c30086db2a10113c3eea23eba/pydantic_core-2.41.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl",hashes = {sha256 = "e019167628f6e6161ae7ab9fb70f6d076a0bf0d55aa9b20833f86a320c70dd65"}}, - {name = "pydantic_core-2.41.1-pp310-pypy310_pp73-win_amd64.whl",url = "https://files.pythonhosted.org/packages/7e/78/3093b334e9c9796c8236a4701cd2ddef1c56fb0928fe282a10c797644380/pydantic_core-2.41.1-pp310-pypy310_pp73-win_amd64.whl",hashes = {sha256 = "13ab9cc2de6f9d4ab645a050ae5aee61a2424ac4d3a16ba23d4c2027705e0301"}}, +sdist = {name = "pydantic_core-2.41.4.tar.gz", url = "https://files.pythonhosted.org/packages/df/18/d0944e8eaaa3efd0a91b0f1fc537d3be55ad35091b6a87638211ba691964/pydantic_core-2.41.4.tar.gz", hashes = {sha256 = "70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5"}} +wheels = [ + {name = "pydantic_core-2.41.4-cp314-cp314-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/54/28/d3325da57d413b9819365546eb9a6e8b7cbd9373d9380efd5f74326143e6/pydantic_core-2.41.4-cp314-cp314-macosx_10_12_x86_64.whl",hashes = {sha256 = "e9205d97ed08a82ebb9a307e92914bb30e18cdf6f6b12ca4bedadb1588a0bfe1"}}, + {name = "pydantic_core-2.41.4-cp314-cp314-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/9e/24/b58a1bc0d834bf1acc4361e61233ee217169a42efbdc15a60296e13ce438/pydantic_core-2.41.4-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "82df1f432b37d832709fbcc0e24394bba04a01b6ecf1ee87578145c19cde12ac"}}, + {name = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/fb/a4/71f759cc41b7043e8ecdaab81b985a9b6cad7cec077e0b92cff8b71ecf6b/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "fc3b4cc4539e055cfa39a3763c939f9d409eb40e85813257dcd761985a108554"}}, + {name = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",url = "https://files.pythonhosted.org/packages/b0/64/1e79ac7aa51f1eec7c4cda8cbe456d5d09f05fdd68b32776d72168d54275/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",hashes = {sha256 = "b1eb1754fce47c63d2ff57fdb88c351a6c0150995890088b33767a10218eaa4e"}}, + {name = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/e9/e3/a3ffc363bd4287b80f1d43dc1c28ba64831f8dfc237d6fec8f2661138d48/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "e6ab5ab30ef325b443f379ddb575a34969c333004fca5a1daa0133a6ffaad616"}}, + {name = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/28/27/78814089b4d2e684a9088ede3790763c64693c3d1408ddc0a248bc789126/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "31a41030b1d9ca497634092b46481b937ff9397a86f9f51bd41c4767b6fc04af"}}, + {name = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/92/97/4de0e2a1159cb85ad737e03306717637842c88c7fd6d97973172fb183149/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "a44ac1738591472c3d020f61c6df1e4015180d6262ebd39bf2aeb52571b60f12"}}, + {name = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl",url = "https://files.pythonhosted.org/packages/0f/50/8cb90ce4b9efcf7ae78130afeb99fd1c86125ccdf9906ef64b9d42f37c25/pydantic_core-2.41.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl",hashes = {sha256 = "d72f2b5e6e82ab8f94ea7d0d42f83c487dc159c5240d8f83beae684472864e2d"}}, + {name = "pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_aarch64.whl",url = "https://files.pythonhosted.org/packages/34/3b/ccdc77af9cd5082723574a1cc1bcae7a6acacc829d7c0a06201f7886a109/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_aarch64.whl",hashes = {sha256 = "c4d1e854aaf044487d31143f541f7aafe7b482ae72a022c664b2de2e466ed0ad"}}, + {name = "pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_armv7l.whl",url = "https://files.pythonhosted.org/packages/ca/ba/e7c7a02651a8f7c52dc2cff2b64a30c313e3b57c7d93703cecea76c09b71/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_armv7l.whl",hashes = {sha256 = "b568af94267729d76e6ee5ececda4e283d07bbb28e8148bb17adad93d025d25a"}}, + {name = "pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/2c/ba/6c533a4ee8aec6b812c643c49bb3bd88d3f01e3cebe451bb85512d37f00f/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_x86_64.whl",hashes = {sha256 = "6d55fb8b1e8929b341cc313a81a26e0d48aa3b519c1dbaadec3a6a2b4fcad025"}}, + {name = "pydantic_core-2.41.4-cp314-cp314-win32.whl",url = "https://files.pythonhosted.org/packages/22/ae/f10524fcc0ab8d7f96cf9a74c880243576fd3e72bd8ce4f81e43d22bcab7/pydantic_core-2.41.4-cp314-cp314-win32.whl",hashes = {sha256 = "5b66584e549e2e32a1398df11da2e0a7eff45d5c2d9db9d5667c5e6ac764d77e"}}, + {name = "pydantic_core-2.41.4-cp314-cp314-win_amd64.whl",url = "https://files.pythonhosted.org/packages/b4/dc/e5aa27aea1ad4638f0c3fb41132f7eb583bd7420ee63204e2d4333a3bbf9/pydantic_core-2.41.4-cp314-cp314-win_amd64.whl",hashes = {sha256 = "557a0aab88664cc552285316809cab897716a372afaf8efdbef756f8b890e894"}}, + {name = "pydantic_core-2.41.4-cp314-cp314-win_arm64.whl",url = "https://files.pythonhosted.org/packages/3e/61/51d89cc2612bd147198e120a13f150afbf0bcb4615cddb049ab10b81b79e/pydantic_core-2.41.4-cp314-cp314-win_arm64.whl",hashes = {sha256 = "3f1ea6f48a045745d0d9f325989d8abd3f1eaf47dd00485912d1a3a63c623a8d"}}, + {name = "pydantic_core-2.41.4-cp314-cp314t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/0d/c2/472f2e31b95eff099961fa050c376ab7156a81da194f9edb9f710f68787b/pydantic_core-2.41.4-cp314-cp314t-macosx_11_0_arm64.whl",hashes = {sha256 = "6c1fe4c5404c448b13188dd8bd2ebc2bdd7e6727fa61ff481bcc2cca894018da"}}, + {name = "pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/4a/07/ea8eeb91173807ecdae4f4a5f4b150a520085b35454350fc219ba79e66a3/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "523e7da4d43b113bf8e7b49fa4ec0c35bf4fe66b2230bfc5c13cc498f12c6c3e"}}, + {name = "pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/1e/29/b53a9ca6cd366bfc928823679c6a76c7a4c69f8201c0ba7903ad18ebae2f/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa"}}, + {name = "pydantic_core-2.41.4-cp314-cp314t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/c7/3d/f8c1a371ceebcaf94d6dd2d77c6cf4b1c078e13a5837aee83f760b4f7cfd/pydantic_core-2.41.4-cp314-cp314t-win_amd64.whl",hashes = {sha256 = "de2cfbb09e88f0f795fd90cf955858fc2c691df65b1f21f0aa00b99f3fbc661d"}}, + {name = "pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/8a/ac/9fc61b4f9d079482a290afe8d206b8f490e9fd32d4fc03ed4fc698214e01/pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl",hashes = {sha256 = "d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0"}}, + {name = "pydantic_core-2.41.4-cp313-cp313-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/13/d0/c20adabd181a029a970738dfe23710b52a31f1258f591874fcdec7359845/pydantic_core-2.41.4-cp313-cp313-macosx_10_12_x86_64.whl",hashes = {sha256 = "85e050ad9e5f6fe1004eec65c914332e52f429bc0ae12d6fa2092407a462c746"}}, + {name = "pydantic_core-2.41.4-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/00/b6/0ce5c03cec5ae94cca220dfecddc453c077d71363b98a4bbdb3c0b22c783/pydantic_core-2.41.4-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "e7393f1d64792763a48924ba31d1e44c2cfbc05e3b1c2c9abb4ceeadd912cced"}}, + {name = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/68/3e/800d3d02c8beb0b5c069c870cbb83799d085debf43499c897bb4b4aaff0d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "94dab0940b0d1fb28bcab847adf887c66a27a40291eedf0b473be58761c9799a"}}, + {name = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",url = "https://files.pythonhosted.org/packages/60/a4/24271cc71a17f64589be49ab8bd0751f6a0a03046c690df60989f2f95c2c/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",hashes = {sha256 = "de7c42f897e689ee6f9e93c4bec72b99ae3b32a2ade1c7e4798e690ff5246e02"}}, + {name = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/68/de/45af3ca2f175d91b96bfb62e1f2d2f1f9f3b14a734afe0bfeff079f78181/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "664b3199193262277b8b3cd1e754fb07f2c6023289c815a1e1e8fb415cb247b1"}}, + {name = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/af/8f/ae4e1ff84672bf869d0a77af24fd78387850e9497753c432875066b5d622/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "d95b253b88f7d308b1c0b417c4624f44553ba4762816f94e6986819b9c273fb2"}}, + {name = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/18/62/273dd70b0026a085c7b74b000394e1ef95719ea579c76ea2f0cc8893736d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "a1351f5bbdbbabc689727cb91649a00cb9ee7203e0a6e54e9f5ba9e22e384b84"}}, + {name = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl",url = "https://files.pythonhosted.org/packages/30/03/cf485fff699b4cdaea469bc481719d3e49f023241b4abb656f8d422189fc/pydantic_core-2.41.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl",hashes = {sha256 = "1affa4798520b148d7182da0615d648e752de4ab1a9566b7471bc803d88a062d"}}, + {name = "pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_aarch64.whl",url = "https://files.pythonhosted.org/packages/f9/7e/c8e713db32405dfd97211f2fc0a15d6bf8adb7640f3d18544c1f39526619/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_aarch64.whl",hashes = {sha256 = "7b74e18052fea4aa8dea2fb7dbc23d15439695da6cbe6cfc1b694af1115df09d"}}, + {name = "pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_armv7l.whl",url = "https://files.pythonhosted.org/packages/04/f7/db71fd4cdccc8b75990f79ccafbbd66757e19f6d5ee724a6252414483fb4/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_armv7l.whl",hashes = {sha256 = "285b643d75c0e30abda9dc1077395624f314a37e3c09ca402d4015ef5979f1a2"}}, + {name = "pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/76/63/a54973ddb945f1bca56742b48b144d85c9fc22f819ddeb9f861c249d5464/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_x86_64.whl",hashes = {sha256 = "f52679ff4218d713b3b33f88c89ccbf3a5c2c12ba665fb80ccc4192b4608dbab"}}, + {name = "pydantic_core-2.41.4-cp313-cp313-win32.whl",url = "https://files.pythonhosted.org/packages/f8/03/5d12891e93c19218af74843a27e32b94922195ded2386f7b55382f904d2f/pydantic_core-2.41.4-cp313-cp313-win32.whl",hashes = {sha256 = "ecde6dedd6fff127c273c76821bb754d793be1024bc33314a120f83a3c69460c"}}, + {name = "pydantic_core-2.41.4-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/be/d8/fd0de71f39db91135b7a26996160de71c073d8635edfce8b3c3681be0d6d/pydantic_core-2.41.4-cp313-cp313-win_amd64.whl",hashes = {sha256 = "d081a1f3800f05409ed868ebb2d74ac39dd0c1ff6c035b5162356d76030736d4"}}, + {name = "pydantic_core-2.41.4-cp313-cp313-win_arm64.whl",url = "https://files.pythonhosted.org/packages/72/86/c99921c1cf6650023c08bfab6fe2d7057a5142628ef7ccfa9921f2dda1d5/pydantic_core-2.41.4-cp313-cp313-win_arm64.whl",hashes = {sha256 = "f8e49c9c364a7edcbe2a310f12733aad95b022495ef2a8d653f645e5d20c1564"}}, + {name = "pydantic_core-2.41.4-cp313-cp313t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/36/0d/b5706cacb70a8414396efdda3d72ae0542e050b591119e458e2490baf035/pydantic_core-2.41.4-cp313-cp313t-macosx_11_0_arm64.whl",hashes = {sha256 = "ed97fd56a561f5eb5706cebe94f1ad7c13b84d98312a05546f2ad036bafe87f4"}}, + {name = "pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/de/2d/cba1fa02cfdea72dfb3a9babb067c83b9dff0bbcb198368e000a6b756ea7/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "a870c307bf1ee91fc58a9a61338ff780d01bfae45922624816878dce784095d2"}}, + {name = "pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/07/ea/3df927c4384ed9b503c9cc2d076cf983b4f2adb0c754578dfb1245c51e46/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "d25e97bc1f5f8f7985bdc2335ef9e73843bb561eb1fa6831fdfc295c1c2061cf"}}, + {name = "pydantic_core-2.41.4-cp313-cp313t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/6a/ee/df8e871f07074250270a3b1b82aad4cd0026b588acd5d7d3eb2fcb1471a3/pydantic_core-2.41.4-cp313-cp313t-win_amd64.whl",hashes = {sha256 = "d405d14bea042f166512add3091c1af40437c2e7f86988f3915fabd27b1e9cd2"}}, + {name = "pydantic_core-2.41.4-cp313-cp313t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/fc/de/b20f4ab954d6d399499c33ec4fafc46d9551e11dc1858fb7f5dca0748ceb/pydantic_core-2.41.4-cp313-cp313t-win_arm64.whl",hashes = {sha256 = "19f3684868309db5263a11bace3c45d93f6f24afa2ffe75a647583df22a2ff89"}}, + {name = "pydantic_core-2.41.4-cp312-cp312-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/e9/81/d3b3e95929c4369d30b2a66a91db63c8ed0a98381ae55a45da2cd1cc1288/pydantic_core-2.41.4-cp312-cp312-macosx_10_12_x86_64.whl",hashes = {sha256 = "ab06d77e053d660a6faaf04894446df7b0a7e7aba70c2797465a0a1af00fc887"}}, + {name = "pydantic_core-2.41.4-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/58/da/46fdac49e6717e3a94fc9201403e08d9d61aa7a770fab6190b8740749047/pydantic_core-2.41.4-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "c53ff33e603a9c1179a9364b0a24694f183717b2e0da2b5ad43c316c956901b2"}}, + {name = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/1e/63/4d948f1b9dd8e991a5a98b77dd66c74641f5f2e5225fee37994b2e07d391/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "304c54176af2c143bd181d82e77c15c41cbacea8872a2225dd37e6544dce9999"}}, + {name = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",url = "https://files.pythonhosted.org/packages/b2/a7/e5fc60a6f781fc634ecaa9ecc3c20171d238794cef69ae0af79ac11b89d7/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",hashes = {sha256 = "025ba34a4cf4fb32f917d5d188ab5e702223d3ba603be4d8aca2f82bede432a4"}}, + {name = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/70/69/dce747b1d21d59e85af433428978a1893c6f8a7068fa2bb4a927fba7a5ff/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "b9f5f30c402ed58f90c70e12eff65547d3ab74685ffe8283c719e6bead8ef53f"}}, + {name = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/83/6a/c070e30e295403bf29c4df1cb781317b6a9bac7cd07b8d3acc94d501a63c/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "dd96e5d15385d301733113bcaa324c8bcf111275b7675a9c6e88bfb19fc05e3b"}}, + {name = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/f0/83/06d001f8043c336baea7fd202a9ac7ad71f87e1c55d8112c50b745c40324/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "98f348cbb44fae6e9653c1055db7e29de67ea6a9ca03a5fa2c2e11a47cff0e47"}}, + {name = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl",url = "https://files.pythonhosted.org/packages/14/0a/e567c2883588dd12bcbc110232d892cf385356f7c8a9910311ac997ab715/pydantic_core-2.41.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl",hashes = {sha256 = "ec22626a2d14620a83ca583c6f5a4080fa3155282718b6055c2ea48d3ef35970"}}, + {name = "pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_aarch64.whl",url = "https://files.pythonhosted.org/packages/f4/1d/3d9fca34273ba03c9b1c5289f7618bc4bd09c3ad2289b5420481aa051a99/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_aarch64.whl",hashes = {sha256 = "3a95d4590b1f1a43bf33ca6d647b990a88f4a3824a8c4572c708f0b45a5290ed"}}, + {name = "pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_armv7l.whl",url = "https://files.pythonhosted.org/packages/52/70/d702ef7a6cd41a8afc61f3554922b3ed8d19dd54c3bd4bdbfe332e610827/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_armv7l.whl",hashes = {sha256 = "f9672ab4d398e1b602feadcffcdd3af44d5f5e6ddc15bc7d15d376d47e8e19f8"}}, + {name = "pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/68/4c/c06be6e27545d08b802127914156f38d10ca287a9e8489342793de8aae3c/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_x86_64.whl",hashes = {sha256 = "84d8854db5f55fead3b579f04bda9a36461dab0730c5d570e1526483e7bb8431"}}, + {name = "pydantic_core-2.41.4-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/b0/e5/35ae4919bcd9f18603419e23c5eaf32750224a89d41a8df1a3704b69f77e/pydantic_core-2.41.4-cp312-cp312-win32.whl",hashes = {sha256 = "9be1c01adb2ecc4e464392c36d17f97e9110fbbc906bcbe1c943b5b87a74aabd"}}, + {name = "pydantic_core-2.41.4-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/1e/c2/49c5bb6d2a49eb2ee3647a93e3dae7080c6409a8a7558b075027644e879c/pydantic_core-2.41.4-cp312-cp312-win_amd64.whl",hashes = {sha256 = "d682cf1d22bab22a5be08539dca3d1593488a99998f9f412137bc323179067ff"}}, + {name = "pydantic_core-2.41.4-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/06/23/936343dbcba6eec93f73e95eb346810fc732f71ba27967b287b66f7b7097/pydantic_core-2.41.4-cp312-cp312-win_arm64.whl",hashes = {sha256 = "833eebfd75a26d17470b58768c1834dfc90141b7afc6eb0429c21fc5a21dcfb8"}}, + {name = "pydantic_core-2.41.4-cp311-cp311-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/62/4c/f6cbfa1e8efacd00b846764e8484fe173d25b8dab881e277a619177f3384/pydantic_core-2.41.4-cp311-cp311-macosx_10_12_x86_64.whl",hashes = {sha256 = "28ff11666443a1a8cf2a044d6a545ebffa8382b5f7973f22c36109205e65dc80"}}, + {name = "pydantic_core-2.41.4-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/21/f8/40b72d3868896bfcd410e1bd7e516e762d326201c48e5b4a06446f6cf9e8/pydantic_core-2.41.4-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "61760c3925d4633290292bad462e0f737b840508b4f722247d8729684f6539ae"}}, + {name = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/94/4d/d203dce8bee7faeca791671c88519969d98d3b4e8f225da5b96dad226fc8/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "eae547b7315d055b0de2ec3965643b0ab82ad0106a7ffd29615ee9f266a02827"}}, + {name = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",url = "https://files.pythonhosted.org/packages/65/f5/6a66187775df87c24d526985b3a5d78d861580ca466fbd9d4d0e792fcf6c/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",hashes = {sha256 = "ef9ee5471edd58d1fcce1c80ffc8783a650e3e3a193fe90d52e43bb4d87bff1f"}}, + {name = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/5e/b9/78336345de97298cf53236b2f271912ce11f32c1e59de25a374ce12f9cce/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "15dd504af121caaf2c95cb90c0ebf71603c53de98305621b94da0f967e572def"}}, + {name = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/99/bb/a4584888b70ee594c3d374a71af5075a68654d6c780369df269118af7402/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "3a926768ea49a8af4d36abd6a8968b8790f7f76dd7cbd5a4c180db2b4ac9a3a2"}}, + {name = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/5f/8d/17fc5de9d6418e4d2ae8c675f905cdafdc59d3bf3bf9c946b7ab796a992a/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "6916b9b7d134bff5440098a4deb80e4cb623e68974a87883299de9124126c2a8"}}, + {name = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl",url = "https://files.pythonhosted.org/packages/54/e7/03d2c5c0b8ed37a4617430db68ec5e7dbba66358b629cd69e11b4d564367/pydantic_core-2.41.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl",hashes = {sha256 = "5cf90535979089df02e6f17ffd076f07237efa55b7343d98760bde8743c4b265"}}, + {name = "pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_aarch64.whl",url = "https://files.pythonhosted.org/packages/be/fc/15d1c9fe5ad9266a5897d9b932b7f53d7e5cfc800573917a2c5d6eea56ec/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_aarch64.whl",hashes = {sha256 = "7533c76fa647fade2d7ec75ac5cc079ab3f34879626dae5689b27790a6cf5a5c"}}, + {name = "pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_armv7l.whl",url = "https://files.pythonhosted.org/packages/26/ef/e735dd008808226c83ba56972566138665b71477ad580fa5a21f0851df48/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_armv7l.whl",hashes = {sha256 = "37e516bca9264cbf29612539801ca3cd5d1be465f940417b002905e6ed79d38a"}}, + {name = "pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/90/00/806efdcf35ff2ac0f938362350cd9827b8afb116cc814b6b75cf23738c7c/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_x86_64.whl",hashes = {sha256 = "0c19cb355224037c83642429b8ce261ae108e1c5fbf5c028bac63c77b0f8646e"}}, + {name = "pydantic_core-2.41.4-cp311-cp311-win32.whl",url = "https://files.pythonhosted.org/packages/41/7e/6ac90673fe6cb36621a2283552897838c020db343fa86e513d3f563b196f/pydantic_core-2.41.4-cp311-cp311-win32.whl",hashes = {sha256 = "09c2a60e55b357284b5f31f5ab275ba9f7f70b7525e18a132ec1f9160b4f1f03"}}, + {name = "pydantic_core-2.41.4-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/e0/9d/7c5e24ee585c1f8b6356e1d11d40ab807ffde44d2db3b7dfd6d20b09720e/pydantic_core-2.41.4-cp311-cp311-win_amd64.whl",hashes = {sha256 = "711156b6afb5cb1cb7c14a2cc2c4a8b4c717b69046f13c6b332d8a0a8f41ca3e"}}, + {name = "pydantic_core-2.41.4-cp311-cp311-win_arm64.whl",url = "https://files.pythonhosted.org/packages/33/90/5c172357460fc28b2871eb4a0fb3843b136b429c6fa827e4b588877bf115/pydantic_core-2.41.4-cp311-cp311-win_arm64.whl",hashes = {sha256 = "6cb9cf7e761f4f8a8589a45e49ed3c0d92d1d696a45a6feaee8c904b26efc2db"}}, + {name = "pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/7e/7d/138e902ed6399b866f7cfe4435d22445e16fff888a1c00560d9dc79a780f/pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl",hashes = {sha256 = "491535d45cd7ad7e4a2af4a5169b0d07bebf1adfd164b0368da8aa41e19907a5"}}, + {name = "pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/47/13/0525623cf94627f7b53b4c2034c81edc8491cbfc7c28d5447fa318791479/pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl",hashes = {sha256 = "54d86c0cada6aba4ec4c047d0e348cbad7063b87ae0f005d9f8c9ad04d4a92a2"}}, + {name = "pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/d6/f9/744bc98137d6ef0a233f808bfc9b18cf94624bf30836a18d3b05d08bf418/pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "eca1124aced216b2500dc2609eade086d718e8249cb9696660ab447d50a758bd"}}, + {name = "pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl",url = "https://files.pythonhosted.org/packages/17/c8/629e88920171173f6049386cc71f893dff03209a9ef32b4d2f7e7c264bcf/pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl",hashes = {sha256 = "6c9024169becccf0cb470ada03ee578d7348c119a0d42af3dcf9eda96e3a247c"}}, + {name = "pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl",url = "https://files.pythonhosted.org/packages/2e/0f/4f2734688d98488782218ca61bcc118329bf5de05bb7fe3adc7dd79b0b86/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl",hashes = {sha256 = "26895a4268ae5a2849269f4991cdc97236e4b9c010e51137becf25182daac405"}}, + {name = "pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl",url = "https://files.pythonhosted.org/packages/ed/f2/ab385dbd94a052c62224b99cf99002eee99dbec40e10006c78575aead256/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl",hashes = {sha256 = "ca4df25762cf71308c446e33c9b1fdca2923a3f13de616e2a949f38bf21ff5a8"}}, + {name = "pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/fc/8e/e4f12afe1beeb9823bba5375f8f258df0cc61b056b0195fb1cf9f62a1a58/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl",hashes = {sha256 = "5a28fcedd762349519276c36634e71853b4541079cab4acaaac60c4421827308"}}, + {name = "pydantic_core-2.41.4-pp311-pypy311_pp73-win_amd64.whl",url = "https://files.pythonhosted.org/packages/48/f7/925f65d930802e3ea2eb4d5afa4cb8730c8dc0d2cb89a59dc4ed2fcb2d74/pydantic_core-2.41.4-pp311-pypy311_pp73-win_amd64.whl",hashes = {sha256 = "c173ddcd86afd2535e2b695217e82191580663a1d1928239f877f5a1649ef39f"}}, + {name = "pydantic_core-2.41.4-cp310-cp310-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/a7/3d/9b8ca77b0f76fcdbf8bc6b72474e264283f461284ca84ac3fde570c6c49a/pydantic_core-2.41.4-cp310-cp310-macosx_10_12_x86_64.whl",hashes = {sha256 = "2442d9a4d38f3411f22eb9dd0912b7cbf4b7d5b6c92c4173b75d3e1ccd84e36e"}}, + {name = "pydantic_core-2.41.4-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/59/92/b7b0fe6ed4781642232755cb7e56a86e2041e1292f16d9ae410a0ccee5ac/pydantic_core-2.41.4-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "30a9876226dda131a741afeab2702e2d127209bde3c65a2b8133f428bc5d006b"}}, + {name = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/52/8c/3eb872009274ffa4fb6a9585114e161aa1a0915af2896e2d441642929fe4/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "d55bbac04711e2980645af68b97d445cdbcce70e5216de444a6c4b6943ebcccd"}}, + {name = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",url = "https://files.pythonhosted.org/packages/f4/21/35adf4a753bcfaea22d925214a0c5b880792e3244731b3f3e6fec0d124f7/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",hashes = {sha256 = "e1d778fb7849a42d0ee5927ab0f7453bf9f85eef8887a546ec87db5ddb178945"}}, + {name = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/7d/d0/cdf7d126825e36d6e3f1eccf257da8954452934ede275a8f390eac775e89/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "1b65077a4693a98b90ec5ad8f203ad65802a1b9b6d4a7e48066925a7e1606706"}}, + {name = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/2e/1c/af1e6fd5ea596327308f9c8d1654e1285cc3d8de0d584a3c9d7705bf8a7c/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "62637c769dee16eddb7686bf421be48dfc2fae93832c25e25bc7242e698361ba"}}, + {name = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/d3/81/8cece29a6ef1b3a92f956ea6da6250d5b2d2e7e4d513dd3b4f0c7a83dfea/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "2dfe3aa529c8f501babf6e502936b9e8d4698502b2cfab41e17a028d91b1ac7b"}}, + {name = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl",url = "https://files.pythonhosted.org/packages/e3/37/a6a579f5fc2cd4d5521284a0ab6a426cc6463a7b3897aeb95b12f1ba607b/pydantic_core-2.41.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl",hashes = {sha256 = "ca2322da745bf2eeb581fc9ea3bbb31147702163ccbcbf12a3bb630e4bf05e1d"}}, + {name = "pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_aarch64.whl",url = "https://files.pythonhosted.org/packages/ae/03/505020dc5c54ec75ecba9f41119fd1e48f9e41e4629942494c4a8734ded1/pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_aarch64.whl",hashes = {sha256 = "e8cd3577c796be7231dcf80badcf2e0835a46665eaafd8ace124d886bab4d700"}}, + {name = "pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_armv7l.whl",url = "https://files.pythonhosted.org/packages/cb/5d/2c0d09fb53aa03bbd2a214d89ebfa6304be7df9ed86ee3dc7770257f41ee/pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_armv7l.whl",hashes = {sha256 = "1cae8851e174c83633f0833e90636832857297900133705ee158cf79d40f03e6"}}, + {name = "pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/ea/4b/c2c9c8f5e1f9c864b57d08539d9d3db160e00491c9f5ee90e1bfd905e644/pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_x86_64.whl",hashes = {sha256 = "a26d950449aae348afe1ac8be5525a00ae4235309b729ad4d3399623125b43c9"}}, + {name = "pydantic_core-2.41.4-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/28/c3/a74c1c37f49c0a02c89c7340fafc0ba816b29bd495d1a31ce1bdeacc6085/pydantic_core-2.41.4-cp310-cp310-win32.whl",hashes = {sha256 = "0cf2a1f599efe57fa0051312774280ee0f650e11152325e41dfd3018ef2c1b57"}}, + {name = "pydantic_core-2.41.4-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/d6/23/5dd5c1324ba80303368f7569e2e2e1a721c7d9eb16acb7eb7b7f85cb1be2/pydantic_core-2.41.4-cp310-cp310-win_amd64.whl",hashes = {sha256 = "a8c2e340d7e454dc3340d3d2e8f23558ebe78c98aa8f68851b04dcb7bc37abdc"}}, + {name = "pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/5d/d4/912e976a2dd0b49f31c98a060ca90b353f3b73ee3ea2fd0030412f6ac5ec/pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl",hashes = {sha256 = "1e5ab4fc177dd41536b3c32b2ea11380dd3d4619a385860621478ac2d25ceb00"}}, + {name = "pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/71/f0/66ec5a626c81eba326072d6ee2b127f8c139543f1bf609b4842978d37833/pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl",hashes = {sha256 = "3d88d0054d3fa11ce936184896bed3c1c5441d6fa483b498fac6a5d0dd6f64a9"}}, + {name = "pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/c4/af/625626278ca801ea0a658c2dcf290dc9f21bb383098e99e7c6a029fccfc0/pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "7b2a054a8725f05b4b6503357e0ac1c4e8234ad3b0c2ac130d6ffc66f0e170e2"}}, + {name = "pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl",url = "https://files.pythonhosted.org/packages/20/f6/2fba049f54e0f4975fef66be654c597a1d005320fa141863699180c7697d/pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl",hashes = {sha256 = "b0d9db5a161c99375a0c68c058e227bee1d89303300802601d76a3d01f74e258"}}, + {name = "pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl",url = "https://files.pythonhosted.org/packages/0e/80/65ab839a2dfcd3b949202f9d920c34f9de5a537c3646662bdf2f7d999680/pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl",hashes = {sha256 = "6273ea2c8ffdac7b7fda2653c49682db815aebf4a89243a6feccf5e36c18c347"}}, + {name = "pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl",url = "https://files.pythonhosted.org/packages/44/58/627565d3d182ce6dfda18b8e1c841eede3629d59c9d7cbc1e12a03aeb328/pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl",hashes = {sha256 = "4c973add636efc61de22530b2ef83a65f39b6d6f656df97f678720e20de26caa"}}, + {name = "pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl",url = "https://files.pythonhosted.org/packages/24/06/8a84711162ad5a5f19a88cead37cca81b4b1f294f46260ef7334ae4f24d3/pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl",hashes = {sha256 = "b69d1973354758007f46cf2d44a4f3d0933f10b6dc9bf15cf1356e037f6f731a"}}, + {name = "pydantic_core-2.41.4-pp310-pypy310_pp73-win_amd64.whl",url = "https://files.pythonhosted.org/packages/aa/8b/b7bb512a4682a2f7fbfae152a755d37351743900226d29bd953aaf870eaa/pydantic_core-2.41.4-pp310-pypy310_pp73-win_amd64.whl",hashes = {sha256 = "3619320641fd212aaf5997b6ca505e97540b7e16418f4a241f44cdf108ffb50d"}}, ] marker = "\"default\" in dependency_groups" @@ -1430,7 +1509,7 @@ sdist = {name = "packaging-25.0.tar.gz", url = "https://files.pythonhosted.org/p wheels = [ {name = "packaging-25.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl",hashes = {sha256 = "29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" [packages.tool.pdm] dependencies = [] @@ -1443,20 +1522,7 @@ sdist = {name = "typing_extensions-4.15.0.tar.gz", url = "https://files.pythonho wheels = [ {name = "typing_extensions-4.15.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl",hashes = {sha256 = "f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras" - -[packages.tool.pdm] -dependencies = [] - -[[packages]] -name = "colorama" -version = "0.4.6" -requires-python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -sdist = {name = "colorama-0.4.6.tar.gz", url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hashes = {sha256 = "08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}} -wheels = [ - {name = "colorama-0.4.6-py2.py3-none-any.whl",url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl",hashes = {sha256 = "4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}}, -] -marker = "\"default\" in dependency_groups or \"dev\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" [packages.tool.pdm] dependencies = [] @@ -1469,7 +1535,7 @@ sdist = {name = "huggingface_hub-0.35.3.tar.gz", url = "https://files.pythonhost wheels = [ {name = "huggingface_hub-0.35.3-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/31/a0/651f93d154cb72323358bf2bbae3e642bdb5d2f1bfc874d096f7cb159fa0/huggingface_hub-0.35.3-py3-none-any.whl",hashes = {sha256 = "0e3a01829c19d86d03793e4577816fe3bdfc1602ac62c7fb220d593d351224ba"}}, ] -marker = "\"default\" in dependency_groups" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" [packages.tool.pdm] dependencies = [ @@ -1483,6 +1549,19 @@ dependencies = [ "hf-xet<2.0.0,>=1.1.3; platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"arm64\" or platform_machine == \"aarch64\"", ] +[[packages]] +name = "colorama" +version = "0.4.6" +requires-python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +sdist = {name = "colorama-0.4.6.tar.gz", url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hashes = {sha256 = "08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}} +wheels = [ + {name = "colorama-0.4.6-py2.py3-none-any.whl",url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl",hashes = {sha256 = "4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}}, +] +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" + +[packages.tool.pdm] +dependencies = [] + [[packages]] name = "markdown-it-py" version = "3.0.0" @@ -1532,7 +1611,7 @@ sdist = {name = "requests-2.32.5.tar.gz", url = "https://files.pythonhosted.org/ wheels = [ {name = "requests-2.32.5-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl",hashes = {sha256 = "2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras or \"openai\" in extras or \"recommended\" in extras" [packages.tool.pdm] dependencies = [ @@ -1550,7 +1629,7 @@ sdist = {name = "urllib3-2.5.0.tar.gz", url = "https://files.pythonhosted.org/pa wheels = [ {name = "urllib3-2.5.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl",hashes = {sha256 = "e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras or \"openai\" in extras or \"recommended\" in extras" [packages.tool.pdm] dependencies = [] @@ -1563,7 +1642,7 @@ sdist = {name = "tqdm-4.67.1.tar.gz", url = "https://files.pythonhosted.org/pack wheels = [ {name = "tqdm-4.67.1-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl",hashes = {sha256 = "26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}}, ] -marker = "\"default\" in dependency_groups" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" [packages.tool.pdm] dependencies = [ @@ -1585,68 +1664,93 @@ dependencies = [] [[packages]] name = "charset-normalizer" -version = "3.4.3" +version = "3.4.4" requires-python = ">=3.7" -sdist = {name = "charset_normalizer-3.4.3.tar.gz", url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hashes = {sha256 = "6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14"}} -wheels = [ - {name = "charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl",url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl",hashes = {sha256 = "3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15"}}, - {name = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db"}}, - {name = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",hashes = {sha256 = "2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d"}}, - {name = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096"}}, - {name = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa"}}, - {name = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl",hashes = {sha256 = "ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049"}}, - {name = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0"}}, - {name = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl",hashes = {sha256 = "30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92"}}, - {name = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl",hashes = {sha256 = "d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16"}}, - {name = "charset_normalizer-3.4.3-cp314-cp314-win32.whl",url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl",hashes = {sha256 = "c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce"}}, - {name = "charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl",url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl",hashes = {sha256 = "73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c"}}, - {name = "charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl",url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl",hashes = {sha256 = "14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe"}}, - {name = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8"}}, - {name = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",hashes = {sha256 = "1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9"}}, - {name = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31"}}, - {name = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f"}}, - {name = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927"}}, - {name = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9"}}, - {name = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl",hashes = {sha256 = "bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5"}}, - {name = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc"}}, - {name = "charset_normalizer-3.4.3-cp313-cp313-win32.whl",url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl",hashes = {sha256 = "6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce"}}, - {name = "charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl",hashes = {sha256 = "cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef"}}, - {name = "charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl",url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl",hashes = {sha256 = "e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1"}}, - {name = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884"}}, - {name = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",hashes = {sha256 = "c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018"}}, - {name = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392"}}, - {name = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f"}}, - {name = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154"}}, - {name = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491"}}, - {name = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl",hashes = {sha256 = "cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93"}}, - {name = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f"}}, - {name = "charset_normalizer-3.4.3-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl",hashes = {sha256 = "fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37"}}, - {name = "charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl",hashes = {sha256 = "86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc"}}, - {name = "charset_normalizer-3.4.3-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl",hashes = {sha256 = "ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a"}}, - {name = "charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl",url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl",hashes = {sha256 = "b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b"}}, - {name = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64"}}, - {name = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",hashes = {sha256 = "00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91"}}, - {name = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f"}}, - {name = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07"}}, - {name = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30"}}, - {name = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14"}}, - {name = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl",hashes = {sha256 = "fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c"}}, - {name = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae"}}, - {name = "charset_normalizer-3.4.3-cp311-cp311-win32.whl",url = "https://files.pythonhosted.org/packages/64/d4/9eb4ff2c167edbbf08cdd28e19078bf195762e9bd63371689cab5ecd3d0d/charset_normalizer-3.4.3-cp311-cp311-win32.whl",hashes = {sha256 = "6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849"}}, - {name = "charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/f4/9c/996a4a028222e7761a96634d1820de8a744ff4327a00ada9c8942033089b/charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl",hashes = {sha256 = "31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c"}}, - {name = "charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl",url = "https://files.pythonhosted.org/packages/d6/98/f3b8013223728a99b908c9344da3aa04ee6e3fa235f19409033eda92fb78/charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl",hashes = {sha256 = "fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72"}}, - {name = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/21/40/5188be1e3118c82dcb7c2a5ba101b783822cfb413a0268ed3be0468532de/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe"}}, - {name = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",url = "https://files.pythonhosted.org/packages/37/60/5d0d74bc1e1380f0b72c327948d9c2aca14b46a9efd87604e724260f384c/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",hashes = {sha256 = "07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601"}}, - {name = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/85/9a/d891f63722d9158688de58d050c59dc3da560ea7f04f4c53e769de5140f5/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c"}}, - {name = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/65/1a/7425c952944a6521a9cfa7e675343f83fd82085b8af2b1373a2409c683dc/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2"}}, - {name = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/f0/c9/a2c9c2a355a8594ce2446085e2ec97fd44d323c684ff32042e2a6b718e1d/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0"}}, - {name = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/3b/38/20a1f44e4851aa1c9105d6e7110c9d020e093dfa5836d712a5f074a12bf7/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0"}}, - {name = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/a4/fa/384d2c0f57edad03d7bec3ebefb462090d8905b4ff5a2d2525f3bb711fac/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl",hashes = {sha256 = "02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0"}}, - {name = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/33/9e/eca49d35867ca2db336b6ca27617deed4653b97ebf45dfc21311ce473c37/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a"}}, - {name = "charset_normalizer-3.4.3-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/2a/91/26c3036e62dfe8de8061182d33be5025e2424002125c9500faff74a6735e/charset_normalizer-3.4.3-cp310-cp310-win32.whl",hashes = {sha256 = "d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f"}}, - {name = "charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/e2/c6/f05db471f81af1fa01839d44ae2a8bfeec8d2a8b4590f16c4e7393afd323/charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl",hashes = {sha256 = "c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669"}}, -] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras" +sdist = {name = "charset_normalizer-3.4.4.tar.gz", url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hashes = {sha256 = "94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a"}} +wheels = [ + {name = "charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl",url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl",hashes = {sha256 = "da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd"}}, + {name = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb"}}, + {name = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl",url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl",hashes = {sha256 = "74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e"}}, + {name = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",hashes = {sha256 = "752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14"}}, + {name = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191"}}, + {name = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838"}}, + {name = "charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6"}}, + {name = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl",hashes = {sha256 = "c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e"}}, + {name = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl",url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl",hashes = {sha256 = "47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c"}}, + {name = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090"}}, + {name = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl",hashes = {sha256 = "2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152"}}, + {name = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl",hashes = {sha256 = "799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828"}}, + {name = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl",hashes = {sha256 = "99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec"}}, + {name = "charset_normalizer-3.4.4-cp314-cp314-win32.whl",url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl",hashes = {sha256 = "f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9"}}, + {name = "charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl",url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl",hashes = {sha256 = "8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c"}}, + {name = "charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl",url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl",hashes = {sha256 = "de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2"}}, + {name = "charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl",url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl",hashes = {sha256 = "e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794"}}, + {name = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed"}}, + {name = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl",url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl",hashes = {sha256 = "3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72"}}, + {name = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",hashes = {sha256 = "81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328"}}, + {name = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede"}}, + {name = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894"}}, + {name = "charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1"}}, + {name = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490"}}, + {name = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl",url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl",hashes = {sha256 = "554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44"}}, + {name = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133"}}, + {name = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl",hashes = {sha256 = "c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3"}}, + {name = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl",hashes = {sha256 = "362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e"}}, + {name = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc"}}, + {name = "charset_normalizer-3.4.4-cp313-cp313-win32.whl",url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl",hashes = {sha256 = "9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac"}}, + {name = "charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl",hashes = {sha256 = "b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14"}}, + {name = "charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl",url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl",hashes = {sha256 = "542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2"}}, + {name = "charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl",url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl",hashes = {sha256 = "0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394"}}, + {name = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25"}}, + {name = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl",url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl",hashes = {sha256 = "74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef"}}, + {name = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",hashes = {sha256 = "f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d"}}, + {name = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8"}}, + {name = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86"}}, + {name = "charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a"}}, + {name = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f"}}, + {name = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl",url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl",hashes = {sha256 = "5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc"}}, + {name = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf"}}, + {name = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl",hashes = {sha256 = "d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15"}}, + {name = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl",hashes = {sha256 = "af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9"}}, + {name = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0"}}, + {name = "charset_normalizer-3.4.4-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl",hashes = {sha256 = "5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26"}}, + {name = "charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl",hashes = {sha256 = "a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525"}}, + {name = "charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl",hashes = {sha256 = "376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3"}}, + {name = "charset_normalizer-3.4.4-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl",hashes = {sha256 = "7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f"}}, + {name = "charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl",url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl",hashes = {sha256 = "6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8"}}, + {name = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0"}}, + {name = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl",url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl",hashes = {sha256 = "a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3"}}, + {name = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",hashes = {sha256 = "8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc"}}, + {name = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897"}}, + {name = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381"}}, + {name = "charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815"}}, + {name = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0"}}, + {name = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl",url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl",hashes = {sha256 = "277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161"}}, + {name = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4"}}, + {name = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl",hashes = {sha256 = "0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89"}}, + {name = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl",hashes = {sha256 = "9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569"}}, + {name = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224"}}, + {name = "charset_normalizer-3.4.4-cp311-cp311-win32.whl",url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl",hashes = {sha256 = "eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a"}}, + {name = "charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl",hashes = {sha256 = "5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016"}}, + {name = "charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl",url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl",hashes = {sha256 = "65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1"}}, + {name = "charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl",url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl",hashes = {sha256 = "e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d"}}, + {name = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8"}}, + {name = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl",url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl",hashes = {sha256 = "027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad"}}, + {name = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",hashes = {sha256 = "f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8"}}, + {name = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",hashes = {sha256 = "798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d"}}, + {name = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313"}}, + {name = "charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e"}}, + {name = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93"}}, + {name = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl",url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl",hashes = {sha256 = "244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0"}}, + {name = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl",url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl",hashes = {sha256 = "64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84"}}, + {name = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl",hashes = {sha256 = "faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e"}}, + {name = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl",url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl",hashes = {sha256 = "6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db"}}, + {name = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6"}}, + {name = "charset_normalizer-3.4.4-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl",hashes = {sha256 = "f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f"}}, + {name = "charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl",hashes = {sha256 = "a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d"}}, + {name = "charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl",url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl",hashes = {sha256 = "cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69"}}, +] +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras or \"openai\" in extras or \"recommended\" in extras" [packages.tool.pdm] dependencies = [] @@ -1659,7 +1763,7 @@ sdist = {name = "dill-0.4.0.tar.gz", url = "https://files.pythonhosted.org/packa wheels = [ {name = "dill-0.4.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-py3-none-any.whl",hashes = {sha256 = "44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049"}}, ] -marker = "\"default\" in dependency_groups" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" [packages.tool.pdm] dependencies = [] @@ -1685,7 +1789,7 @@ sdist = {name = "filelock-3.20.0.tar.gz", url = "https://files.pythonhosted.org/ wheels = [ {name = "filelock-3.20.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl",hashes = {sha256 = "339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras or \"openai\" in extras or \"recommended\" in extras" [packages.tool.pdm] dependencies = [] @@ -1698,7 +1802,7 @@ sdist = {name = "fsspec-2025.9.0.tar.gz", url = "https://files.pythonhosted.org/ wheels = [ {name = "fsspec-2025.9.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/47/71/70db47e4f6ce3e5c37a607355f80da8860a33226be640226ac52cb05ef2e/fsspec-2025.9.0-py3-none-any.whl",hashes = {sha256 = "530dc2a2af60a414a832059574df4a6e10cce927f6f4a78209390fe38955cfb7"}}, ] -marker = "\"default\" in dependency_groups" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" [packages.tool.pdm] dependencies = [] @@ -1812,7 +1916,7 @@ wheels = [ {name = "aiohttp-3.13.0-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/23/ba/47fd065510a8bfab5d5f6e1d97c0de672447c0a941c5021298bd7210afc3/aiohttp-3.13.0-cp310-cp310-win32.whl",hashes = {sha256 = "3b64f22fbb6dcd5663de5ef2d847a5638646ef99112503e6f7704bdecb0d1c4d"}}, {name = "aiohttp-3.13.0-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/c4/38/f5385cb79afa1f31bcaa3625a9e8d849b782edaeac09f894f46439e006a1/aiohttp-3.13.0-cp310-cp310-win_amd64.whl",hashes = {sha256 = "f8d877aa60d80715b2afc565f0f1aea66565824c229a2d065b31670e09fed6d7"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" [packages.tool.pdm] dependencies = [ @@ -1960,7 +2064,7 @@ wheels = [ {name = "multidict-6.7.0-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/00/35/f6a637ea2c75f0d3b7c7d41b1189189acff0d9deeb8b8f35536bb30f5e33/multidict-6.7.0-cp310-cp310-win_amd64.whl",hashes = {sha256 = "b15b3afff74f707b9275d5ba6a91ae8f6429c3ffb29bbfd216b0b375a56f13d7"}}, {name = "multidict-6.7.0-cp310-cp310-win_arm64.whl",url = "https://files.pythonhosted.org/packages/e7/b8/f7bf8329b39893d02d9d95cf610c75885d12fc0f402b1c894e1c8e01c916/multidict-6.7.0-cp310-cp310-win_arm64.whl",hashes = {sha256 = "4b73189894398d59131a66ff157837b1fafea9974be486d036bb3d32331fdbf0"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" [packages.tool.pdm] dependencies = [ @@ -1997,7 +2101,7 @@ wheels = [ {name = "hf_xet-1.1.10-cp37-abi3-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/2c/3d/ab7109e607ed321afaa690f557a9ada6d6d164ec852fd6bf9979665dc3d6/hf_xet-1.1.10-cp37-abi3-musllinux_1_2_x86_64.whl",hashes = {sha256 = "f900481cf6e362a6c549c61ff77468bd59d6dd082f3170a36acfef2eb6a6793f"}}, {name = "hf_xet-1.1.10-cp37-abi3-win_amd64.whl",url = "https://files.pythonhosted.org/packages/ee/0e/471f0a21db36e71a2f1752767ad77e92d8cde24e974e03d662931b1305ec/hf_xet-1.1.10-cp37-abi3-win_amd64.whl",hashes = {sha256 = "5f54b19cc347c13235ae7ee98b330c26dd65ef1df47e5316ffb1e87713ca7045"}}, ] -marker = "(platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"arm64\" or platform_machine == \"aarch64\") and \"default\" in dependency_groups" +marker = "\"default\" in dependency_groups and (platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"arm64\" or platform_machine == \"aarch64\") or \"all\" in extras and (platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"arm64\" or platform_machine == \"aarch64\") or \"dev\" in extras and (platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"arm64\" or platform_machine == \"aarch64\") or \"multimodal\" in extras and (platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"arm64\" or platform_machine == \"aarch64\")" [packages.tool.pdm] dependencies = [] @@ -2030,13 +2134,13 @@ dependencies = [] [[packages]] name = "idna" -version = "3.10" -requires-python = ">=3.6" -sdist = {name = "idna-3.10.tar.gz", url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hashes = {sha256 = "12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}} +version = "3.11" +requires-python = ">=3.8" +sdist = {name = "idna-3.11.tar.gz", url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hashes = {sha256 = "795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}} wheels = [ - {name = "idna-3.10-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl",hashes = {sha256 = "946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}}, + {name = "idna-3.11-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl",hashes = {sha256 = "771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras or \"openai\" in extras or \"recommended\" in extras" [packages.tool.pdm] dependencies = [] @@ -2212,11 +2316,11 @@ dependencies = [ [[packages]] name = "virtualenv" -version = "20.34.0" +version = "20.35.3" requires-python = ">=3.8" -sdist = {name = "virtualenv-20.34.0.tar.gz", url = "https://files.pythonhosted.org/packages/1c/14/37fcdba2808a6c615681cd216fecae00413c9dab44fb2e57805ecf3eaee3/virtualenv-20.34.0.tar.gz", hashes = {sha256 = "44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a"}} +sdist = {name = "virtualenv-20.35.3.tar.gz", url = "https://files.pythonhosted.org/packages/a4/d5/b0ccd381d55c8f45d46f77df6ae59fbc23d19e901e2d523395598e5f4c93/virtualenv-20.35.3.tar.gz", hashes = {sha256 = "4f1a845d131133bdff10590489610c98c168ff99dc75d6c96853801f7f67af44"}} wheels = [ - {name = "virtualenv-20.34.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/76/06/04c8e804f813cf972e3262f3f8584c232de64f0cde9f703b46cf53a45090/virtualenv-20.34.0-py3-none-any.whl",hashes = {sha256 = "341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026"}}, + {name = "virtualenv-20.35.3-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/27/73/d9a94da0e9d470a543c1b9d3ccbceb0f59455983088e727b8a1824ed90fb/virtualenv-20.35.3-py3-none-any.whl",hashes = {sha256 = "63d106565078d8c8d0b206d48080f938a8b25361e19432d2c9db40d2899c810a"}}, ] marker = "\"dev\" in extras" @@ -2374,7 +2478,7 @@ wheels = [ {name = "yarl-1.22.0-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/62/46/94c76196642dbeae634c7a61ba3da88cd77bed875bf6e4a8bed037505aa6/yarl-1.22.0-cp310-cp310-win_amd64.whl",hashes = {sha256 = "cb95a9b1adaa48e41815a55ae740cfda005758104049a640a398120bf02515ca"}}, {name = "yarl-1.22.0-cp310-cp310-win_arm64.whl",url = "https://files.pythonhosted.org/packages/af/af/7df4f179d3b1a6dcb9a4bd2ffbc67642746fcafdb62580e66876ce83fff4/yarl-1.22.0-cp310-cp310-win_arm64.whl",hashes = {sha256 = "b85b982afde6df99ecc996990d4ad7ccbdbb70e2a4ba4de0aecde5922ba98a0b"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" [packages.tool.pdm] dependencies = [ @@ -2496,18 +2600,18 @@ wheels = [ {name = "propcache-0.4.1-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/a6/67/354aac4e0603a15f76439caf0427781bcd6797f370377f75a642133bc954/propcache-0.4.1-cp310-cp310-win_amd64.whl",hashes = {sha256 = "1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37"}}, {name = "propcache-0.4.1-cp310-cp310-win_arm64.whl",url = "https://files.pythonhosted.org/packages/e0/e1/74e55b9fd1a4c209ff1a9a824bf6c8b3d1fc5a1ac3eabe23462637466785/propcache-0.4.1-cp310-cp310-win_arm64.whl",hashes = {sha256 = "d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" [packages.tool.pdm] dependencies = [] [[packages]] name = "aiofiles" -version = "24.1.0" -requires-python = ">=3.8" -sdist = {name = "aiofiles-24.1.0.tar.gz", url = "https://files.pythonhosted.org/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hashes = {sha256 = "22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"}} +version = "25.1.0" +requires-python = ">=3.9" +sdist = {name = "aiofiles-25.1.0.tar.gz", url = "https://files.pythonhosted.org/packages/41/c3/534eac40372d8ee36ef40df62ec129bee4fdb5ad9706e58a29be53b2c970/aiofiles-25.1.0.tar.gz", hashes = {sha256 = "a8d728f0a29de45dc521f18f07297428d56992a742f0cd2701ba86e44d23d5b2"}} wheels = [ - {name = "aiofiles-24.1.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl",hashes = {sha256 = "b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5"}}, + {name = "aiofiles-25.1.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/bc/8a/340a1555ae33d7354dbca4faa54948d76d89a27ceef032c8c3bc661d003e/aiofiles-25.1.0-py3-none-any.whl",hashes = {sha256 = "abe311e527c862958650f9438e859c1fa7568a141b22abcd015e120e86a85695"}}, ] marker = "\"default\" in dependency_groups" @@ -2522,7 +2626,7 @@ sdist = {name = "aiohappyeyeballs-2.6.1.tar.gz", url = "https://files.pythonhost wheels = [ {name = "aiohappyeyeballs-2.6.1-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl",hashes = {sha256 = "f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" [packages.tool.pdm] dependencies = [] @@ -2550,7 +2654,7 @@ sdist = {name = "aiosignal-1.4.0.tar.gz", url = "https://files.pythonhosted.org/ wheels = [ {name = "aiosignal-1.4.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl",hashes = {sha256 = "053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" [packages.tool.pdm] dependencies = [ @@ -2678,7 +2782,7 @@ wheels = [ {name = "frozenlist-1.8.0-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/66/aa/3f04523fb189a00e147e60c5b2205126118f216b0aa908035c45336e27e4/frozenlist-1.8.0-cp310-cp310-win_amd64.whl",hashes = {sha256 = "667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6"}}, {name = "frozenlist-1.8.0-cp310-cp310-win_arm64.whl",url = "https://files.pythonhosted.org/packages/39/75/1135feecdd7c336938bd55b4dc3b0dfc46d85b9be12ef2628574b28de776/frozenlist-1.8.0-cp310-cp310-win_arm64.whl",hashes = {sha256 = "80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" [packages.tool.pdm] dependencies = [] @@ -2706,7 +2810,7 @@ sdist = {name = "attrs-25.4.0.tar.gz", url = "https://files.pythonhosted.org/pac wheels = [ {name = "attrs-25.4.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl",hashes = {sha256 = "adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" [packages.tool.pdm] dependencies = [] @@ -2728,11 +2832,11 @@ dependencies = [ [[packages]] name = "cachetools" -version = "6.2.0" +version = "6.2.1" requires-python = ">=3.9" -sdist = {name = "cachetools-6.2.0.tar.gz", url = "https://files.pythonhosted.org/packages/9d/61/e4fad8155db4a04bfb4734c7c8ff0882f078f24294d42798b3568eb63bff/cachetools-6.2.0.tar.gz", hashes = {sha256 = "38b328c0889450f05f5e120f56ab68c8abaf424e1275522b138ffc93253f7e32"}} +sdist = {name = "cachetools-6.2.1.tar.gz", url = "https://files.pythonhosted.org/packages/cc/7e/b975b5814bd36faf009faebe22c1072a1fa1168db34d285ef0ba071ad78c/cachetools-6.2.1.tar.gz", hashes = {sha256 = "3f391e4bd8f8bf0931169baf7456cc822705f4e2a31f840d218f445b9a854201"}} wheels = [ - {name = "cachetools-6.2.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/6c/56/3124f61d37a7a4e7cc96afc5492c78ba0cb551151e530b54669ddd1436ef/cachetools-6.2.0-py3-none-any.whl",hashes = {sha256 = "1c76a8960c0041fcc21097e357f882197c79da0dbff766e7317890a65d7d8ba6"}}, + {name = "cachetools-6.2.1-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/96/c5/1e741d26306c42e2bf6ab740b2202872727e0f606033c9dd713f8b93f5a8/cachetools-6.2.1-py3-none-any.whl",hashes = {sha256 = "09868944b6dde876dfd44e1d47e18484541eaf12f26f29b7af91b26cc892d701"}}, ] marker = "\"dev\" in extras" @@ -2747,7 +2851,7 @@ sdist = {name = "certifi-2025.10.5.tar.gz", url = "https://files.pythonhosted.or wheels = [ {name = "certifi-2025.10.5-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl",hashes = {sha256 = "0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras or \"openai\" in extras or \"recommended\" in extras" [packages.tool.pdm] dependencies = [] @@ -2889,7 +2993,7 @@ sdist = {name = "h11-0.16.0.tar.gz", url = "https://files.pythonhosted.org/packa wheels = [ {name = "h11-0.16.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl",hashes = {sha256 = "63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" [packages.tool.pdm] dependencies = [] @@ -2909,38 +3013,45 @@ dependencies = [] [[packages]] name = "httptools" -version = "0.6.4" -requires-python = ">=3.8.0" -sdist = {name = "httptools-0.6.4.tar.gz", url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hashes = {sha256 = "4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c"}} -wheels = [ - {name = "httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl",url = "https://files.pythonhosted.org/packages/94/a3/9fe9ad23fd35f7de6b91eeb60848986058bd8b5a5c1e256f5860a160cc3e/httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl",hashes = {sha256 = "ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660"}}, - {name = "httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/ea/d9/82d5e68bab783b632023f2fa31db20bebb4e89dfc4d2293945fd68484ee4/httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083"}}, - {name = "httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/96/c1/cb499655cbdbfb57b577734fde02f6fa0bbc3fe9fb4d87b742b512908dff/httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3"}}, - {name = "httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/af/71/ee32fd358f8a3bb199b03261f10921716990808a675d8160b5383487a317/httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071"}}, - {name = "httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/8a/0a/0d4df132bfca1507114198b766f1737d57580c9ad1cf93c1ff673e3387be/httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5"}}, - {name = "httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/1e/6a/787004fdef2cabea27bad1073bf6a33f2437b4dbd3b6fb4a9d71172b1c7c/httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0"}}, - {name = "httptools-0.6.4-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/4d/dc/7decab5c404d1d2cdc1bb330b1bf70e83d6af0396fd4fc76fc60c0d522bf/httptools-0.6.4-cp313-cp313-win_amd64.whl",hashes = {sha256 = "28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8"}}, - {name = "httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl",url = "https://files.pythonhosted.org/packages/bb/0e/d0b71465c66b9185f90a091ab36389a7352985fe857e352801c39d6127c8/httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl",hashes = {sha256 = "df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2"}}, - {name = "httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/e2/b8/412a9bb28d0a8988de3296e01efa0bd62068b33856cdda47fe1b5e890954/httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44"}}, - {name = "httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/9b/01/6fb20be3196ffdc8eeec4e653bc2a275eca7f36634c86302242c4fbb2760/httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1"}}, - {name = "httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/f7/d8/b644c44acc1368938317d76ac991c9bba1166311880bcc0ac297cb9d6bd7/httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2"}}, - {name = "httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/52/d8/254d16a31d543073a0e57f1c329ca7378d8924e7e292eda72d0064987486/httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81"}}, - {name = "httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/5f/3c/4aee161b4b7a971660b8be71a92c24d6c64372c1ab3ae7f366b3680df20f/httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f"}}, - {name = "httptools-0.6.4-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/12/b7/5cae71a8868e555f3f67a50ee7f673ce36eac970f029c0c5e9d584352961/httptools-0.6.4-cp312-cp312-win_amd64.whl",hashes = {sha256 = "db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970"}}, - {name = "httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl",url = "https://files.pythonhosted.org/packages/7b/26/bb526d4d14c2774fe07113ca1db7255737ffbb119315839af2065abfdac3/httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl",hashes = {sha256 = "f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069"}}, - {name = "httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/a6/17/3e0d3e9b901c732987a45f4f94d4e2c62b89a041d93db89eafb262afd8d5/httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a"}}, - {name = "httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/b7/24/0fe235d7b69c42423c7698d086d4db96475f9b50b6ad26a718ef27a0bce6/httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975"}}, - {name = "httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/b1/2f/205d1f2a190b72da6ffb5f41a3736c26d6fa7871101212b15e9b5cd8f61d/httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636"}}, - {name = "httptools-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/6e/4c/d09ce0eff09057a206a74575ae8f1e1e2f0364d20e2442224f9e6612c8b9/httptools-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721"}}, - {name = "httptools-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/3e/d2/84c9e23edbccc4a4c6f96a1b8d99dfd2350289e94f00e9ccc7aadde26fb5/httptools-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988"}}, - {name = "httptools-0.6.4-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/d0/46/4d8e7ba9581416de1c425b8264e2cadd201eb709ec1584c381f3e98f51c1/httptools-0.6.4-cp311-cp311-win_amd64.whl",hashes = {sha256 = "288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17"}}, - {name = "httptools-0.6.4-cp310-cp310-macosx_10_9_universal2.whl",url = "https://files.pythonhosted.org/packages/3b/6f/972f8eb0ea7d98a1c6be436e2142d51ad2a64ee18e02b0e7ff1f62171ab1/httptools-0.6.4-cp310-cp310-macosx_10_9_universal2.whl",hashes = {sha256 = "3c73ce323711a6ffb0d247dcd5a550b8babf0f757e86a52558fe5b86d6fefcc0"}}, - {name = "httptools-0.6.4-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/6a/b0/17c672b4bc5c7ba7f201eada4e96c71d0a59fbc185e60e42580093a86f21/httptools-0.6.4-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "345c288418f0944a6fe67be8e6afa9262b18c7626c3ef3c28adc5eabc06a68da"}}, - {name = "httptools-0.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/92/5e/b4a826fe91971a0b68e8c2bd4e7db3e7519882f5a8ccdb1194be2b3ab98f/httptools-0.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "deee0e3343f98ee8047e9f4c5bc7cedbf69f5734454a94c38ee829fb2d5fa3c1"}}, - {name = "httptools-0.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/b0/51/ce61e531e40289a681a463e1258fa1e05e0be54540e40d91d065a264cd8f/httptools-0.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "ca80b7485c76f768a3bc83ea58373f8db7b015551117375e4918e2aa77ea9b50"}}, - {name = "httptools-0.6.4-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/ea/9e/270b7d767849b0c96f275c695d27ca76c30671f8eb8cc1bab6ced5c5e1d0/httptools-0.6.4-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "90d96a385fa941283ebd231464045187a31ad932ebfa541be8edf5b3c2328959"}}, - {name = "httptools-0.6.4-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/81/86/ced96e3179c48c6f656354e106934e65c8963d48b69be78f355797f0e1b3/httptools-0.6.4-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "59e724f8b332319e2875efd360e61ac07f33b492889284a3e05e6d13746876f4"}}, - {name = "httptools-0.6.4-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/75/73/187a3f620ed3175364ddb56847d7a608a6fc42d551e133197098c0143eca/httptools-0.6.4-cp310-cp310-win_amd64.whl",hashes = {sha256 = "c26f313951f6e26147833fc923f78f95604bbec812a43e5ee37f26dc9e5a686c"}}, +version = "0.7.1" +requires-python = ">=3.9" +sdist = {name = "httptools-0.7.1.tar.gz", url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hashes = {sha256 = "abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9"}} +wheels = [ + {name = "httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl",url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl",hashes = {sha256 = "c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270"}}, + {name = "httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3"}}, + {name = "httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1"}}, + {name = "httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b"}}, + {name = "httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl",hashes = {sha256 = "f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60"}}, + {name = "httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl",hashes = {sha256 = "7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca"}}, + {name = "httptools-0.7.1-cp314-cp314-win_amd64.whl",url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl",hashes = {sha256 = "cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96"}}, + {name = "httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl",url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl",hashes = {sha256 = "6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3"}}, + {name = "httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca"}}, + {name = "httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c"}}, + {name = "httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66"}}, + {name = "httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346"}}, + {name = "httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650"}}, + {name = "httptools-0.7.1-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl",hashes = {sha256 = "322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6"}}, + {name = "httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl",url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl",hashes = {sha256 = "38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5"}}, + {name = "httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5"}}, + {name = "httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03"}}, + {name = "httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2"}}, + {name = "httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362"}}, + {name = "httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c"}}, + {name = "httptools-0.7.1-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl",hashes = {sha256 = "3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321"}}, + {name = "httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl",url = "https://files.pythonhosted.org/packages/9c/08/17e07e8d89ab8f343c134616d72eebfe03798835058e2ab579dcc8353c06/httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl",hashes = {sha256 = "474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657"}}, + {name = "httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/aa/06/c9c1b41ff52f16aee526fd10fbda99fa4787938aa776858ddc4a1ea825ec/httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70"}}, + {name = "httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/cc/cc/10935db22fda0ee34c76f047590ca0a8bd9de531406a3ccb10a90e12ea21/httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df"}}, + {name = "httptools-0.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/0e/84/875382b10d271b0c11aa5d414b44f92f8dd53e9b658aec338a79164fa548/httptools-0.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e"}}, + {name = "httptools-0.7.1-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/30/e1/44f89b280f7e46c0b1b2ccee5737d46b3bb13136383958f20b580a821ca0/httptools-0.7.1-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274"}}, + {name = "httptools-0.7.1-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/6f/7e/b9287763159e700e335028bc1824359dc736fa9b829dacedace91a39b37e/httptools-0.7.1-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec"}}, + {name = "httptools-0.7.1-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/b3/07/5b614f592868e07f5c94b1f301b5e14a21df4e8076215a3bccb830a687d8/httptools-0.7.1-cp311-cp311-win_amd64.whl",hashes = {sha256 = "135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb"}}, + {name = "httptools-0.7.1-cp310-cp310-macosx_10_9_universal2.whl",url = "https://files.pythonhosted.org/packages/c7/e5/c07e0bcf4ec8db8164e9f6738c048b2e66aabf30e7506f440c4cc6953f60/httptools-0.7.1-cp310-cp310-macosx_10_9_universal2.whl",hashes = {sha256 = "11d01b0ff1fe02c4c32d60af61a4d613b74fad069e47e06e9067758c01e9ac78"}}, + {name = "httptools-0.7.1-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/7e/4f/35e3a63f863a659f92ffd92bef131f3e81cf849af26e6435b49bd9f6f751/httptools-0.7.1-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "84d86c1e5afdc479a6fdabf570be0d3eb791df0ae727e8dbc0259ed1249998d4"}}, + {name = "httptools-0.7.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/f5/71/b0a9193641d9e2471ac541d3b1b869538a5fb6419d52fd2669fa9c79e4b8/httptools-0.7.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "c8c751014e13d88d2be5f5f14fc8b89612fcfa92a9cc480f2bc1598357a23a05"}}, + {name = "httptools-0.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/eb/d9/2e34811397b76718750fea44658cb0205b84566e895192115252e008b152/httptools-0.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "654968cb6b6c77e37b832a9be3d3ecabb243bbe7a0b8f65fbc5b6b04c8fcabed"}}, + {name = "httptools-0.7.1-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/01/3f/a04626ebeacc489866bb4d82362c0657b2262bef381d68310134be7f40bb/httptools-0.7.1-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "b580968316348b474b020edf3988eecd5d6eec4634ee6561e72ae3a2a0e00a8a"}}, + {name = "httptools-0.7.1-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/a5/99/adcd4f66614db627b587627c8ad6f4c55f18881549bab10ecf180562e7b9/httptools-0.7.1-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "d496e2f5245319da9d764296e86c5bb6fcf0cf7a8806d3d000717a889c8c0b7b"}}, + {name = "httptools-0.7.1-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/d5/72/ec8fc904a8fd30ba022dfa85f3bbc64c3c7cd75b669e24242c0658e22f3c/httptools-0.7.1-cp310-cp310-win_amd64.whl",hashes = {sha256 = "cbf8317bfccf0fed3b5680c559d3459cccf1abe9039bfa159e62e391c7270568"}}, ] marker = "\"default\" in dependency_groups" @@ -2981,7 +3092,7 @@ sdist = {name = "jinja2-3.1.6.tar.gz", url = "https://files.pythonhosted.org/pac wheels = [ {name = "jinja2-3.1.6-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl",hashes = {sha256 = "85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" [packages.tool.pdm] dependencies = [ @@ -3200,7 +3311,7 @@ wheels = [ {name = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl",hashes = {sha256 = "c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"}}, {name = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl",url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl",hashes = {sha256 = "e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" [packages.tool.pdm] dependencies = [] @@ -3246,7 +3357,7 @@ wheels = [ {name = "multiprocess-0.70.16-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/0f/ab/1e6e8009e380e22254ff539ebe117861e5bdb3bff1fc977920972237c6c7/multiprocess-0.70.16-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl",hashes = {sha256 = "d951bed82c8f73929ac82c61f01a7b5ce8f3e5ef40f5b52553b4f547ce2b08ec"}}, {name = "multiprocess-0.70.16-py310-none-any.whl",url = "https://files.pythonhosted.org/packages/bc/f7/7ec7fddc92e50714ea3745631f79bd9c96424cb2702632521028e57d3a36/multiprocess-0.70.16-py310-none-any.whl",hashes = {sha256 = "c4a9944c67bd49f823687463660a2d6daae94c289adff97e0f9d696ba6371d02"}}, ] -marker = "\"default\" in dependency_groups" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" [packages.tool.pdm] dependencies = [ @@ -3274,7 +3385,7 @@ sdist = {name = "networkx-3.5.tar.gz", url = "https://files.pythonhosted.org/pac wheels = [ {name = "networkx-3.5-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl",hashes = {sha256 = "0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec"}}, ] -marker = "python_version ~= \"3.12\" and \"default\" in dependency_groups" +marker = "\"default\" in dependency_groups and python_version ~= \"3.12\" or \"all\" in extras and python_version ~= \"3.12\" or \"dev\" in extras and python_version ~= \"3.12\" or \"multimodal\" in extras and python_version ~= \"3.12\"" [packages.tool.pdm] dependencies = [] @@ -3334,7 +3445,7 @@ wheels = [ {name = "pyarrow-21.0.0-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/a6/3e/99473332ac40278f196e105ce30b79ab8affab12f6194802f2593d6b0be2/pyarrow-21.0.0-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "9b0b14b49ac10654332a805aedfc0147fb3469cbf8ea951b3d040dab12372594"}}, {name = "pyarrow-21.0.0-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/7b/f5/c372ef60593d713e8bfbb7e0c743501605f0ad00719146dc075faf11172b/pyarrow-21.0.0-cp310-cp310-win_amd64.whl",hashes = {sha256 = "9d9f8bcb4c3be7738add259738abdeddc363de1b80e3310e04067aa1ca596634"}}, ] -marker = "\"default\" in dependency_groups" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" [packages.tool.pdm] dependencies = [] @@ -3380,18 +3491,18 @@ dependencies = [] [[packages]] name = "pyproject-api" -version = "1.9.1" -requires-python = ">=3.9" -sdist = {name = "pyproject_api-1.9.1.tar.gz", url = "https://files.pythonhosted.org/packages/19/fd/437901c891f58a7b9096511750247535e891d2d5a5a6eefbc9386a2b41d5/pyproject_api-1.9.1.tar.gz", hashes = {sha256 = "43c9918f49daab37e302038fc1aed54a8c7a91a9fa935d00b9a485f37e0f5335"}} +version = "1.10.0" +requires-python = ">=3.10" +sdist = {name = "pyproject_api-1.10.0.tar.gz", url = "https://files.pythonhosted.org/packages/45/7b/c0e1333b61d41c69e59e5366e727b18c4992688caf0de1be10b3e5265f6b/pyproject_api-1.10.0.tar.gz", hashes = {sha256 = "40c6f2d82eebdc4afee61c773ed208c04c19db4c4a60d97f8d7be3ebc0bbb330"}} wheels = [ - {name = "pyproject_api-1.9.1-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/ef/e6/c293c06695d4a3ab0260ef124a74ebadba5f4c511ce3a4259e976902c00b/pyproject_api-1.9.1-py3-none-any.whl",hashes = {sha256 = "7d6238d92f8962773dd75b5f0c4a6a27cce092a14b623b811dba656f3b628948"}}, + {name = "pyproject_api-1.10.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/54/cc/cecf97be298bee2b2a37dd360618c819a2a7fd95251d8e480c1f0eb88f3b/pyproject_api-1.10.0-py3-none-any.whl",hashes = {sha256 = "8757c41a79c0f4ab71b99abed52b97ecf66bd20b04fa59da43b5840bac105a09"}}, ] marker = "\"dev\" in extras" [packages.tool.pdm] dependencies = [ "packaging>=25", - "tomli>=2.2.1; python_version < \"3.11\"", + "tomli>=2.3; python_version < \"3.11\"", ] [[packages]] @@ -3492,7 +3603,7 @@ sdist = {name = "sympy-1.14.0.tar.gz", url = "https://files.pythonhosted.org/pac wheels = [ {name = "sympy-1.14.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl",hashes = {sha256 = "e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5"}}, ] -marker = "\"default\" in dependency_groups" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" [packages.tool.pdm] dependencies = [ @@ -3506,7 +3617,7 @@ sdist = {name = "mpmath-1.3.0.tar.gz", url = "https://files.pythonhosted.org/pac wheels = [ {name = "mpmath-1.3.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl",hashes = {sha256 = "a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"}}, ] -marker = "\"default\" in dependency_groups" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" [packages.tool.pdm] dependencies = [] @@ -3779,7 +3890,7 @@ sdist = {name = "anyio-4.11.0.tar.gz", url = "https://files.pythonhosted.org/pac wheels = [ {name = "anyio-4.11.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl",hashes = {sha256 = "0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" [packages.tool.pdm] dependencies = [ @@ -3797,7 +3908,7 @@ sdist = {name = "sniffio-1.3.1.tar.gz", url = "https://files.pythonhosted.org/pa wheels = [ {name = "sniffio-1.3.1-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl",hashes = {sha256 = "2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}}, ] -marker = "\"default\" in dependency_groups or \"dev\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" [packages.tool.pdm] dependencies = [] @@ -3884,7 +3995,7 @@ wheels = [ {name = "pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/10/ae/89b3283800ab58f7af2952704078555fa60c807fff764395bb57ea0b0dbd/pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4"}}, {name = "pandas-2.3.3-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/85/72/530900610650f54a35a19476eca5104f38555afccda1aa11a92ee14cb21d/pandas-2.3.3-cp310-cp310-win_amd64.whl",hashes = {sha256 = "503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826"}}, ] -marker = "\"default\" in dependency_groups" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" [packages.tool.pdm] dependencies = [ @@ -3904,7 +4015,7 @@ sdist = {name = "python-dateutil-2.9.0.post0.tar.gz", url = "https://files.pytho wheels = [ {name = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl",url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl",hashes = {sha256 = "a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}}, ] -marker = "\"default\" in dependency_groups" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" [packages.tool.pdm] dependencies = [ @@ -3918,7 +4029,7 @@ sdist = {name = "pytz-2025.2.tar.gz", url = "https://files.pythonhosted.org/pack wheels = [ {name = "pytz-2025.2-py2.py3-none-any.whl",url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl",hashes = {sha256 = "5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}}, ] -marker = "\"default\" in dependency_groups" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" [packages.tool.pdm] dependencies = [] @@ -3931,7 +4042,7 @@ sdist = {name = "six-1.17.0.tar.gz", url = "https://files.pythonhosted.org/packa wheels = [ {name = "six-1.17.0-py2.py3-none-any.whl",url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl",hashes = {sha256 = "4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}}, ] -marker = "\"default\" in dependency_groups" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" [packages.tool.pdm] dependencies = [] @@ -3944,7 +4055,7 @@ sdist = {name = "tzdata-2025.2.tar.gz", url = "https://files.pythonhosted.org/pa wheels = [ {name = "tzdata-2025.2-py2.py3-none-any.whl",url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl",hashes = {sha256 = "1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}}, ] -marker = "\"default\" in dependency_groups" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" [packages.tool.pdm] dependencies = [] @@ -4215,7 +4326,7 @@ wheels = [ {name = "xxhash-3.6.0-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/bb/cc/53f87e8b5871a6eb2ff7e89c48c66093bda2be52315a8161ddc54ea550c4/xxhash-3.6.0-cp310-cp310-win_amd64.whl",hashes = {sha256 = "e82da5670f2d0d98950317f82a0e4a0197150ff19a6df2ba40399c2a3b9ae5fb"}}, {name = "xxhash-3.6.0-cp310-cp310-win_arm64.whl",url = "https://files.pythonhosted.org/packages/9f/00/60f9ea3bb697667a14314d7269956f58bf56bb73864f8f8d52a3c2535e9a/xxhash-3.6.0-cp310-cp310-win_arm64.whl",hashes = {sha256 = "4a082ffff8c6ac07707fb6b671caf7c6e020c75226c561830b73d862060f281d"}}, ] -marker = "\"default\" in dependency_groups" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" [packages.tool.pdm] dependencies = [] @@ -4251,7 +4362,7 @@ wheels = [ {name = "numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543"}}, {name = "numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl",url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl",hashes = {sha256 = "d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00"}}, ] -marker = "python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"default\" in dependency_groups or python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"dev\" in extras" +marker = "python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"default\" in dependency_groups or python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"all\" in extras or python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"dev\" in extras or python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"multimodal\" in extras" [packages.tool.pdm] dependencies = [] @@ -4330,7 +4441,7 @@ sdist = {name = "async_timeout-5.0.1.tar.gz", url = "https://files.pythonhosted. wheels = [ {name = "async_timeout-5.0.1-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl",hashes = {sha256 = "39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}}, ] -marker = "\"default\" in dependency_groups and python_full_version ~= \"3.10.0\" or \"dev\" in extras and python_full_version ~= \"3.10.0\"" +marker = "\"default\" in dependency_groups and python_full_version ~= \"3.10.0\" or \"all\" in extras and python_full_version ~= \"3.10.0\" or \"dev\" in extras and python_full_version ~= \"3.10.0\" or \"multimodal\" in extras and python_full_version ~= \"3.10.0\"" [packages.tool.pdm] dependencies = [] @@ -4343,7 +4454,7 @@ sdist = {name = "exceptiongroup-1.3.0.tar.gz", url = "https://files.pythonhosted wheels = [ {name = "exceptiongroup-1.3.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl",hashes = {sha256 = "4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}}, ] -marker = "\"default\" in dependency_groups and python_full_version ~= \"3.10.0\" or \"dev\" in extras and python_full_version ~= \"3.10.0\"" +marker = "\"default\" in dependency_groups and python_full_version ~= \"3.10.0\" or \"all\" in extras and python_full_version ~= \"3.10.0\" or \"dev\" in extras and python_full_version ~= \"3.10.0\" or \"multimodal\" in extras and python_full_version ~= \"3.10.0\"" [packages.tool.pdm] dependencies = [ @@ -4374,7 +4485,7 @@ sdist = {name = "networkx-3.4.2.tar.gz", url = "https://files.pythonhosted.org/p wheels = [ {name = "networkx-3.4.2-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl",hashes = {sha256 = "df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f"}}, ] -marker = "python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"default\" in dependency_groups" +marker = "python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"default\" in dependency_groups or python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"all\" in extras or python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"dev\" in extras or python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"multimodal\" in extras" [packages.tool.pdm] dependencies = [] From ec7071bbdd7dddeea7e3d3a989825b5d116c3537 Mon Sep 17 00:00:00 2001 From: Samuel Monson Date: Tue, 14 Oct 2025 17:21:54 -0400 Subject: [PATCH 40/57] Rewrite encode_audio to use torchcodec Signed-off-by: Samuel Monson --- pylock.toml | 194 +++++++++++++-------------- pyproject.toml | 2 +- src/guidellm/data/utils/functions.py | 98 ++++++-------- 3 files changed, 139 insertions(+), 155 deletions(-) diff --git a/pylock.toml b/pylock.toml index 8bd5e1d0..f8bdf6a1 100644 --- a/pylock.toml +++ b/pylock.toml @@ -11,6 +11,75 @@ dependency-groups = ["default"] default-groups = ["default"] created-by = "pdm" +[[packages]] +name = "torch" +version = "2.8.0+cpu" +requires-python = ">=3.9.0" +wheels = [ + {name = "torch-2.8.0+cpu-cp313-cp313-linux_s390x.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-linux_s390x.whl",hashes = {sha256 = "8b5882276633cf91fe3d2d7246c743b94d44a7e660b27f1308007fdb1bb89f7d"}}, + {name = "torch-2.8.0+cpu-cp313-cp313-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl",hashes = {sha256 = "a5064b5e23772c8d164068cc7c12e01a75faf7b948ecd95a0d4007d7487e5f25"}}, + {name = "torch-2.8.0+cpu-cp313-cp313-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl",hashes = {sha256 = "8f81dedb4c6076ec325acc3b47525f9c550e5284a18eae1d9061c543f7b6e7de"}}, + {name = "torch-2.8.0+cpu-cp313-cp313-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-win_amd64.whl",hashes = {sha256 = "e1ee1b2346ade3ea90306dfbec7e8ff17bc220d344109d189ae09078333b0856"}}, + {name = "torch-2.8.0+cpu-cp313-cp313-win_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-win_arm64.whl",hashes = {sha256 = "64c187345509f2b1bb334feed4666e2c781ca381874bde589182f81247e61f88"}}, + {name = "torch-2.8.0+cpu-cp313-cp313t-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl",hashes = {sha256 = "af81283ac671f434b1b25c95ba295f270e72db1fad48831eb5e4748ff9840041"}}, + {name = "torch-2.8.0+cpu-cp313-cp313t-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl",hashes = {sha256 = "a9dbb6f64f63258bc811e2c0c99640a81e5af93c531ad96e95c5ec777ea46dab"}}, + {name = "torch-2.8.0+cpu-cp313-cp313t-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-win_amd64.whl",hashes = {sha256 = "6d93a7165419bc4b2b907e859ccab0dea5deeab261448ae9a5ec5431f14c0e64"}}, + {name = "torch-2.8.0-cp313-cp313t-macosx_14_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0-cp313-cp313t-macosx_14_0_arm64.whl",hashes = {sha256 = "fbe2e149c5174ef90d29a5f84a554dfaf28e003cb4f61fa2c8c024c17ec7ca58"}}, + {name = "torch-2.8.0-cp313-none-macosx_11_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0-cp313-none-macosx_11_0_arm64.whl",hashes = {sha256 = "057efd30a6778d2ee5e2374cd63a63f63311aa6f33321e627c655df60abdd390"}}, + {name = "torch-2.8.0+cpu-cp312-cp312-linux_s390x.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-linux_s390x.whl",hashes = {sha256 = "0e34e276722ab7dd0dffa9e12fe2135a9b34a0e300c456ed7ad6430229404eb5"}}, + {name = "torch-2.8.0+cpu-cp312-cp312-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl",hashes = {sha256 = "610f600c102386e581327d5efc18c0d6edecb9820b4140d26163354a99cd800d"}}, + {name = "torch-2.8.0+cpu-cp312-cp312-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl",hashes = {sha256 = "cb9a8ba8137ab24e36bf1742cb79a1294bd374db570f09fc15a5e1318160db4e"}}, + {name = "torch-2.8.0+cpu-cp312-cp312-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-win_amd64.whl",hashes = {sha256 = "2be20b2c05a0cce10430cc25f32b689259640d273232b2de357c35729132256d"}}, + {name = "torch-2.8.0+cpu-cp312-cp312-win_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-win_arm64.whl",hashes = {sha256 = "99fc421a5d234580e45957a7b02effbf3e1c884a5dd077afc85352c77bf41434"}}, + {name = "torch-2.8.0-cp312-none-macosx_11_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0-cp312-none-macosx_11_0_arm64.whl",hashes = {sha256 = "a47b7986bee3f61ad217d8a8ce24605809ab425baf349f97de758815edd2ef54"}}, + {name = "torch-2.8.0+cpu-cp311-cp311-linux_s390x.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-linux_s390x.whl",hashes = {sha256 = "2bfc013dd6efdc8f8223a0241d3529af9f315dffefb53ffa3bf14d3f10127da6"}}, + {name = "torch-2.8.0+cpu-cp311-cp311-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-manylinux_2_28_aarch64.whl",hashes = {sha256 = "680129efdeeec3db5da3f88ee5d28c1b1e103b774aef40f9d638e2cce8f8d8d8"}}, + {name = "torch-2.8.0+cpu-cp311-cp311-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-manylinux_2_28_x86_64.whl",hashes = {sha256 = "cb06175284673a581dd91fb1965662ae4ecaba6e5c357aa0ea7bb8b84b6b7eeb"}}, + {name = "torch-2.8.0+cpu-cp311-cp311-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-win_amd64.whl",hashes = {sha256 = "7631ef49fbd38d382909525b83696dc12a55d68492ade4ace3883c62b9fc140f"}}, + {name = "torch-2.8.0+cpu-cp311-cp311-win_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-win_arm64.whl",hashes = {sha256 = "41e6fc5ec0914fcdce44ccf338b1d19a441b55cafdd741fd0bf1af3f9e4cfd14"}}, + {name = "torch-2.8.0-cp311-none-macosx_11_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0-cp311-none-macosx_11_0_arm64.whl",hashes = {sha256 = "3d05017d19bc99741288e458888283a44b0ee881d53f05f72f8b1cfea8998122"}}, + {name = "torch-2.8.0+cpu-cp310-cp310-linux_s390x.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp310-cp310-linux_s390x.whl",hashes = {sha256 = "5d255d259fbc65439b671580e40fdb8faea4644761b64fed90d6904ffe71bbc1"}}, + {name = "torch-2.8.0+cpu-cp310-cp310-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp310-cp310-manylinux_2_28_aarch64.whl",hashes = {sha256 = "b2149858b8340aeeb1f3056e0bff5b82b96e43b596fe49a9dba3184522261213"}}, + {name = "torch-2.8.0+cpu-cp310-cp310-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp310-cp310-manylinux_2_28_x86_64.whl",hashes = {sha256 = "16d75fa4e96ea28a785dfd66083ca55eb1058b6d6c5413f01656ca965ee2077e"}}, + {name = "torch-2.8.0+cpu-cp310-cp310-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp310-cp310-win_amd64.whl",hashes = {sha256 = "7cc4af6ba954f36c2163eab98cf113c137fc25aa8bbf1b06ef155968627beed2"}}, + {name = "torch-2.8.0-cp310-none-macosx_11_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0-cp310-none-macosx_11_0_arm64.whl",hashes = {sha256 = "a467b49fe893a6a6cce89e3aee556edfdc64a722d7195fdfdd75cec9dea13779"}}, +] +marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" + +[packages.tool.pdm] +dependencies = [ + "filelock", + "typing-extensions>=4.10.0", + "sympy>=1.13.3", + "networkx", + "jinja2", + "fsspec", + "setuptools; python_version >= \"3.12\"", +] + +[[packages]] +name = "torchcodec" +version = "0.7.0" +requires-python = ">=3.8" +wheels = [ + {name = "torchcodec-0.7.0-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/3c/82/7c7691d538f67704b2b2444deb0e234ae564f9329bc9becf66d69998bc9b/torchcodec-0.7.0-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "32a0115035a7f0a77fa451f67c101e0273a3a37d33b69e1bcd777f00aceb7340"}}, + {name = "torchcodec-0.7.0-cp313-cp313-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/01/25/177ea01d138598ab68d5e3b000789e8617bf97874bd8f761d89093f419ba/torchcodec-0.7.0-cp313-cp313-manylinux_2_28_x86_64.whl",hashes = {sha256 = "9c70f910f9f48e6625aacaed534f766e13d447b895dc7299e96d4db9a93f1514"}}, + {name = "torchcodec-0.7.0-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/5c/a9/e2b6301fbf4590d352e183bef64927f74ef4d4f660cca3ed7a32dda60484/torchcodec-0.7.0-cp313-cp313-win_amd64.whl",hashes = {sha256 = "31b402c9ae3c6e9f33c41fddf7058f9492c443ad55d02f022395f8fa196b58f6"}}, + {name = "torchcodec-0.7.0-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/c7/b2/6d3e190fcd18c65b35f6da734d4415c72b42c8a72ffc2494d998bad8caf3/torchcodec-0.7.0-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "d9d082bbb599f4f7715bfc3b1afa5bc16d8fb9d852e68084c63f1973cc78a1cb"}}, + {name = "torchcodec-0.7.0-cp312-cp312-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/41/10/4a1a8407d0fad37cb43d1f749e7b422e5a0f6def17f3b90ab9ab9a105e32/torchcodec-0.7.0-cp312-cp312-manylinux_2_28_x86_64.whl",hashes = {sha256 = "3cd23c3296c9b071d56bb2c534a6a98275d65c1a6a7213cdb72a26ec9f9d2fd8"}}, + {name = "torchcodec-0.7.0-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/9b/e7/a2fa7ed9c81d7d683d37ca7204007b421c0537132364a9cfd8d577f19a96/torchcodec-0.7.0-cp312-cp312-win_amd64.whl",hashes = {sha256 = "ac942831bff02e6041d8718b71c6f63e4e37c05dd95e72863725c9dbef0d4a7b"}}, + {name = "torchcodec-0.7.0-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/6a/f1/bb2b5ab929ef3f092cb6508673510ffc2aafd8324493c94a2d41f1c8a683/torchcodec-0.7.0-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "967a14b31e04721901ddbf965f9e9f733f328c5e98a51e22f414e25ac32e20ba"}}, + {name = "torchcodec-0.7.0-cp311-cp311-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/06/14/8ff28247988365fc47e8471e28cdfd8d037232fcf73abb67ee815ac80f1d/torchcodec-0.7.0-cp311-cp311-manylinux_2_28_x86_64.whl",hashes = {sha256 = "afb1c48b52bd4ee8f485f5a427bb4e82380590255a26b8e9e3fe099e0779287f"}}, + {name = "torchcodec-0.7.0-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/1f/80/04f23dff2c7ac406d2d6b24a52be7654a946d2fdfe158b19341a524dae20/torchcodec-0.7.0-cp311-cp311-win_amd64.whl",hashes = {sha256 = "a68765cd29159da3cf36eb5716481c617ad9d168fe06418bcde2a9360cc7eb5e"}}, + {name = "torchcodec-0.7.0-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/d8/23/ca6bd1bc5e22786596e25d1dd62a6a4e733802940b54726a54fcf5a8795b/torchcodec-0.7.0-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "6ae0b7acbc0c1a755ae817a8843715131f24be7807ddc46092d8b49a0fc970eb"}}, + {name = "torchcodec-0.7.0-cp310-cp310-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/0d/81/cff42793544b7d3e2ff9a4912542c6d1c7a617aabe8404f8fd3d52453f20/torchcodec-0.7.0-cp310-cp310-manylinux_2_28_x86_64.whl",hashes = {sha256 = "a0071096724e8ded6a171457ce4680f646499b4a4d285cdb46e130983f965ce4"}}, + {name = "torchcodec-0.7.0-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/ad/b9/7f03bf7d42e0f7ab5598d400cb1133d3f227b52aad15d88b2ab9c97fe1ff/torchcodec-0.7.0-cp310-cp310-win_amd64.whl",hashes = {sha256 = "737da9212594bf2f205582512a7a4f56d39591b357bf5a30e72e858cfcedc2ac"}}, +] +marker = "\"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" + +[packages.tool.pdm] +dependencies = [] + [[packages]] name = "blobfile" version = "3.1.0" @@ -870,79 +939,6 @@ dependencies = [ "colorama>=0.4.5; sys_platform == \"win32\"", ] -[[packages]] -name = "torch" -version = "2.9.0+cpu" -requires-python = ">=3.10" -wheels = [ - {name = "torch-2.9.0+cpu-cp314-cp314-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp314-cp314-manylinux_2_28_aarch64.whl",hashes = {sha256 = "44aadb735774d4a99525d2ec29126b23016c44a07b02ce6c237dfa61a223dd52"}}, - {name = "torch-2.9.0+cpu-cp314-cp314-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp314-cp314-manylinux_2_28_x86_64.whl",hashes = {sha256 = "b355e07b7f0c369cb031adfcbff5c37a609abcea091b918a39886412afd2e07d"}}, - {name = "torch-2.9.0+cpu-cp314-cp314-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp314-cp314-win_amd64.whl",hashes = {sha256 = "c2698999361d73c2d25d7cc8a787130188d49b183abb18b554228daa102e1594"}}, - {name = "torch-2.9.0+cpu-cp314-cp314t-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp314-cp314t-manylinux_2_28_aarch64.whl",hashes = {sha256 = "fa0d1373d04b30ff8f12d542135d292f1a1ddb7c0d852a3d487a320360e5dab9"}}, - {name = "torch-2.9.0+cpu-cp314-cp314t-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp314-cp314t-manylinux_2_28_x86_64.whl",hashes = {sha256 = "2f49bb57a5fe0dc7f8e73ea9e5d36ebda2ea25b8a714a788f0fc2fc47d20a830"}}, - {name = "torch-2.9.0+cpu-cp314-cp314t-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp314-cp314t-win_amd64.whl",hashes = {sha256 = "3a60d1ecf27a9cce839b3aa665b26f0af1b1007b9c9f1e7f597f6b7bdf107617"}}, - {name = "torch-2.9.0-cp314-cp314-macosx_11_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "d8e2ab7f86010330bdcc39c8b2c795590cc75e37df4823cdaee2c98d6e3ff4a3"}}, - {name = "torch-2.9.0-cp314-cp314t-macosx_11_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0-cp314-cp314t-macosx_11_0_arm64.whl",hashes = {sha256 = "a3e859039c985d8e3ea60d7a54ca7e97ea2ae15e31beced4f3260128a161bb01"}}, - {name = "torch-2.9.0+cpu-cp313-cp313-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl",hashes = {sha256 = "be4438d8dad7f0d5a5e54f0feef8a893446894ec87f102bb1d82dcc4518542e4"}}, - {name = "torch-2.9.0+cpu-cp313-cp313-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl",hashes = {sha256 = "6c9b217584400963d5b4daddb3711ec7a3778eab211e18654fba076cce3b8682"}}, - {name = "torch-2.9.0+cpu-cp313-cp313-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp313-cp313-win_amd64.whl",hashes = {sha256 = "728372e3f58c5826445f677746e5311c1935c1a7c59599f73a49ded850e038e8"}}, - {name = "torch-2.9.0+cpu-cp313-cp313-win_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp313-cp313-win_arm64.whl",hashes = {sha256 = "95e56c26f919fbb98f16e7a0b87af494b893f9da9a65a020f17a01c13e520a81"}}, - {name = "torch-2.9.0+cpu-cp313-cp313t-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl",hashes = {sha256 = "6c777160288b08555820781ae0f3a2c67a59bd24b065e88ca1ec20e2f9dc8ac7"}}, - {name = "torch-2.9.0+cpu-cp313-cp313t-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl",hashes = {sha256 = "528fd338311f31c9fb18038cafd00e6eae0bf5ad5577521701acb62510753d18"}}, - {name = "torch-2.9.0+cpu-cp313-cp313t-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp313-cp313t-win_amd64.whl",hashes = {sha256 = "d572863990e7d2762b547735ef589f6350d9eb4e441d38753a1c33636698cf4c"}}, - {name = "torch-2.9.0-cp313-cp313t-macosx_11_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0-cp313-cp313t-macosx_11_0_arm64.whl",hashes = {sha256 = "259548471194ab63d7ea273873053a6e3cc23530c1510f01e9d7ad259187bbd0"}}, - {name = "torch-2.9.0-cp313-none-macosx_11_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0-cp313-none-macosx_11_0_arm64.whl",hashes = {sha256 = "e24836d968b54ef4dfb05594001a61958711ac9224026291e4e3f92f83a6fd7f"}}, - {name = "torch-2.9.0+cpu-cp312-cp312-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl",hashes = {sha256 = "3a651434ae1248b0568c12b5f9e3acc8942eb28378d9d04a79302938b68c6f24"}}, - {name = "torch-2.9.0+cpu-cp312-cp312-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl",hashes = {sha256 = "28f6eb31b08180a5c5e98d5bc14eef6909c9f5a1dbff9632c3e02a8773449349"}}, - {name = "torch-2.9.0+cpu-cp312-cp312-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp312-cp312-win_amd64.whl",hashes = {sha256 = "e438061b87ec7dd6018fca9f975219889aa0a3f6cdc3ea10dd0ae2bc7f1c47ce"}}, - {name = "torch-2.9.0+cpu-cp312-cp312-win_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp312-cp312-win_arm64.whl",hashes = {sha256 = "eb13ff1c34e338d722e76a4fd83b8d282782505bd1b99af4b3c32da66eba6eb4"}}, - {name = "torch-2.9.0-cp312-none-macosx_11_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0-cp312-none-macosx_11_0_arm64.whl",hashes = {sha256 = "4de0ed8cbc457a506dbca40376e206a29efee10756a00f1f3404bf67ad737d04"}}, - {name = "torch-2.9.0+cpu-cp311-cp311-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp311-cp311-manylinux_2_28_aarch64.whl",hashes = {sha256 = "da77341ccaba31762d9238b0942c165c4582a26818f3045b052b39cebdd7ad9d"}}, - {name = "torch-2.9.0+cpu-cp311-cp311-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp311-cp311-manylinux_2_28_x86_64.whl",hashes = {sha256 = "add3e93ecc1eeaa6853f6a973ce60ffb3cb14ed2e80f5055e139b09385dce0a7"}}, - {name = "torch-2.9.0+cpu-cp311-cp311-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp311-cp311-win_amd64.whl",hashes = {sha256 = "389e1e0b8083fd355f7caf5ba82356b5e01c318998bd575dbf2285a0d8137089"}}, - {name = "torch-2.9.0+cpu-cp311-cp311-win_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp311-cp311-win_arm64.whl",hashes = {sha256 = "5ce3d01aef91dc078fbb121814e556d55bc886d303efaf42c4fe67e411f5f9ad"}}, - {name = "torch-2.9.0-cp311-none-macosx_11_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0-cp311-none-macosx_11_0_arm64.whl",hashes = {sha256 = "aa4483602586cc9a35d1cf33771a9977f05f642b9161518a289e36548a0b77c2"}}, - {name = "torch-2.9.0+cpu-cp310-cp310-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp310-cp310-manylinux_2_28_aarch64.whl",hashes = {sha256 = "b224792ea567b52c7f1ce1d789567f6920e06fd3b339fa1e1b05948845f783ad"}}, - {name = "torch-2.9.0+cpu-cp310-cp310-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp310-cp310-manylinux_2_28_x86_64.whl",hashes = {sha256 = "bd2a257e670ede9fc01c6d76dccdc473040913b8e9328169bf177dbdc38e2484"}}, - {name = "torch-2.9.0+cpu-cp310-cp310-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp310-cp310-win_amd64.whl",hashes = {sha256 = "96f3f7aa4eb9e7fc5af8a722eaf1e5e32e3039dbafe817178d7b90a8566be32d"}}, - {name = "torch-2.9.0-cp310-none-macosx_11_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0-cp310-none-macosx_11_0_arm64.whl",hashes = {sha256 = "59484193b01299bf669520505a72b29d59a0028ae4c6d95f492938f186592208"}}, -] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" - -[packages.tool.pdm] -dependencies = [ - "filelock", - "typing-extensions>=4.10.0", - "setuptools; python_version >= \"3.12\"", - "sympy>=1.13.3", - "networkx>=2.5.1", - "jinja2", - "fsspec>=0.8.5", -] - -[[packages]] -name = "torchcodec" -version = "0.7.0" -requires-python = ">=3.8" -wheels = [ - {name = "torchcodec-0.7.0-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/3c/82/7c7691d538f67704b2b2444deb0e234ae564f9329bc9becf66d69998bc9b/torchcodec-0.7.0-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "32a0115035a7f0a77fa451f67c101e0273a3a37d33b69e1bcd777f00aceb7340"}}, - {name = "torchcodec-0.7.0-cp313-cp313-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/01/25/177ea01d138598ab68d5e3b000789e8617bf97874bd8f761d89093f419ba/torchcodec-0.7.0-cp313-cp313-manylinux_2_28_x86_64.whl",hashes = {sha256 = "9c70f910f9f48e6625aacaed534f766e13d447b895dc7299e96d4db9a93f1514"}}, - {name = "torchcodec-0.7.0-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/5c/a9/e2b6301fbf4590d352e183bef64927f74ef4d4f660cca3ed7a32dda60484/torchcodec-0.7.0-cp313-cp313-win_amd64.whl",hashes = {sha256 = "31b402c9ae3c6e9f33c41fddf7058f9492c443ad55d02f022395f8fa196b58f6"}}, - {name = "torchcodec-0.7.0-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/c7/b2/6d3e190fcd18c65b35f6da734d4415c72b42c8a72ffc2494d998bad8caf3/torchcodec-0.7.0-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "d9d082bbb599f4f7715bfc3b1afa5bc16d8fb9d852e68084c63f1973cc78a1cb"}}, - {name = "torchcodec-0.7.0-cp312-cp312-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/41/10/4a1a8407d0fad37cb43d1f749e7b422e5a0f6def17f3b90ab9ab9a105e32/torchcodec-0.7.0-cp312-cp312-manylinux_2_28_x86_64.whl",hashes = {sha256 = "3cd23c3296c9b071d56bb2c534a6a98275d65c1a6a7213cdb72a26ec9f9d2fd8"}}, - {name = "torchcodec-0.7.0-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/9b/e7/a2fa7ed9c81d7d683d37ca7204007b421c0537132364a9cfd8d577f19a96/torchcodec-0.7.0-cp312-cp312-win_amd64.whl",hashes = {sha256 = "ac942831bff02e6041d8718b71c6f63e4e37c05dd95e72863725c9dbef0d4a7b"}}, - {name = "torchcodec-0.7.0-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/6a/f1/bb2b5ab929ef3f092cb6508673510ffc2aafd8324493c94a2d41f1c8a683/torchcodec-0.7.0-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "967a14b31e04721901ddbf965f9e9f733f328c5e98a51e22f414e25ac32e20ba"}}, - {name = "torchcodec-0.7.0-cp311-cp311-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/06/14/8ff28247988365fc47e8471e28cdfd8d037232fcf73abb67ee815ac80f1d/torchcodec-0.7.0-cp311-cp311-manylinux_2_28_x86_64.whl",hashes = {sha256 = "afb1c48b52bd4ee8f485f5a427bb4e82380590255a26b8e9e3fe099e0779287f"}}, - {name = "torchcodec-0.7.0-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/1f/80/04f23dff2c7ac406d2d6b24a52be7654a946d2fdfe158b19341a524dae20/torchcodec-0.7.0-cp311-cp311-win_amd64.whl",hashes = {sha256 = "a68765cd29159da3cf36eb5716481c617ad9d168fe06418bcde2a9360cc7eb5e"}}, - {name = "torchcodec-0.7.0-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/d8/23/ca6bd1bc5e22786596e25d1dd62a6a4e733802940b54726a54fcf5a8795b/torchcodec-0.7.0-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "6ae0b7acbc0c1a755ae817a8843715131f24be7807ddc46092d8b49a0fc970eb"}}, - {name = "torchcodec-0.7.0-cp310-cp310-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/0d/81/cff42793544b7d3e2ff9a4912542c6d1c7a617aabe8404f8fd3d52453f20/torchcodec-0.7.0-cp310-cp310-manylinux_2_28_x86_64.whl",hashes = {sha256 = "a0071096724e8ded6a171457ce4680f646499b4a4d285cdb46e130983f965ce4"}}, - {name = "torchcodec-0.7.0-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/ad/b9/7f03bf7d42e0f7ab5598d400cb1133d3f227b52aad15d88b2ab9c97fe1ff/torchcodec-0.7.0-cp310-cp310-win_amd64.whl",hashes = {sha256 = "737da9212594bf2f205582512a7a4f56d39591b357bf5a30e72e858cfcedc2ac"}}, -] -marker = "\"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" - -[packages.tool.pdm] -dependencies = [] - [[packages]] name = "tox" version = "4.16.0" @@ -3377,19 +3373,6 @@ marker = "\"dev\" in extras" [packages.tool.pdm] dependencies = [] -[[packages]] -name = "networkx" -version = "3.5" -requires-python = ">=3.11" -sdist = {name = "networkx-3.5.tar.gz", url = "https://files.pythonhosted.org/packages/6c/4f/ccdb8ad3a38e583f214547fd2f7ff1fc160c43a75af88e6aec213404b96a/networkx-3.5.tar.gz", hashes = {sha256 = "d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037"}} -wheels = [ - {name = "networkx-3.5-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl",hashes = {sha256 = "0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec"}}, -] -marker = "\"default\" in dependency_groups and python_version ~= \"3.12\" or \"all\" in extras and python_version ~= \"3.12\" or \"dev\" in extras and python_version ~= \"3.12\" or \"multimodal\" in extras and python_version ~= \"3.12\"" - -[packages.tool.pdm] -dependencies = [] - [[packages]] name = "nodeenv" version = "1.9.1" @@ -3941,6 +3924,19 @@ dependencies = [ "uc-micro-py", ] +[[packages]] +name = "networkx" +version = "3.5" +requires-python = ">=3.11" +sdist = {name = "networkx-3.5.tar.gz", url = "https://files.pythonhosted.org/packages/6c/4f/ccdb8ad3a38e583f214547fd2f7ff1fc160c43a75af88e6aec213404b96a/networkx-3.5.tar.gz", hashes = {sha256 = "d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037"}} +wheels = [ + {name = "networkx-3.5-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl",hashes = {sha256 = "0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec"}}, +] +marker = "\"default\" in dependency_groups and python_version ~= \"3.12\" or \"all\" in extras and python_version ~= \"3.12\" or \"dev\" in extras and python_version ~= \"3.12\" or \"multimodal\" in extras and python_version ~= \"3.12\"" + +[packages.tool.pdm] +dependencies = [] + [[packages]] name = "pandas" version = "2.3.3" @@ -4478,33 +4474,33 @@ dependencies = [ ] [[packages]] -name = "networkx" -version = "3.4.2" -requires-python = ">=3.10" -sdist = {name = "networkx-3.4.2.tar.gz", url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hashes = {sha256 = "307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1"}} +name = "zipp" +version = "3.23.0" +requires-python = ">=3.9" +sdist = {name = "zipp-3.23.0.tar.gz", url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hashes = {sha256 = "a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}} wheels = [ - {name = "networkx-3.4.2-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl",hashes = {sha256 = "df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f"}}, + {name = "zipp-3.23.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl",hashes = {sha256 = "071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}}, ] -marker = "python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"default\" in dependency_groups or python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"all\" in extras or python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"dev\" in extras or python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"multimodal\" in extras" +marker = "python_full_version >= \"3.10.0\" and python_full_version < \"3.10.2\" and \"dev\" in extras" [packages.tool.pdm] dependencies = [] [[packages]] -name = "zipp" -version = "3.23.0" -requires-python = ">=3.9" -sdist = {name = "zipp-3.23.0.tar.gz", url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hashes = {sha256 = "a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}} +name = "networkx" +version = "3.4.2" +requires-python = ">=3.10" +sdist = {name = "networkx-3.4.2.tar.gz", url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hashes = {sha256 = "307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1"}} wheels = [ - {name = "zipp-3.23.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl",hashes = {sha256 = "071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}}, + {name = "networkx-3.4.2-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl",hashes = {sha256 = "df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f"}}, ] -marker = "python_full_version >= \"3.10.0\" and python_full_version < \"3.10.2\" and \"dev\" in extras" +marker = "python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"default\" in dependency_groups or python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"all\" in extras or python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"dev\" in extras or python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"multimodal\" in extras" [packages.tool.pdm] dependencies = [] [tool.pdm] -hashes = {sha256 = "42547540cdbfb38c3bf9f67e7159db84b94a9abd466da78f7c1c481c41bea687"} +hashes = {sha256 = "5848d0fb723d7bb4169ae2d860b8f2a90af8c2be18b9bc7e6c6f5d79e7b71bc5"} strategy = ["inherit_metadata", "static_urls"] [[tool.pdm.targets]] diff --git a/pyproject.toml b/pyproject.toml index f5420b6a..2fa35ce0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,7 +82,7 @@ recommended = ["guidellm[perf,openai]"] # Feature Extras perf = ["orjson", "msgpack", "msgspec", "uvloop"] openai = ["tiktoken>=0.11.0", "blobfile>=3.1.0"] -multimodal = ["datasets[audio,vision]>=4.1.0", "torchcodec"] +multimodal = ["datasets[audio,vision]>=4.1.0", "torchcodec==0.7", "torch==2.8.*"] # Dev Tooling dev = [ # Install all optional dependencies diff --git a/src/guidellm/data/utils/functions.py b/src/guidellm/data/utils/functions.py index e11c5cb8..f6f05072 100644 --- a/src/guidellm/data/utils/functions.py +++ b/src/guidellm/data/utils/functions.py @@ -6,12 +6,12 @@ from typing import Any, Literal import httpx -import librosa import numpy as np -import soundfile +import torch from PIL import Image as PILImage -from pydub import AudioSegment from torch import Tensor +from torchcodec.decoders import AudioDecoder +from torchcodec.encoders import AudioEncoder __all__ = [ "encode_audio", @@ -253,7 +253,7 @@ def encode_video( def encode_audio( audio: Any, - b64encode: bool, + b64encode: bool = False, sample_rate: int = 16000, file_name: str = "audio.wav", encode_sample_rate: int = 16000, @@ -289,72 +289,60 @@ def encode_audio( bitrate=bitrate, ) - audio_numpy: np.ndarray - - if hasattr(audio, "get_samples_played_in_range"): - # HF datasets Audio object - audio_samples = audio.get_samples_played_in_range( - start_seconds=0.0, - stop_seconds=( - None - if max_duration is None - else min(max_duration, audio.metadata.duration_seconds_from_header) - ), + decoder: AudioDecoder + + if isinstance(audio, AudioDecoder): + decoder = audio + elif isinstance(audio, Tensor | bytes): + decoder = AudioDecoder( + source=audio, + sample_rate=sample_rate, ) - audio_numpy = np.array(audio_samples.data) - elif isinstance(audio, Tensor): - audio_numpy = audio.numpy() elif isinstance(audio, str | Path): if is_url(audio): response = httpx.get(audio) response.raise_for_status() - audio_stream = response.content file_name = get_file_name(audio) + decoder = AudioDecoder( + source=response.content, + ) else: if not Path(audio).exists(): raise ValueError(f"Audio file does not exist: {audio}") file_name = get_file_name(audio) - audio_stream = Path(audio).read_bytes() - - audio_numpy, sample_rate = soundfile.read( - io.BytesIO(audio_stream), dtype="float32" - ) - elif isinstance(audio, bytes): - audio_numpy, sample_rate = soundfile.read(io.BytesIO(audio), dtype="float32") + decoder = AudioDecoder( + source=audio, + ) elif isinstance(audio, np.ndarray): - audio_numpy = audio + # AudioDecoder really needs a from_raw method + pre_encoder = AudioEncoder( + samples=torch.from_numpy(audio), + sample_rate=sample_rate, + ) + decoder = AudioDecoder(source=pre_encoder.to_tensor(format="wav")) else: raise ValueError(f"Unsupported audio type: {type(audio)}") - if sample_rate != encode_sample_rate: - audio_numpy = librosa.resample( - audio_numpy.astype(np.float32), - orig_sr=sample_rate, - target_sr=encode_sample_rate, - ) - sample_rate = encode_sample_rate + samples = decoder.get_samples_played_in_range(stop_seconds=max_duration) + encoder = AudioEncoder( + samples=samples.data, + sample_rate=samples.sample_rate, + ) - audio_numpy = librosa.to_mono(audio_numpy) + bit_rate_val = ( + int(bitrate.rstrip("k")) * 1000 if bitrate.endswith("k") else int(bitrate) + ) + format_val = audio_format.lower() - if ( - max_duration is not None - and max_duration > 0 - and (max_samples := int(max_duration * sample_rate)) < len(audio_numpy) - ): - audio_numpy = audio_numpy[max_samples:] + audio_tensor = encoder.to_tensor( + format=format_val, + bit_rate=bit_rate_val if format_val == "mp3" else None, + num_channels=1 if mono else None, + sample_rate=encode_sample_rate if sample_rate != encode_sample_rate else None, + ) audio_buffer = io.BytesIO() - - if audio_format.lower() == "mp3": - wav = io.BytesIO() - soundfile.write(wav, audio_numpy, sample_rate, format="WAV", subtype="PCM_16") - wav.seek(0) - - sound = AudioSegment.from_wav(wav) - sound.export(audio_buffer, format="mp3", bitrate=bitrate) - else: - soundfile.write(audio_buffer, audio, sample_rate, format=audio_format.upper()) - + torch.save(audio_tensor, audio_buffer) audio_buffer.seek(0) decoded_audio = audio_buffer.read() @@ -367,9 +355,9 @@ def encode_audio( ), "file_name": file_name, "format": audio_format, - "mimetype": f"audio/{audio_format}", - "audio_samples": len(audio_numpy), - "audio_seconds": len(audio_numpy) / sample_rate, + "mimetype": f"audio/{format_val}", + "audio_samples": samples.sample_rate, + "audio_seconds": samples.duration_seconds, "audio_bytes": len(decoded_audio), } From aee230c230724ea0d9ec2bfed9cfce9f1d56db09 Mon Sep 17 00:00:00 2001 From: Samuel Monson Date: Wed, 15 Oct 2025 10:59:28 -0400 Subject: [PATCH 41/57] Dump raw bytes not tensor Signed-off-by: Samuel Monson --- src/guidellm/data/utils/functions.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/guidellm/data/utils/functions.py b/src/guidellm/data/utils/functions.py index f6f05072..c30dd083 100644 --- a/src/guidellm/data/utils/functions.py +++ b/src/guidellm/data/utils/functions.py @@ -341,24 +341,21 @@ def encode_audio( sample_rate=encode_sample_rate if sample_rate != encode_sample_rate else None, ) - audio_buffer = io.BytesIO() - torch.save(audio_tensor, audio_buffer) - audio_buffer.seek(0) - decoded_audio = audio_buffer.read() + encoded_audio = audio_tensor.numpy().tobytes() return { "type": "audio_base64" if b64encode else "audio_file", "audio": ( - base64.b64encode(decoded_audio).decode("utf-8") + base64.b64encode(encoded_audio).decode("utf-8") if b64encode - else decoded_audio + else encoded_audio ), "file_name": file_name, "format": audio_format, "mimetype": f"audio/{format_val}", "audio_samples": samples.sample_rate, "audio_seconds": samples.duration_seconds, - "audio_bytes": len(decoded_audio), + "audio_bytes": len(encoded_audio), } From c1340b489ba1e6a74c1947d73203bf20849ad2ae Mon Sep 17 00:00:00 2001 From: Samuel Monson Date: Wed, 15 Oct 2025 15:22:31 -0400 Subject: [PATCH 42/57] Code pathway cleanup Signed-off-by: Samuel Monson --- src/guidellm/data/utils/functions.py | 167 +++++++++++++++++++-------- 1 file changed, 116 insertions(+), 51 deletions(-) diff --git a/src/guidellm/data/utils/functions.py b/src/guidellm/data/utils/functions.py index c30dd083..baa5af76 100644 --- a/src/guidellm/data/utils/functions.py +++ b/src/guidellm/data/utils/functions.py @@ -10,6 +10,7 @@ import torch from PIL import Image as PILImage from torch import Tensor +from torchcodec import AudioSamples from torchcodec.decoders import AudioDecoder from torchcodec.encoders import AudioEncoder @@ -251,10 +252,10 @@ def encode_video( } -def encode_audio( - audio: Any, +def encode_audio( # noqa: C901 # noqa: PLR0913 + audio: AudioDecoder | bytes | str | Path | np.ndarray | Tensor | dict[str, Any], b64encode: bool = False, - sample_rate: int = 16000, + sample_rate: int | None = None, file_name: str = "audio.wav", encode_sample_rate: int = 16000, max_duration: float | None = None, @@ -273,90 +274,154 @@ def encode_audio( ], str | int | float | None, ]: + """Decode audio (if nessary) and re-encode to specified format.""" + samples = _decode_audio(audio, sample_rate=sample_rate, max_duration=max_duration) + + bitrate_val = ( + int(bitrate.rstrip("k")) * 1000 if bitrate.endswith("k") else int(bitrate) + ) + format_val = audio_format.lower() + + encoded_audio = _encode_audio( + samples=samples, + resample_rate=encode_sample_rate, + bitrate=bitrate_val, + audio_format=format_val, + mono=mono, + ) + + return { + "type": "audio_base64" if b64encode else "audio_file", + "audio": ( + base64.b64encode(encoded_audio).decode("utf-8") + if b64encode + else encoded_audio + ), + "file_name": get_file_name(audio) + if isinstance(audio, str | Path) + else file_name, + "format": audio_format, + "mimetype": f"audio/{format_val}", + "audio_samples": samples.sample_rate, + "audio_seconds": samples.duration_seconds, + "audio_bytes": len(encoded_audio), + } + + +def _decode_audio( # noqa: C901, PLR0912 + audio: AudioDecoder | bytes | str | Path | np.ndarray | Tensor | dict[str, Any], + sample_rate: int | None = None, + max_duration: float | None = None, +) -> AudioSamples: + """Decode audio from various input types into AudioSamples.""" + # If input is a dict, unwrap it into a function call if isinstance(audio, dict): sample_rate = audio.get("sample_rate", audio.get("sampling_rate", sample_rate)) if "data" not in audio and "url" not in audio: raise ValueError( f"Audio dict must contain either 'data' or 'url' keys, got {audio}" ) - return encode_audio( + return _decode_audio( audio=audio.get("data") or audio.get("url"), sample_rate=sample_rate, - encode_sample_rate=encode_sample_rate, max_duration=max_duration, - mono=mono, - audio_format=audio_format, - bitrate=bitrate, ) - decoder: AudioDecoder + # Convert numpy array to torch tensor and re-call + if isinstance(audio, np.ndarray): + return _decode_audio( + audio=torch.from_numpy(audio), + sample_rate=sample_rate, + max_duration=max_duration, + ) + + samples: AudioSamples + # HF datasets return AudioDecoder for audio column if isinstance(audio, AudioDecoder): - decoder = audio - elif isinstance(audio, Tensor | bytes): + samples = audio.get_samples_played_in_range(stop_seconds=max_duration) + + elif isinstance(audio, Tensor): + # If float stream assume decoded audio + if torch.is_floating_point(audio): + if sample_rate is None: + raise ValueError("Sample rate must be set for decoded audio") + + full_duration = audio.shape[1] / sample_rate + # If max_duration is set, trim the audio to that duration + if max_duration is not None: + num_samples = int(max_duration * sample_rate) + duration = min(max_duration, full_duration) + data = audio[:, :num_samples] + else: + duration = full_duration + data = audio + + samples = AudioSamples( + data=data, + pts_seconds=0.0, + duration_seconds=duration, + sample_rate=sample_rate, + ) + # If bytes tensor assume encoded audio + elif audio.dtype == torch.uint8: + decoder = AudioDecoder( + source=audio, + sample_rate=sample_rate, + ) + samples = decoder.get_samples_played_in_range(stop_seconds=max_duration) + + else: + raise ValueError(f"Unsupported audio type: {type(audio)}") + + # If bytes, assume encoded audio + elif isinstance(audio, bytes): decoder = AudioDecoder( source=audio, sample_rate=sample_rate, ) + samples = decoder.get_samples_played_in_range(stop_seconds=max_duration) + + # If str or Path, assume file path or URL to encoded audio elif isinstance(audio, str | Path): - if is_url(audio): + if isinstance(audio, str) and is_url(audio): response = httpx.get(audio) response.raise_for_status() - file_name = get_file_name(audio) - decoder = AudioDecoder( - source=response.content, - ) + data = response.content else: if not Path(audio).exists(): raise ValueError(f"Audio file does not exist: {audio}") - file_name = get_file_name(audio) - decoder = AudioDecoder( - source=audio, - ) - elif isinstance(audio, np.ndarray): - # AudioDecoder really needs a from_raw method - pre_encoder = AudioEncoder( - samples=torch.from_numpy(audio), - sample_rate=sample_rate, + data = Path(audio).read_bytes() + decoder = AudioDecoder( + source=data, ) - decoder = AudioDecoder(source=pre_encoder.to_tensor(format="wav")) + samples = decoder.get_samples_played_in_range(stop_seconds=max_duration) else: raise ValueError(f"Unsupported audio type: {type(audio)}") - samples = decoder.get_samples_played_in_range(stop_seconds=max_duration) + return samples + + +def _encode_audio( + samples: AudioSamples, + resample_rate: int | None = None, + bitrate: int = 64000, + audio_format: str = "mp3", + mono: bool = True, +) -> bytes: encoder = AudioEncoder( samples=samples.data, sample_rate=samples.sample_rate, ) - bit_rate_val = ( - int(bitrate.rstrip("k")) * 1000 if bitrate.endswith("k") else int(bitrate) - ) - format_val = audio_format.lower() - audio_tensor = encoder.to_tensor( - format=format_val, - bit_rate=bit_rate_val if format_val == "mp3" else None, + format=audio_format, + bit_rate=bitrate if audio_format == "mp3" else None, num_channels=1 if mono else None, - sample_rate=encode_sample_rate if sample_rate != encode_sample_rate else None, + sample_rate=resample_rate, ) - encoded_audio = audio_tensor.numpy().tobytes() - - return { - "type": "audio_base64" if b64encode else "audio_file", - "audio": ( - base64.b64encode(encoded_audio).decode("utf-8") - if b64encode - else encoded_audio - ), - "file_name": file_name, - "format": audio_format, - "mimetype": f"audio/{format_val}", - "audio_samples": samples.sample_rate, - "audio_seconds": samples.duration_seconds, - "audio_bytes": len(encoded_audio), - } + return audio_tensor.numpy().tobytes() def get_file_name(path: Path | str) -> str: From c8e9ff980a0e28ec39c4c6aa7d00eaab572cf3a9 Mon Sep 17 00:00:00 2001 From: Samuel Monson Date: Wed, 15 Oct 2025 17:38:04 -0400 Subject: [PATCH 43/57] Defer multimodal imports Signed-off-by: Samuel Monson --- pylock.toml | 169 +++---- pyproject.toml | 8 +- src/guidellm/data/preprocessors/formatters.py | 41 +- src/guidellm/data/utils/__init__.py | 12 - src/guidellm/data/utils/functions.py | 421 +---------------- src/guidellm/extras/__init__.py | 4 + src/guidellm/extras/multimodal.py | 436 ++++++++++++++++++ 7 files changed, 552 insertions(+), 539 deletions(-) create mode 100644 src/guidellm/extras/__init__.py create mode 100644 src/guidellm/extras/multimodal.py diff --git a/pylock.toml b/pylock.toml index f8bdf6a1..f995c082 100644 --- a/pylock.toml +++ b/pylock.toml @@ -644,7 +644,7 @@ wheels = [ {name = "pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/62/bb/78347dbe13219991877ffb3a91bf09da8317fbfcd4b5f9140aeae020ad71/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a"}}, {name = "pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl",url = "https://files.pythonhosted.org/packages/d9/28/1000353d5e61498aaeaaf7f1e4b49ddb05f2c6575f9d4f9f914a3538b6e1/pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl",hashes = {sha256 = "8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" +marker = "\"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" [packages.tool.pdm] dependencies = [] @@ -2880,101 +2880,76 @@ dependencies = [] [[packages]] name = "coverage" -version = "7.10.7" -requires-python = ">=3.9" -sdist = {name = "coverage-7.10.7.tar.gz", url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hashes = {sha256 = "f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239"}} -wheels = [ - {name = "coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/23/9c/5844ab4ca6a4dd97a1850e030a15ec7d292b5c5cb93082979225126e35dd/coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl",hashes = {sha256 = "b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520"}}, - {name = "coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/f0/89/673f6514b0961d1f0e20ddc242e9342f6da21eaba3489901b565c0689f34/coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32"}}, - {name = "coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/05/e8/261cae479e85232828fb17ad536765c88dd818c8470aca690b0ac6feeaa3/coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f"}}, - {name = "coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/82/62/14ed6546d0207e6eda876434e3e8475a3e9adbe32110ce896c9e0c06bb9a/coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a"}}, - {name = "coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/ff/49/07f00db9ac6478e4358165a08fb41b469a1b053212e8a00cb02f0d27a05f/coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360"}}, - {name = "coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/a2/59/c5201c62dbf165dfbc91460f6dbbaa85a8b82cfa6131ac45d6c1bfb52deb/coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69"}}, - {name = "coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/07/ae/5920097195291a51fb00b3a70b9bbd2edbfe3c84876a1762bd1ef1565ebc/coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl",hashes = {sha256 = "cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14"}}, - {name = "coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/b9/3c/a815dde77a2981f5743a60b63df31cb322c944843e57dbd579326625a413/coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl",hashes = {sha256 = "39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe"}}, - {name = "coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/aa/99/f5cdd8421ea656abefb6c0ce92556709db2265c41e8f9fc6c8ae0f7824c9/coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl",hashes = {sha256 = "925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e"}}, - {name = "coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/c3/7a/e9a2da6a1fc5d007dd51fca083a663ab930a8c4d149c087732a5dbaa0029/coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl",hashes = {sha256 = "2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd"}}, - {name = "coverage-7.10.7-cp314-cp314-win32.whl",url = "https://files.pythonhosted.org/packages/ef/5b/0b5799aa30380a949005a353715095d6d1da81927d6dbed5def2200a4e25/coverage-7.10.7-cp314-cp314-win32.whl",hashes = {sha256 = "b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2"}}, - {name = "coverage-7.10.7-cp314-cp314-win_amd64.whl",url = "https://files.pythonhosted.org/packages/da/b0/e802fbb6eb746de006490abc9bb554b708918b6774b722bb3a0e6aa1b7de/coverage-7.10.7-cp314-cp314-win_amd64.whl",hashes = {sha256 = "1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681"}}, - {name = "coverage-7.10.7-cp314-cp314-win_arm64.whl",url = "https://files.pythonhosted.org/packages/9e/e8/71d0c8e374e31f39e3389bb0bd19e527d46f00ea8571ec7ec8fd261d8b44/coverage-7.10.7-cp314-cp314-win_arm64.whl",hashes = {sha256 = "097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880"}}, - {name = "coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/62/09/9a5608d319fa3eba7a2019addeacb8c746fb50872b57a724c9f79f146969/coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl",hashes = {sha256 = "a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63"}}, - {name = "coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/f5/6f/f58d46f33db9f2e3647b2d0764704548c184e6f5e014bef528b7f979ef84/coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl",hashes = {sha256 = "9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2"}}, - {name = "coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/74/5c/183ffc817ba68e0b443b8c934c8795553eb0c14573813415bd59941ee165/coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d"}}, - {name = "coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/0f/48/71a8abe9c1ad7e97548835e3cc1adbf361e743e9d60310c5f75c9e7bf847/coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0"}}, - {name = "coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/84/fd/193a8fb132acfc0a901f72020e54be5e48021e1575bb327d8ee1097a28fd/coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699"}}, - {name = "coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/b1/8f/74ecc30607dd95ad50e3034221113ccb1c6d4e8085cc761134782995daae/coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9"}}, - {name = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/0f/55/79ff53a769f20d71b07023ea115c9167c0bb56f281320520cf64c5298a96/coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f"}}, - {name = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/88/e2/dac66c140009b61ac3fc13af673a574b00c16efdf04f9b5c740703e953c0/coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl",hashes = {sha256 = "0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1"}}, - {name = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/a2/f1/f48f645e3f33bb9ca8a496bc4a9671b52f2f353146233ebd7c1df6160440/coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl",hashes = {sha256 = "a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0"}}, - {name = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/bb/3b/8442618972c51a7affeead957995cfa8323c0c9bcf8fa5a027421f720ff4/coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399"}}, - {name = "coverage-7.10.7-cp314-cp314t-win32.whl",url = "https://files.pythonhosted.org/packages/b2/dc/101f3fa3a45146db0cb03f5b4376e24c0aac818309da23e2de0c75295a91/coverage-7.10.7-cp314-cp314t-win32.whl",hashes = {sha256 = "67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235"}}, - {name = "coverage-7.10.7-cp314-cp314t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/4c/a1/74c51803fc70a8a40d7346660379e144be772bab4ac7bb6e6b905152345c/coverage-7.10.7-cp314-cp314t-win_amd64.whl",hashes = {sha256 = "e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d"}}, - {name = "coverage-7.10.7-cp314-cp314t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/12/65/f116a6d2127df30bcafbceef0302d8a64ba87488bf6f73a6d8eebf060873/coverage-7.10.7-cp314-cp314t-win_arm64.whl",hashes = {sha256 = "7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a"}}, - {name = "coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/9a/94/b765c1abcb613d103b64fcf10395f54d69b0ef8be6a0dd9c524384892cc7/coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl",hashes = {sha256 = "981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d"}}, - {name = "coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/72/4f/732fff31c119bb73b35236dd333030f32c4bfe909f445b423e6c7594f9a2/coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b"}}, - {name = "coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/87/02/ae7e0af4b674be47566707777db1aa375474f02a1d64b9323e5813a6cdd5/coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e"}}, - {name = "coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/a2/77/8c6d22bf61921a59bce5471c2f1f7ac30cd4ac50aadde72b8c48d5727902/coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b"}}, - {name = "coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/b1/20/b6ea4f69bbb52dac0aebd62157ba6a9dddbfe664f5af8122dac296c3ee15/coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49"}}, - {name = "coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/f9/28/4831523ba483a7f90f7b259d2018fef02cb4d5b90bc7c1505d6e5a84883c/coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911"}}, - {name = "coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/a7/9f/4331142bc98c10ca6436d2d620c3e165f31e6c58d43479985afce6f3191c/coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0"}}, - {name = "coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/ce/60/bda83b96602036b77ecf34e6393a3836365481b69f7ed7079ab85048202b/coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl",hashes = {sha256 = "b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f"}}, - {name = "coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/5f/af/152633ff35b2af63977edd835d8e6430f0caef27d171edf2fc76c270ef31/coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl",hashes = {sha256 = "b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c"}}, - {name = "coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/9d/71/d92105d122bd21cebba877228990e1646d862e34a98bb3374d3fece5a794/coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f"}}, - {name = "coverage-7.10.7-cp313-cp313-win32.whl",url = "https://files.pythonhosted.org/packages/a2/9e/9fdb08f4bf476c912f0c3ca292e019aab6712c93c9344a1653986c3fd305/coverage-7.10.7-cp313-cp313-win32.whl",hashes = {sha256 = "dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698"}}, - {name = "coverage-7.10.7-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/b1/b1/a75fd25df44eab52d1931e89980d1ada46824c7a3210be0d3c88a44aaa99/coverage-7.10.7-cp313-cp313-win_amd64.whl",hashes = {sha256 = "cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843"}}, - {name = "coverage-7.10.7-cp313-cp313-win_arm64.whl",url = "https://files.pythonhosted.org/packages/14/3a/d720d7c989562a6e9a14b2c9f5f2876bdb38e9367126d118495b89c99c37/coverage-7.10.7-cp313-cp313-win_arm64.whl",hashes = {sha256 = "4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546"}}, - {name = "coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/bb/22/e04514bf2a735d8b0add31d2b4ab636fc02370730787c576bb995390d2d5/coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl",hashes = {sha256 = "a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c"}}, - {name = "coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/11/0b/91128e099035ece15da3445d9015e4b4153a6059403452d324cbb0a575fa/coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl",hashes = {sha256 = "dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15"}}, - {name = "coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/8b/51/66420081e72801536a091a0c8f8c1f88a5c4bf7b9b1bdc6222c7afe6dc9b/coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4"}}, - {name = "coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/5d/22/9b8d458c2881b22df3db5bb3e7369e63d527d986decb6c11a591ba2364f7/coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0"}}, - {name = "coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/f7/08/16bee2c433e60913c610ea200b276e8eeef084b0d200bdcff69920bd5828/coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0"}}, - {name = "coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/20/9d/e53eb9771d154859b084b90201e5221bca7674ba449a17c101a5031d4054/coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65"}}, - {name = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/ad/b0/69bc7050f8d4e56a89fb550a1577d5d0d1db2278106f6f626464067b3817/coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541"}}, - {name = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/ef/4b/2514b060dbd1bc0aaf23b852c14bb5818f244c664cb16517feff6bb3a5ab/coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl",hashes = {sha256 = "2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6"}}, - {name = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/54/78/7ba2175007c246d75e496f64c06e94122bdb914790a1285d627a918bd271/coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl",hashes = {sha256 = "0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999"}}, - {name = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/c0/b3/fac9f7abbc841409b9a410309d73bfa6cfb2e51c3fada738cb607ce174f8/coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2"}}, - {name = "coverage-7.10.7-cp313-cp313t-win32.whl",url = "https://files.pythonhosted.org/packages/ee/51/a03bec00d37faaa891b3ff7387192cef20f01604e5283a5fabc95346befa/coverage-7.10.7-cp313-cp313t-win32.whl",hashes = {sha256 = "2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a"}}, - {name = "coverage-7.10.7-cp313-cp313t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/53/22/3cf25d614e64bf6d8e59c7c669b20d6d940bb337bdee5900b9ca41c820bb/coverage-7.10.7-cp313-cp313t-win_amd64.whl",hashes = {sha256 = "33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb"}}, - {name = "coverage-7.10.7-cp313-cp313t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/49/a1/00164f6d30d8a01c3c9c48418a7a5be394de5349b421b9ee019f380df2a0/coverage-7.10.7-cp313-cp313t-win_arm64.whl",hashes = {sha256 = "86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb"}}, - {name = "coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/13/e4/eb12450f71b542a53972d19117ea5a5cea1cab3ac9e31b0b5d498df1bd5a/coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl",hashes = {sha256 = "7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417"}}, - {name = "coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/37/66/593f9be12fc19fb36711f19a5371af79a718537204d16ea1d36f16bd78d2/coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973"}}, - {name = "coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/66/80/4c49f7ae09cafdacc73fbc30949ffe77359635c168f4e9ff33c9ebb07838/coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c"}}, - {name = "coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/a6/90/a64aaacab3b37a17aaedd83e8000142561a29eb262cede42d94a67f7556b/coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7"}}, - {name = "coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/98/2e/2dda59afd6103b342e096f246ebc5f87a3363b5412609946c120f4e7750d/coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6"}}, - {name = "coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/53/dc/8d8119c9051d50f3119bb4a75f29f1e4a6ab9415cd1fa8bf22fcc3fb3b5f/coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59"}}, - {name = "coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/98/b3/edaff9c5d79ee4d4b6d3fe046f2b1d799850425695b789d491a64225d493/coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b"}}, - {name = "coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/11/25/9a0728564bb05863f7e513e5a594fe5ffef091b325437f5430e8cfb0d530/coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl",hashes = {sha256 = "78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a"}}, - {name = "coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/e0/fd/ca2650443bfbef5b0e74373aac4df67b08180d2f184b482c41499668e258/coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl",hashes = {sha256 = "5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb"}}, - {name = "coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/24/79/f692f125fb4299b6f963b0745124998ebb8e73ecdfce4ceceb06a8c6bec5/coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1"}}, - {name = "coverage-7.10.7-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/5e/75/61b9bbd6c7d24d896bfeec57acba78e0f8deac68e6baf2d4804f7aae1f88/coverage-7.10.7-cp312-cp312-win32.whl",hashes = {sha256 = "77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256"}}, - {name = "coverage-7.10.7-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/ca/f3/3bf7905288b45b075918d372498f1cf845b5b579b723c8fd17168018d5f5/coverage-7.10.7-cp312-cp312-win_amd64.whl",hashes = {sha256 = "f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba"}}, - {name = "coverage-7.10.7-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/5c/44/3e32dbe933979d05cf2dac5e697c8599cfe038aaf51223ab901e208d5a62/coverage-7.10.7-cp312-cp312-win_arm64.whl",hashes = {sha256 = "bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf"}}, - {name = "coverage-7.10.7-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl",hashes = {sha256 = "f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260"}}, - {name = "coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/d2/5d/c1a17867b0456f2e9ce2d8d4708a4c3a089947d0bec9c66cdf60c9e7739f/coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl",hashes = {sha256 = "a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59"}}, - {name = "coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/54/f0/514dcf4b4e3698b9a9077f084429681bf3aad2b4a72578f89d7f643eb506/coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a"}}, - {name = "coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/20/f6/9626b81d17e2a4b25c63ac1b425ff307ecdeef03d67c9a147673ae40dc36/coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699"}}, - {name = "coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/b0/ef/bd8e719c2f7417ba03239052e099b76ea1130ac0cbb183ee1fcaa58aaff3/coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d"}}, - {name = "coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/a5/b6/bf054de41ec948b151ae2b79a55c107f5760979538f5fb80c195f2517718/coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e"}}, - {name = "coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/0f/e5/3860756aa6f9318227443c6ce4ed7bf9e70bb7f1447a0353f45ac5c7974b/coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23"}}, - {name = "coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/26/0f/bd08bd042854f7fd07b45808927ebcce99a7ed0f2f412d11629883517ac2/coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab"}}, - {name = "coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/8e/a7/4777b14de4abcc2e80c6b1d430f5d51eb18ed1d75fca56cbce5f2db9b36e/coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl",hashes = {sha256 = "121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82"}}, - {name = "coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/34/72/17d082b00b53cd45679bad682fac058b87f011fd8b9fe31d77f5f8d3a4e4/coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl",hashes = {sha256 = "88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2"}}, - {name = "coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/81/7a/92367572eb5bdd6a84bfa278cc7e97db192f9f45b28c94a9ca1a921c3577/coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61"}}, - {name = "coverage-7.10.7-cp311-cp311-win32.whl",url = "https://files.pythonhosted.org/packages/2f/88/a23cc185f6a805dfc4fdf14a94016835eeb85e22ac3a0e66d5e89acd6462/coverage-7.10.7-cp311-cp311-win32.whl",hashes = {sha256 = "972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14"}}, - {name = "coverage-7.10.7-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/fe/ef/0b510a399dfca17cec7bc2f05ad8bd78cf55f15c8bc9a73ab20c5c913c2e/coverage-7.10.7-cp311-cp311-win_amd64.whl",hashes = {sha256 = "a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2"}}, - {name = "coverage-7.10.7-cp311-cp311-win_arm64.whl",url = "https://files.pythonhosted.org/packages/51/7f/023657f301a276e4ba1850f82749bc136f5a7e8768060c2e5d9744a22951/coverage-7.10.7-cp311-cp311-win_arm64.whl",hashes = {sha256 = "736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a"}}, - {name = "coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/e5/6c/3a3f7a46888e69d18abe3ccc6fe4cb16cccb1e6a2f99698931dafca489e6/coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl",hashes = {sha256 = "fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a"}}, - {name = "coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/03/94/952d30f180b1a916c11a56f5c22d3535e943aa22430e9e3322447e520e1c/coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5"}}, - {name = "coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/50/2b/9e0cf8ded1e114bcd8b2fd42792b57f1c4e9e4ea1824cde2af93a67305be/coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17"}}, - {name = "coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/19/20/d0384ac06a6f908783d9b6aa6135e41b093971499ec488e47279f5b846e6/coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b"}}, - {name = "coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/60/83/5c283cff3d41285f8eab897651585db908a909c572bdc014bcfaf8a8b6ae/coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87"}}, - {name = "coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/60/22/02eb98fdc5ff79f423e990d877693e5310ae1eab6cb20ae0b0b9ac45b23b/coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e"}}, - {name = "coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/b4/bc/25c83bcf3ad141b32cd7dc45485ef3c01a776ca3aa8ef0a93e77e8b5bc43/coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e"}}, - {name = "coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/3c/b7/95574702888b58c0928a6e982038c596f9c34d52c5e5107f1eef729399b5/coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl",hashes = {sha256 = "b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df"}}, - {name = "coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/47/b6/40095c185f235e085df0e0b158f6bd68cc6e1d80ba6c7721dc81d97ec318/coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl",hashes = {sha256 = "606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0"}}, - {name = "coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/c8/50/4aea0556da7a4b93ec9168420d170b55e2eb50ae21b25062513d020c6861/coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13"}}, - {name = "coverage-7.10.7-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/6a/28/ea1a84a60828177ae3b100cb6723838523369a44ec5742313ed7db3da160/coverage-7.10.7-cp310-cp310-win32.whl",hashes = {sha256 = "b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b"}}, - {name = "coverage-7.10.7-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/fc/1a/a81d46bbeb3c3fd97b9602ebaa411e076219a150489bcc2c025f151bd52d/coverage-7.10.7-cp310-cp310-win_amd64.whl",hashes = {sha256 = "3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807"}}, +version = "7.11.0" +requires-python = ">=3.10" +sdist = {name = "coverage-7.11.0.tar.gz", url = "https://files.pythonhosted.org/packages/1c/38/ee22495420457259d2f3390309505ea98f98a5eed40901cf62196abad006/coverage-7.11.0.tar.gz", hashes = {sha256 = "167bd504ac1ca2af7ff3b81d245dfea0292c5032ebef9d66cc08a7d28c1b8050"}} +wheels = [ + {name = "coverage-7.11.0-cp314-cp314-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/f4/06/e923830c1985ce808e40a3fa3eb46c13350b3224b7da59757d37b6ce12b8/coverage-7.11.0-cp314-cp314-macosx_10_13_x86_64.whl",hashes = {sha256 = "c770885b28fb399aaf2a65bbd1c12bf6f307ffd112d6a76c5231a94276f0c497"}}, + {name = "coverage-7.11.0-cp314-cp314-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/42/82/cdeed03bfead45203fb651ed756dfb5266028f5f939e7f06efac4041dad5/coverage-7.11.0-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "a3d0e2087dba64c86a6b254f43e12d264b636a39e88c5cc0a01a7c71bcfdab7e"}}, + {name = "coverage-7.11.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/fc/ba/e1c80caffc3199aa699813f73ff097bc2df7b31642bdbc7493600a8f1de5/coverage-7.11.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "73feb83bb41c32811973b8565f3705caf01d928d972b72042b44e97c71fd70d1"}}, + {name = "coverage-7.11.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/80/c0/5b259b029694ce0a5bbc1548834c7ba3db41d3efd3474489d7efce4ceb18/coverage-7.11.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "c6f31f281012235ad08f9a560976cc2fc9c95c17604ff3ab20120fe480169bca"}}, + {name = "coverage-7.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/8c/86/171b2b5e1aac7e2fd9b43f7158b987dbeb95f06d1fbecad54ad8163ae3e8/coverage-7.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "e9570ad567f880ef675673992222746a124b9595506826b210fbe0ce3f0499cd"}}, + {name = "coverage-7.11.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/1a/7e/7e10414d343385b92024af3932a27a1caf75c6e27ee88ba211221ff1a145/coverage-7.11.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "8badf70446042553a773547a61fecaa734b55dc738cacf20c56ab04b77425e43"}}, + {name = "coverage-7.11.0-cp314-cp314-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/c4/3b/e4f966b21f5be8c4bf86ad75ae94efa0de4c99c7bbb8114476323102e345/coverage-7.11.0-cp314-cp314-musllinux_1_2_aarch64.whl",hashes = {sha256 = "a09c1211959903a479e389685b7feb8a17f59ec5a4ef9afde7650bd5eabc2777"}}, + {name = "coverage-7.11.0-cp314-cp314-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/00/a2/8479325576dfcd909244d0df215f077f47437ab852ab778cfa2f8bf4d954/coverage-7.11.0-cp314-cp314-musllinux_1_2_i686.whl",hashes = {sha256 = "5ef83b107f50db3f9ae40f69e34b3bd9337456c5a7fe3461c7abf8b75dd666a2"}}, + {name = "coverage-7.11.0-cp314-cp314-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/7b/d8/3a9e2db19d94d65771d0f2e21a9ea587d11b831332a73622f901157cc24b/coverage-7.11.0-cp314-cp314-musllinux_1_2_riscv64.whl",hashes = {sha256 = "f91f927a3215b8907e214af77200250bb6aae36eca3f760f89780d13e495388d"}}, + {name = "coverage-7.11.0-cp314-cp314-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/b3/b1/bbca3c472544f9e2ad2d5116b2379732957048be4b93a9c543fcd0207e5f/coverage-7.11.0-cp314-cp314-musllinux_1_2_x86_64.whl",hashes = {sha256 = "cdbcd376716d6b7fbfeedd687a6c4be019c5a5671b35f804ba76a4c0a778cba4"}}, + {name = "coverage-7.11.0-cp314-cp314-win32.whl",url = "https://files.pythonhosted.org/packages/89/49/638d5a45a6a0f00af53d6b637c87007eb2297042186334e9923a61aa8854/coverage-7.11.0-cp314-cp314-win32.whl",hashes = {sha256 = "bab7ec4bb501743edc63609320aaec8cd9188b396354f482f4de4d40a9d10721"}}, + {name = "coverage-7.11.0-cp314-cp314-win_amd64.whl",url = "https://files.pythonhosted.org/packages/30/cc/b675a51f2d068adb3cdf3799212c662239b0ca27f4691d1fff81b92ea850/coverage-7.11.0-cp314-cp314-win_amd64.whl",hashes = {sha256 = "3d4ba9a449e9364a936a27322b20d32d8b166553bfe63059bd21527e681e2fad"}}, + {name = "coverage-7.11.0-cp314-cp314-win_arm64.whl",url = "https://files.pythonhosted.org/packages/93/98/5ac886876026de04f00820e5094fe22166b98dcb8b426bf6827aaf67048c/coverage-7.11.0-cp314-cp314-win_arm64.whl",hashes = {sha256 = "ce37f215223af94ef0f75ac68ea096f9f8e8c8ec7d6e8c346ee45c0d363f0479"}}, + {name = "coverage-7.11.0-cp314-cp314t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/14/d1/b4145d35b3e3ecf4d917e97fc8895bcf027d854879ba401d9ff0f533f997/coverage-7.11.0-cp314-cp314t-macosx_10_13_x86_64.whl",hashes = {sha256 = "f413ce6e07e0d0dc9c433228727b619871532674b45165abafe201f200cc215f"}}, + {name = "coverage-7.11.0-cp314-cp314t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/ca/d1/7f645fc2eccd318369a8a9948acc447bb7c1ade2911e31d3c5620544c22b/coverage-7.11.0-cp314-cp314t-macosx_11_0_arm64.whl",hashes = {sha256 = "05791e528a18f7072bf5998ba772fe29db4da1234c45c2087866b5ba4dea710e"}}, + {name = "coverage-7.11.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/54/7d/64d124649db2737ceced1dfcbdcb79898d5868d311730f622f8ecae84250/coverage-7.11.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "cacb29f420cfeb9283b803263c3b9a068924474ff19ca126ba9103e1278dfa44"}}, + {name = "coverage-7.11.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/6c/3f/6f5922f80dc6f2d8b2c6f974835c43f53eb4257a7797727e6ca5b7b2ec1f/coverage-7.11.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "314c24e700d7027ae3ab0d95fbf8d53544fca1f20345fd30cd219b737c6e58d3"}}, + {name = "coverage-7.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/0e/5f/9e883523c4647c860b3812b417a2017e361eca5b635ee658387dc11b13c1/coverage-7.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "630d0bd7a293ad2fc8b4b94e5758c8b2536fdf36c05f1681270203e463cbfa9b"}}, + {name = "coverage-7.11.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/07/bb/43b5a8e94c09c8bf51743ffc65c4c841a4ca5d3ed191d0a6919c379a1b83/coverage-7.11.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "e89641f5175d65e2dbb44db15fe4ea48fade5d5bbb9868fdc2b4fce22f4a469d"}}, + {name = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/aa/e5/0ead8af411411330b928733e1d201384b39251a5f043c1612970310e8283/coverage-7.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "c9f08ea03114a637dab06cedb2e914da9dc67fa52c6015c018ff43fdde25b9c2"}}, + {name = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/ae/66/03dd8bb0ba5b971620dcaac145461950f6d8204953e535d2b20c6b65d729/coverage-7.11.0-cp314-cp314t-musllinux_1_2_i686.whl",hashes = {sha256 = "ce9f3bde4e9b031eaf1eb61df95c1401427029ea1bfddb8621c1161dcb0fa02e"}}, + {name = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/45/ae/28a9cce40bf3174426cb2f7e71ee172d98e7f6446dff936a7ccecee34b14/coverage-7.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl",hashes = {sha256 = "e4dc07e95495923d6fd4d6c27bf70769425b71c89053083843fd78f378558996"}}, + {name = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/5c/7c/3a44234a8599513684bfc8684878fd7b126c2760f79712bb78c56f19efc4/coverage-7.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "424538266794db2861db4922b05d729ade0940ee69dcf0591ce8f69784db0e11"}}, + {name = "coverage-7.11.0-cp314-cp314t-win32.whl",url = "https://files.pythonhosted.org/packages/e1/e6/0108519cba871af0351725ebdb8660fd7a0fe2ba3850d56d32490c7d9b4b/coverage-7.11.0-cp314-cp314t-win32.whl",hashes = {sha256 = "4c1eeb3fb8eb9e0190bebafd0462936f75717687117339f708f395fe455acc73"}}, + {name = "coverage-7.11.0-cp314-cp314t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/c9/76/44ba876e0942b4e62fdde23ccb029ddb16d19ba1bef081edd00857ba0b16/coverage-7.11.0-cp314-cp314t-win_amd64.whl",hashes = {sha256 = "b56efee146c98dbf2cf5cffc61b9829d1e94442df4d7398b26892a53992d3547"}}, + {name = "coverage-7.11.0-cp314-cp314t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/b9/0c/0df55ecb20d0d0ed5c322e10a441775e1a3a5d78c60f0c4e1abfe6fcf949/coverage-7.11.0-cp314-cp314t-win_arm64.whl",hashes = {sha256 = "b5c2705afa83f49bd91962a4094b6b082f94aef7626365ab3f8f4bd159c5acf3"}}, + {name = "coverage-7.11.0-cp313-cp313-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/60/7f/85e4dfe65e400645464b25c036a26ac226cf3a69d4a50c3934c532491cdd/coverage-7.11.0-cp313-cp313-macosx_10_13_x86_64.whl",hashes = {sha256 = "cc3f49e65ea6e0d5d9bd60368684fe52a704d46f9e7fc413918f18d046ec40e1"}}, + {name = "coverage-7.11.0-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/96/5d/dc5fa98fea3c175caf9d360649cb1aa3715e391ab00dc78c4c66fabd7356/coverage-7.11.0-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "f39ae2f63f37472c17b4990f794035c9890418b1b8cca75c01193f3c8d3e01be"}}, + {name = "coverage-7.11.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/b2/f5/3da9cc9596708273385189289c0e4d8197d37a386bdf17619013554b3447/coverage-7.11.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "7db53b5cdd2917b6eaadd0b1251cf4e7d96f4a8d24e174bdbdf2f65b5ea7994d"}}, + {name = "coverage-7.11.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/65/6c/f7f59c342359a235559d2bc76b0c73cfc4bac7d61bb0df210965cb1ecffd/coverage-7.11.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "10ad04ac3a122048688387828b4537bc9cf60c0bf4869c1e9989c46e45690b82"}}, + {name = "coverage-7.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/e7/8c/042dede2e23525e863bf1ccd2b92689692a148d8b5fd37c37899ba882645/coverage-7.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "4036cc9c7983a2b1f2556d574d2eb2154ac6ed55114761685657e38782b23f52"}}, + {name = "coverage-7.11.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/7b/a9/3c58df67bfa809a7bddd786356d9c5283e45d693edb5f3f55d0986dd905a/coverage-7.11.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "7ab934dd13b1c5e94b692b1e01bd87e4488cb746e3a50f798cb9464fd128374b"}}, + {name = "coverage-7.11.0-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/26/5b/c7f32efd862ee0477a18c41e4761305de6ddd2d49cdeda0c1116227570fd/coverage-7.11.0-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "59a6e5a265f7cfc05f76e3bb53eca2e0dfe90f05e07e849930fecd6abb8f40b4"}}, + {name = "coverage-7.11.0-cp313-cp313-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/76/b5/78cb4f1e86c1611431c990423ec0768122905b03837e1b4c6a6f388a858b/coverage-7.11.0-cp313-cp313-musllinux_1_2_i686.whl",hashes = {sha256 = "df01d6c4c81e15a7c88337b795bb7595a8596e92310266b5072c7e301168efbd"}}, + {name = "coverage-7.11.0-cp313-cp313-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/87/c9/23c753a8641a330f45f221286e707c427e46d0ffd1719b080cedc984ec40/coverage-7.11.0-cp313-cp313-musllinux_1_2_riscv64.whl",hashes = {sha256 = "8c934bd088eed6174210942761e38ee81d28c46de0132ebb1801dbe36a390dcc"}}, + {name = "coverage-7.11.0-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/c5/42/6e0cc71dc8a464486e944a4fa0d85bdec031cc2969e98ed41532a98336b9/coverage-7.11.0-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "5a03eaf7ec24078ad64a07f02e30060aaf22b91dedf31a6b24d0d98d2bba7f48"}}, + {name = "coverage-7.11.0-cp313-cp313-win32.whl",url = "https://files.pythonhosted.org/packages/e8/1c/743c2ef665e6858cccb0f84377dfe3a4c25add51e8c7ef19249be92465b6/coverage-7.11.0-cp313-cp313-win32.whl",hashes = {sha256 = "695340f698a5f56f795b2836abe6fb576e7c53d48cd155ad2f80fd24bc63a040"}}, + {name = "coverage-7.11.0-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/ff/d5/226daadfd1bf8ddbccefbd3aa3547d7b960fb48e1bdac124e2dd13a2b71a/coverage-7.11.0-cp313-cp313-win_amd64.whl",hashes = {sha256 = "2727d47fce3ee2bac648528e41455d1b0c46395a087a229deac75e9f88ba5a05"}}, + {name = "coverage-7.11.0-cp313-cp313-win_arm64.whl",url = "https://files.pythonhosted.org/packages/97/54/47db81dcbe571a48a298f206183ba8a7ba79200a37cd0d9f4788fcd2af4a/coverage-7.11.0-cp313-cp313-win_arm64.whl",hashes = {sha256 = "0efa742f431529699712b92ecdf22de8ff198df41e43aeaaadf69973eb93f17a"}}, + {name = "coverage-7.11.0-cp313-cp313t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/e5/8b/cb68425420154e7e2a82fd779a8cc01549b6fa83c2ad3679cd6c088ebd07/coverage-7.11.0-cp313-cp313t-macosx_10_13_x86_64.whl",hashes = {sha256 = "587c38849b853b157706407e9ebdca8fd12f45869edb56defbef2daa5fb0812b"}}, + {name = "coverage-7.11.0-cp313-cp313t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/33/55/9d61b5765a025685e14659c8d07037247de6383c0385757544ffe4606475/coverage-7.11.0-cp313-cp313t-macosx_11_0_arm64.whl",hashes = {sha256 = "b971bdefdd75096163dd4261c74be813c4508477e39ff7b92191dea19f24cd37"}}, + {name = "coverage-7.11.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/52/85/292459c9186d70dcec6538f06ea251bc968046922497377bf4a1dc9a71de/coverage-7.11.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "269bfe913b7d5be12ab13a95f3a76da23cf147be7fa043933320ba5625f0a8de"}}, + {name = "coverage-7.11.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/1f/e2/46edd73fb8bf51446c41148d81944c54ed224854812b6ca549be25113ee0/coverage-7.11.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "dadbcce51a10c07b7c72b0ce4a25e4b6dcb0c0372846afb8e5b6307a121eb99f"}}, + {name = "coverage-7.11.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/07/5e/1df469a19007ff82e2ca8fe509822820a31e251f80ee7344c34f6cd2ec43/coverage-7.11.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "9ed43fa22c6436f7957df036331f8fe4efa7af132054e1844918866cd228af6c"}}, + {name = "coverage-7.11.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/f9/50/de216b31a1434b94d9b34a964c09943c6be45069ec704bfc379d8d89a649/coverage-7.11.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "9516add7256b6713ec08359b7b05aeff8850c98d357784c7205b2e60aa2513fa"}}, + {name = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/82/1e/3f9f8344a48111e152e0fd495b6fff13cc743e771a6050abf1627a7ba918/coverage-7.11.0-cp313-cp313t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "eb92e47c92fcbcdc692f428da67db33337fa213756f7adb6a011f7b5a7a20740"}}, + {name = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/65/9b/3f52741f9e7d82124272f3070bbe316006a7de1bad1093f88d59bfc6c548/coverage-7.11.0-cp313-cp313t-musllinux_1_2_i686.whl",hashes = {sha256 = "d06f4fc7acf3cabd6d74941d53329e06bab00a8fe10e4df2714f0b134bfc64ef"}}, + {name = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/0b/8b/918f0e15f0365d50d3986bbd3338ca01178717ac5678301f3f547b6619e6/coverage-7.11.0-cp313-cp313t-musllinux_1_2_riscv64.whl",hashes = {sha256 = "6fbcee1a8f056af07ecd344482f711f563a9eb1c2cad192e87df00338ec3cdb0"}}, + {name = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/44/9e/7776829f82d3cf630878a7965a7d70cc6ca94f22c7d20ec4944f7148cb46/coverage-7.11.0-cp313-cp313t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "dbbf012be5f32533a490709ad597ad8a8ff80c582a95adc8d62af664e532f9ca"}}, + {name = "coverage-7.11.0-cp313-cp313t-win32.whl",url = "https://files.pythonhosted.org/packages/9a/b8/49cf253e1e7a3bedb85199b201862dd7ca4859f75b6cf25ffa7298aa0760/coverage-7.11.0-cp313-cp313t-win32.whl",hashes = {sha256 = "cee6291bb4fed184f1c2b663606a115c743df98a537c969c3c64b49989da96c2"}}, + {name = "coverage-7.11.0-cp313-cp313t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/ac/e1/1a541703826be7ae2125a0fb7f821af5729d56bb71e946e7b933cc7a89a4/coverage-7.11.0-cp313-cp313t-win_amd64.whl",hashes = {sha256 = "a386c1061bf98e7ea4758e4313c0ab5ecf57af341ef0f43a0bf26c2477b5c268"}}, + {name = "coverage-7.11.0-cp313-cp313t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/d5/d1/5ee0e0a08621140fd418ec4020f595b4d52d7eb429ae6a0c6542b4ba6f14/coverage-7.11.0-cp313-cp313t-win_arm64.whl",hashes = {sha256 = "f9ea02ef40bb83823b2b04964459d281688fe173e20643870bb5d2edf68bc836"}}, + {name = "coverage-7.11.0-cp312-cp312-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/c4/db/86f6906a7c7edc1a52b2c6682d6dd9be775d73c0dfe2b84f8923dfea5784/coverage-7.11.0-cp312-cp312-macosx_10_13_x86_64.whl",hashes = {sha256 = "9c49e77811cf9d024b95faf86c3f059b11c0c9be0b0d61bc598f453703bd6fd1"}}, + {name = "coverage-7.11.0-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/21/54/e7b26157048c7ba555596aad8569ff903d6cd67867d41b75287323678ede/coverage-7.11.0-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "a61e37a403a778e2cda2a6a39abcc895f1d984071942a41074b5c7ee31642007"}}, + {name = "coverage-7.11.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/b9/19/1ce6bf444f858b83a733171306134a0544eaddf1ca8851ede6540a55b2ad/coverage-7.11.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "c79cae102bb3b1801e2ef1511fb50e91ec83a1ce466b2c7c25010d884336de46"}}, + {name = "coverage-7.11.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/71/0b/d3bcbbc259fcced5fb67c5d78f6e7ee965f49760c14afd931e9e663a83b2/coverage-7.11.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "16ce17ceb5d211f320b62df002fa7016b7442ea0fd260c11cec8ce7730954893"}}, + {name = "coverage-7.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/58/8d/b0ff3641a320abb047258d36ed1c21d16be33beed4152628331a1baf3365/coverage-7.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "80027673e9d0bd6aef86134b0771845e2da85755cf686e7c7c59566cf5a89115"}}, + {name = "coverage-7.11.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/59/c8/5a586fe8c7b0458053d9c687f5cff515a74b66c85931f7fe17a1c958b4ac/coverage-7.11.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "4d3ffa07a08657306cd2215b0da53761c4d73cb54d9143b9303a6481ec0cd415"}}, + {name = "coverage-7.11.0-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/d0/ff/3a25e3132804ba44cfa9a778cdf2b73dbbe63ef4b0945e39602fc896ba52/coverage-7.11.0-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "a3b6a5f8b2524fd6c1066bc85bfd97e78709bb5e37b5b94911a6506b65f47186"}}, + {name = "coverage-7.11.0-cp312-cp312-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/c5/12/ff10c8ce3895e1b17a73485ea79ebc1896a9e466a9d0f4aef63e0d17b718/coverage-7.11.0-cp312-cp312-musllinux_1_2_i686.whl",hashes = {sha256 = "fcc0a4aa589de34bc56e1a80a740ee0f8c47611bdfb28cd1849de60660f3799d"}}, + {name = "coverage-7.11.0-cp312-cp312-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/16/02/d500b91f5471b2975947e0629b8980e5e90786fe316b6d7299852c1d793d/coverage-7.11.0-cp312-cp312-musllinux_1_2_riscv64.whl",hashes = {sha256 = "dba82204769d78c3fd31b35c3d5f46e06511936c5019c39f98320e05b08f794d"}}, + {name = "coverage-7.11.0-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/77/11/dee0284fbbd9cd64cfce806b827452c6df3f100d9e66188e82dfe771d4af/coverage-7.11.0-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "81b335f03ba67309a95210caf3eb43bd6fe75a4e22ba653ef97b4696c56c7ec2"}}, + {name = "coverage-7.11.0-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/59/1b/cdf1def928f0a150a057cab03286774e73e29c2395f0d30ce3d9e9f8e697/coverage-7.11.0-cp312-cp312-win32.whl",hashes = {sha256 = "037b2d064c2f8cc8716fe4d39cb705779af3fbf1ba318dc96a1af858888c7bb5"}}, + {name = "coverage-7.11.0-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/ff/55/e5884d55e031da9c15b94b90a23beccc9d6beee65e9835cd6da0a79e4f3a/coverage-7.11.0-cp312-cp312-win_amd64.whl",hashes = {sha256 = "d66c0104aec3b75e5fd897e7940188ea1892ca1d0235316bf89286d6a22568c0"}}, + {name = "coverage-7.11.0-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/23/a8/faa930cfc71c1d16bc78f9a19bb73700464f9c331d9e547bfbc1dbd3a108/coverage-7.11.0-cp312-cp312-win_arm64.whl",hashes = {sha256 = "d91ebeac603812a09cf6a886ba6e464f3bbb367411904ae3790dfe28311b15ad"}}, + {name = "coverage-7.11.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/5f/04/642c1d8a448ae5ea1369eac8495740a79eb4e581a9fb0cbdce56bbf56da1/coverage-7.11.0-py3-none-any.whl",hashes = {sha256 = "4b7589765348d78fb4e5fb6ea35d07564e387da2fc5efff62e0222971f155f68"}}, ] marker = "\"dev\" in extras" @@ -4500,7 +4475,7 @@ marker = "python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \" dependencies = [] [tool.pdm] -hashes = {sha256 = "5848d0fb723d7bb4169ae2d860b8f2a90af8c2be18b9bc7e6c6f5d79e7b71bc5"} +hashes = {sha256 = "e1e5eefffb30a72d8f9d4ec126e806fe0464e9453bfcca5adb26cf2e8b49103f"} strategy = ["inherit_metadata", "static_urls"] [[tool.pdm.targets]] diff --git a/pyproject.toml b/pyproject.toml index 2fa35ce0..1297881b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,6 @@ dependencies = [ "loguru", "msgpack", "numpy>=2.0.0", - "pillow", "protobuf", "pydantic>=2.11.7", "pydantic-settings>=2.0.0", @@ -82,7 +81,12 @@ recommended = ["guidellm[perf,openai]"] # Feature Extras perf = ["orjson", "msgpack", "msgspec", "uvloop"] openai = ["tiktoken>=0.11.0", "blobfile>=3.1.0"] -multimodal = ["datasets[audio,vision]>=4.1.0", "torchcodec==0.7", "torch==2.8.*"] +multimodal = [ + "datasets[audio,vision]>=4.1.0", + "pillow", + "torch==2.8.*", + "torchcodec==0.7", +] # Dev Tooling dev = [ # Install all optional dependencies diff --git a/src/guidellm/data/preprocessors/formatters.py b/src/guidellm/data/preprocessors/formatters.py index ce0e46fc..0b216085 100644 --- a/src/guidellm/data/preprocessors/formatters.py +++ b/src/guidellm/data/preprocessors/formatters.py @@ -1,5 +1,6 @@ from __future__ import annotations +from abc import ABCMeta from typing import Any from guidellm.data.preprocessors.preprocessor import ( @@ -7,7 +8,7 @@ PreprocessorRegistry, ) from guidellm.data.schemas import GenerativeDatasetColumnType -from guidellm.data.utils import encode_audio, encode_image, encode_video, text_stats +from guidellm.data.utils import text_stats from guidellm.schemas import GenerationRequest, GenerationRequestArguments, UsageMetrics __all__ = [ @@ -18,8 +19,28 @@ ] +class RequestFormatter(DatasetPreprocessor, metaclass=ABCMeta): + @staticmethod + def encode_audio(*args, **kwargs): + from guidellm.extras.multimodal import encode_audio + + return encode_audio(*args, **kwargs) + + @staticmethod + def encode_image(*args, **kwargs): + from guidellm.extras.multimodal import encode_image + + return encode_image(*args, **kwargs) + + @staticmethod + def encode_video(*args, **kwargs): + from guidellm.extras.multimodal import encode_video + + return encode_video(*args, **kwargs) + + @PreprocessorRegistry.register("text_completions") -class GenerativeTextCompletionsRequestFormatter(DatasetPreprocessor): +class GenerativeTextCompletionsRequestFormatter(RequestFormatter): def __init__( self, model: str, @@ -92,7 +113,7 @@ def __call__( @PreprocessorRegistry.register("chat_completions") -class GenerativeChatCompletionsRequestFormatter(DatasetPreprocessor): +class GenerativeChatCompletionsRequestFormatter(RequestFormatter): def __init__( self, model: str, @@ -120,7 +141,7 @@ def __init__( encode_kwargs.get("audio", {}) if encode_kwargs else {} ) - def __call__( + def __call__( # noqa: C901, PLR0912, PLR0915 self, columns: dict[GenerativeDatasetColumnType, list[Any]] ) -> GenerationRequest: arguments = GenerationRequestArguments(body={}) @@ -200,7 +221,7 @@ def __call__( if not image: continue - image_dict = encode_image(image, **self.encode_image_kwargs) + image_dict = self.encode_image(image, **self.encode_image_kwargs) if (image_pixels := image_dict.get("image_pixels")) is not None: input_metrics.image_pixels = ( input_metrics.image_pixels or 0 @@ -223,7 +244,7 @@ def __call__( if not video: continue - video_dict = encode_video(video, **self.encode_video_kwargs) + video_dict = self.encode_video(video, **self.encode_video_kwargs) if (video_frames := video_dict.get("video_frames")) is not None: input_metrics.video_frames = ( input_metrics.video_frames or 0 @@ -250,7 +271,9 @@ def __call__( if not audio: continue - audio_dict = encode_audio(audio, b64encode=True, **self.encode_audio_kwargs) + audio_dict = self.encode_audio( + audio, b64encode=True, **self.encode_audio_kwargs + ) if (audio_samples := audio_dict.get("audio_samples")) is not None: input_metrics.audio_samples = ( input_metrics.audio_samples or 0 @@ -288,7 +311,7 @@ def __call__( @PreprocessorRegistry.register("audio_transcriptions") -class GenerativeAudioTranscriptionRequestFormatter(DatasetPreprocessor): +class GenerativeAudioTranscriptionRequestFormatter(RequestFormatter): def __init__( self, model: str, @@ -345,7 +368,7 @@ def __call__( # noqa: C901 f"one audio column, but got {len(audio_columns)}." ) - audio_dict = encode_audio( + audio_dict = self.encode_audio( audio_columns[0], b64encode=False, **self.encode_audio_kwargs ) input_metrics.audio_samples = audio_dict.get("audio_samples") diff --git a/src/guidellm/data/utils/__init__.py b/src/guidellm/data/utils/__init__.py index cd257898..d71e6236 100644 --- a/src/guidellm/data/utils/__init__.py +++ b/src/guidellm/data/utils/__init__.py @@ -1,22 +1,10 @@ from .dataset import DEFAULT_SPLITS, resolve_dataset_split from .functions import ( - encode_audio, - encode_image, - encode_video, - get_file_format, - is_url, - resize_image, text_stats, ) __all__ = [ "DEFAULT_SPLITS", - "encode_audio", - "encode_image", - "encode_video", - "get_file_format", - "is_url", - "resize_image", "resolve_dataset_split", "text_stats", ] diff --git a/src/guidellm/data/utils/functions.py b/src/guidellm/data/utils/functions.py index baa5af76..4260b1f1 100644 --- a/src/guidellm/data/utils/functions.py +++ b/src/guidellm/data/utils/functions.py @@ -1,32 +1,6 @@ -from __future__ import annotations +from typing import Literal -import base64 -import io -from pathlib import Path -from typing import Any, Literal - -import httpx -import numpy as np -import torch -from PIL import Image as PILImage -from torch import Tensor -from torchcodec import AudioSamples -from torchcodec.decoders import AudioDecoder -from torchcodec.encoders import AudioEncoder - -__all__ = [ - "encode_audio", - "encode_image", - "encode_video", - "get_file_format", - "is_url", - "resize_image", - "text_stats", -] - - -def is_url(text: Any) -> bool: - return isinstance(text, str) and text.startswith(("http://", "https://")) +__all__ = ["text_stats"] def text_stats( @@ -42,394 +16,3 @@ def text_stats( "num_chars": num_chars, "num_words": num_words, } - - -def encode_image( - image: bytes | str | Path | np.ndarray | PILImage.Image, - width: int | None = None, - height: int | None = None, - max_size: int | None = None, - max_width: int | None = None, - max_height: int | None = None, - encode_type: Literal["base64", "url"] | None = "base64", -) -> dict[Literal["type", "image", "image_pixels", "image_bytes"], str | int | None]: - """ - Input image types: - - bytes: raw image bytes, decoded with Pillow - - str: file path on disk, url, or already base64 encoded image string - - pathlib.Path: file path on disk - - np.ndarray: image array, decoded with Pillow - - PIL.Image.Image: Pillow image - - datasets.Image: HuggingFace datasets Image object - - max_size: maximum size of the longest edge of the image - max_width: maximum width of the image - max_height: maximum height of the image - - encode_type: None to return the supported format - (url for url, base64 string for others) - "base64" to return base64 encoded string (or download URL and encode) - "url" to return url (only if input is url, otherwise fails) - - Returns a str of either: - - image url - - "data:image/{type};base64, {data}" string - """ - if isinstance(image, str) and is_url(image): - if encode_type == "base64": - response = httpx.get(image) - response.raise_for_status() - return encode_image( - image=response.content, - max_size=max_size, - max_width=max_width, - max_height=max_height, - encode_type="base64", - ) - - if any([width, height, max_size, max_width, max_height]): - raise ValueError(f"Cannot resize image {image} when encode_type is 'url'") - - return { - "type": "image_url", - "image": image, - "image_pixels": None, - "image_bytes": None, - } - - decoded_image: PILImage.Image - - if isinstance(image, bytes): - decoded_image = PILImage.open(io.BytesIO(image)) - elif isinstance(image, str) and image.startswith("data:image/"): - _, encoded = image.split(",", 1) - image_data = base64.b64decode(encoded) - decoded_image = PILImage.open(io.BytesIO(image_data)) - elif isinstance(image, str | Path): - decoded_image = PILImage.open(image) - elif isinstance(image, np.ndarray): - decoded_image = PILImage.fromarray(image) - elif isinstance(image, PILImage.Image): - decoded_image = image - else: - raise ValueError(f"Unsupported image type: {type(image)} for {image}") - - output_image = resize_image( - decoded_image, - width=width, - height=height, - max_width=max_width, - max_height=max_height, - max_size=max_size, - ) - if output_image.mode != "RGB": - output_image = output_image.convert("RGB") - - buffer = io.BytesIO() - output_image.save(buffer, format="JPEG") - image_bytes = buffer.getvalue() - image_base64 = base64.b64encode(image_bytes).decode("utf-8") - - return { - "type": "image_base64", - "image": f"data:image/jpeg;base64,{image_base64}", - "image_pixels": output_image.width * output_image.height, - "image_bytes": len(image_bytes), - } - - -def resize_image( - image: PILImage.Image, - width: int | None = None, - height: int | None = None, - max_width: int | None = None, - max_height: int | None = None, - max_size: int | None = None, -) -> PILImage.Image: - if not isinstance(image, PILImage.Image): - raise ValueError(f"Unsupported image type: {type(image)}") - - if width is not None and height is not None: - return image.resize((width, height), PILImage.Resampling.BILINEAR) - - orig_w, orig_h = image.size - aspect = orig_w / orig_h - - if width is not None: - target_w = width - target_h = round(width / aspect) - elif height is not None: - target_h = height - target_w = round(height * aspect) - else: - target_w, target_h = orig_w, orig_h - - # Normalize max_size → max_width/max_height - if max_size is not None: - max_width = max_width or max_size - max_height = max_height or max_size - - # Apply max constraints (preserve aspect ratio) - if max_width or max_height: - scale_w = max_width / target_w if max_width else 1.0 - scale_h = max_height / target_h if max_height else 1.0 - scale = min(scale_w, scale_h, 1.0) # never upscale - target_w = round(target_w * scale) - target_h = round(target_h * scale) - - if (target_w, target_h) != (orig_w, orig_h): - image = image.resize((target_w, target_h), PILImage.Resampling.BILINEAR) - - return image - - -def encode_video( - video: bytes | str | Path, - encode_type: Literal["base64", "url"] | None = "base64", -) -> dict[ - Literal["type", "video", "video_frames", "video_seconds", "video_bytes"], - str | int | float | None, -]: - """ - Input video types: - - bytes: raw video bytes - - str: file path on disk, url, or already base64 encoded video string - - pathlib.Path: file path on disk - - datasets.Video: HuggingFace datasets Video object - - encode_type: None to return the supported format - (url for url, base64 string for others) - "base64" to return base64 encoded string (or download URL and encode) - "url" to return url (only if input is url, otherwise fails) - - Returns a str of either: - - video url - - "data:video/{type};base64, {data}" string - """ - if isinstance(video, str) and is_url(video): - if encode_type == "base64": - response = httpx.get(video) - response.raise_for_status() - return encode_video(video=response.content, encode_type="base64") - - return { - "type": "video_url", - "video": video, - "video_frames": None, - "video_seconds": None, - "video_bytes": None, - } - - if isinstance(video, str) and video.startswith("data:video/"): - data_str = video.split(",", 1)[1] - - return { - "type": "video_base64", - "video": video, - "video_frames": None, - "video_seconds": None, - "video_bytes": len(data_str) * 3 // 4, # base64 to bytes - } - - if isinstance(video, str | Path): - path = Path(video) - video_bytes = path.read_bytes() - video_format = get_file_format(path) - elif isinstance(video, bytes): - video_bytes = video - video_format = "unknown" - else: - raise ValueError(f"Unsupported video type: {type(video)} for {video}") - - video_base64 = base64.b64encode(video).decode("utf-8") - - return { - "type": "video_base64", - "video": f"data:video/{video_format};base64,{video_base64}", - "video_frames": None, - "video_seconds": None, - "video_bytes": len(video_bytes), - } - - -def encode_audio( # noqa: C901 # noqa: PLR0913 - audio: AudioDecoder | bytes | str | Path | np.ndarray | Tensor | dict[str, Any], - b64encode: bool = False, - sample_rate: int | None = None, - file_name: str = "audio.wav", - encode_sample_rate: int = 16000, - max_duration: float | None = None, - mono: bool = True, - audio_format: str = "mp3", - bitrate: str = "64k", -) -> dict[ - Literal[ - "type", - "audio", - "format", - "mimetype", - "audio_samples", - "audio_seconds", - "audio_bytes", - ], - str | int | float | None, -]: - """Decode audio (if nessary) and re-encode to specified format.""" - samples = _decode_audio(audio, sample_rate=sample_rate, max_duration=max_duration) - - bitrate_val = ( - int(bitrate.rstrip("k")) * 1000 if bitrate.endswith("k") else int(bitrate) - ) - format_val = audio_format.lower() - - encoded_audio = _encode_audio( - samples=samples, - resample_rate=encode_sample_rate, - bitrate=bitrate_val, - audio_format=format_val, - mono=mono, - ) - - return { - "type": "audio_base64" if b64encode else "audio_file", - "audio": ( - base64.b64encode(encoded_audio).decode("utf-8") - if b64encode - else encoded_audio - ), - "file_name": get_file_name(audio) - if isinstance(audio, str | Path) - else file_name, - "format": audio_format, - "mimetype": f"audio/{format_val}", - "audio_samples": samples.sample_rate, - "audio_seconds": samples.duration_seconds, - "audio_bytes": len(encoded_audio), - } - - -def _decode_audio( # noqa: C901, PLR0912 - audio: AudioDecoder | bytes | str | Path | np.ndarray | Tensor | dict[str, Any], - sample_rate: int | None = None, - max_duration: float | None = None, -) -> AudioSamples: - """Decode audio from various input types into AudioSamples.""" - # If input is a dict, unwrap it into a function call - if isinstance(audio, dict): - sample_rate = audio.get("sample_rate", audio.get("sampling_rate", sample_rate)) - if "data" not in audio and "url" not in audio: - raise ValueError( - f"Audio dict must contain either 'data' or 'url' keys, got {audio}" - ) - return _decode_audio( - audio=audio.get("data") or audio.get("url"), - sample_rate=sample_rate, - max_duration=max_duration, - ) - - # Convert numpy array to torch tensor and re-call - if isinstance(audio, np.ndarray): - return _decode_audio( - audio=torch.from_numpy(audio), - sample_rate=sample_rate, - max_duration=max_duration, - ) - - samples: AudioSamples - - # HF datasets return AudioDecoder for audio column - if isinstance(audio, AudioDecoder): - samples = audio.get_samples_played_in_range(stop_seconds=max_duration) - - elif isinstance(audio, Tensor): - # If float stream assume decoded audio - if torch.is_floating_point(audio): - if sample_rate is None: - raise ValueError("Sample rate must be set for decoded audio") - - full_duration = audio.shape[1] / sample_rate - # If max_duration is set, trim the audio to that duration - if max_duration is not None: - num_samples = int(max_duration * sample_rate) - duration = min(max_duration, full_duration) - data = audio[:, :num_samples] - else: - duration = full_duration - data = audio - - samples = AudioSamples( - data=data, - pts_seconds=0.0, - duration_seconds=duration, - sample_rate=sample_rate, - ) - # If bytes tensor assume encoded audio - elif audio.dtype == torch.uint8: - decoder = AudioDecoder( - source=audio, - sample_rate=sample_rate, - ) - samples = decoder.get_samples_played_in_range(stop_seconds=max_duration) - - else: - raise ValueError(f"Unsupported audio type: {type(audio)}") - - # If bytes, assume encoded audio - elif isinstance(audio, bytes): - decoder = AudioDecoder( - source=audio, - sample_rate=sample_rate, - ) - samples = decoder.get_samples_played_in_range(stop_seconds=max_duration) - - # If str or Path, assume file path or URL to encoded audio - elif isinstance(audio, str | Path): - if isinstance(audio, str) and is_url(audio): - response = httpx.get(audio) - response.raise_for_status() - data = response.content - else: - if not Path(audio).exists(): - raise ValueError(f"Audio file does not exist: {audio}") - data = Path(audio).read_bytes() - decoder = AudioDecoder( - source=data, - ) - samples = decoder.get_samples_played_in_range(stop_seconds=max_duration) - else: - raise ValueError(f"Unsupported audio type: {type(audio)}") - - return samples - - -def _encode_audio( - samples: AudioSamples, - resample_rate: int | None = None, - bitrate: int = 64000, - audio_format: str = "mp3", - mono: bool = True, -) -> bytes: - encoder = AudioEncoder( - samples=samples.data, - sample_rate=samples.sample_rate, - ) - - audio_tensor = encoder.to_tensor( - format=audio_format, - bit_rate=bitrate if audio_format == "mp3" else None, - num_channels=1 if mono else None, - sample_rate=resample_rate, - ) - - return audio_tensor.numpy().tobytes() - - -def get_file_name(path: Path | str) -> str: - """Get file name from path.""" - return Path(path).name - - -def get_file_format(path: Path | str) -> str: - """Get file format from path extension.""" - suffix = Path(path).suffix.lower() - return suffix[1:] if suffix.startswith(".") else "unknown" diff --git a/src/guidellm/extras/__init__.py b/src/guidellm/extras/__init__.py new file mode 100644 index 00000000..b9f178ba --- /dev/null +++ b/src/guidellm/extras/__init__.py @@ -0,0 +1,4 @@ +""" +Code that depends on optional dependencies. +Each submodule should be defered imported. +""" diff --git a/src/guidellm/extras/multimodal.py b/src/guidellm/extras/multimodal.py new file mode 100644 index 00000000..80c5e716 --- /dev/null +++ b/src/guidellm/extras/multimodal.py @@ -0,0 +1,436 @@ +from __future__ import annotations + +import base64 +import io +from pathlib import Path +from typing import Any, Literal + +import httpx +import numpy as np +import torch + +try: + from PIL import Image as PILImage + from torchcodec import AudioSamples + from torchcodec.decoders import AudioDecoder + from torchcodec.encoders import AudioEncoder +except ImportError as e: + raise ImportError( + "Please install guidellm[multimodal] to use audio/image/video features" + ) from e + +__all__ = [ + "encode_audio", + "encode_image", + "encode_video", + "get_file_format", + "is_url", + "resize_image", +] + + +def is_url(text: Any) -> bool: + return isinstance(text, str) and text.startswith(("http://", "https://")) + + +def encode_image( + image: bytes | str | Path | np.ndarray | PILImage.Image, + width: int | None = None, + height: int | None = None, + max_size: int | None = None, + max_width: int | None = None, + max_height: int | None = None, + encode_type: Literal["base64", "url"] | None = "base64", +) -> dict[Literal["type", "image", "image_pixels", "image_bytes"], str | int | None]: + """ + Input image types: + - bytes: raw image bytes, decoded with Pillow + - str: file path on disk, url, or already base64 encoded image string + - pathlib.Path: file path on disk + - np.ndarray: image array, decoded with Pillow + - PIL.Image.Image: Pillow image + - datasets.Image: HuggingFace datasets Image object + + max_size: maximum size of the longest edge of the image + max_width: maximum width of the image + max_height: maximum height of the image + + encode_type: None to return the supported format + (url for url, base64 string for others) + "base64" to return base64 encoded string (or download URL and encode) + "url" to return url (only if input is url, otherwise fails) + + Returns a str of either: + - image url + - "data:image/{type};base64, {data}" string + """ + if isinstance(image, str) and is_url(image): + if encode_type == "base64": + response = httpx.get(image) + response.raise_for_status() + return encode_image( + image=response.content, + max_size=max_size, + max_width=max_width, + max_height=max_height, + encode_type="base64", + ) + + if any([width, height, max_size, max_width, max_height]): + raise ValueError(f"Cannot resize image {image} when encode_type is 'url'") + + return { + "type": "image_url", + "image": image, + "image_pixels": None, + "image_bytes": None, + } + + decoded_image: PILImage.Image + + if isinstance(image, bytes): + decoded_image = PILImage.open(io.BytesIO(image)) + elif isinstance(image, str) and image.startswith("data:image/"): + _, encoded = image.split(",", 1) + image_data = base64.b64decode(encoded) + decoded_image = PILImage.open(io.BytesIO(image_data)) + elif isinstance(image, str | Path): + decoded_image = PILImage.open(image) + elif isinstance(image, np.ndarray): + decoded_image = PILImage.fromarray(image) + elif isinstance(image, PILImage.Image): + decoded_image = image + else: + raise ValueError(f"Unsupported image type: {type(image)} for {image}") + + output_image = resize_image( + decoded_image, + width=width, + height=height, + max_width=max_width, + max_height=max_height, + max_size=max_size, + ) + if output_image.mode != "RGB": + output_image = output_image.convert("RGB") + + buffer = io.BytesIO() + output_image.save(buffer, format="JPEG") + image_bytes = buffer.getvalue() + image_base64 = base64.b64encode(image_bytes).decode("utf-8") + + return { + "type": "image_base64", + "image": f"data:image/jpeg;base64,{image_base64}", + "image_pixels": output_image.width * output_image.height, + "image_bytes": len(image_bytes), + } + + +def resize_image( + image: PILImage.Image, + width: int | None = None, + height: int | None = None, + max_width: int | None = None, + max_height: int | None = None, + max_size: int | None = None, +) -> PILImage.Image: + if not isinstance(image, PILImage.Image): + raise ValueError(f"Unsupported image type: {type(image)}") + + if width is not None and height is not None: + return image.resize((width, height), PILImage.Resampling.BILINEAR) + + orig_w, orig_h = image.size + aspect = orig_w / orig_h + + if width is not None: + target_w = width + target_h = round(width / aspect) + elif height is not None: + target_h = height + target_w = round(height * aspect) + else: + target_w, target_h = orig_w, orig_h + + # Normalize max_size → max_width/max_height + if max_size is not None: + max_width = max_width or max_size + max_height = max_height or max_size + + # Apply max constraints (preserve aspect ratio) + if max_width or max_height: + scale_w = max_width / target_w if max_width else 1.0 + scale_h = max_height / target_h if max_height else 1.0 + scale = min(scale_w, scale_h, 1.0) # never upscale + target_w = round(target_w * scale) + target_h = round(target_h * scale) + + if (target_w, target_h) != (orig_w, orig_h): + image = image.resize((target_w, target_h), PILImage.Resampling.BILINEAR) + + return image + + +def encode_video( + video: bytes | str | Path, + encode_type: Literal["base64", "url"] | None = "base64", +) -> dict[ + Literal["type", "video", "video_frames", "video_seconds", "video_bytes"], + str | int | float | None, +]: + """ + Input video types: + - bytes: raw video bytes + - str: file path on disk, url, or already base64 encoded video string + - pathlib.Path: file path on disk + - datasets.Video: HuggingFace datasets Video object + + encode_type: None to return the supported format + (url for url, base64 string for others) + "base64" to return base64 encoded string (or download URL and encode) + "url" to return url (only if input is url, otherwise fails) + + Returns a str of either: + - video url + - "data:video/{type};base64, {data}" string + """ + if isinstance(video, str) and is_url(video): + if encode_type == "base64": + response = httpx.get(video) + response.raise_for_status() + return encode_video(video=response.content, encode_type="base64") + + return { + "type": "video_url", + "video": video, + "video_frames": None, + "video_seconds": None, + "video_bytes": None, + } + + if isinstance(video, str) and video.startswith("data:video/"): + data_str = video.split(",", 1)[1] + + return { + "type": "video_base64", + "video": video, + "video_frames": None, + "video_seconds": None, + "video_bytes": len(data_str) * 3 // 4, # base64 to bytes + } + + if isinstance(video, str | Path): + path = Path(video) + video_bytes = path.read_bytes() + video_format = get_file_format(path) + elif isinstance(video, bytes): + video_bytes = video + video_format = "unknown" + else: + raise ValueError(f"Unsupported video type: {type(video)} for {video}") + + video_base64 = base64.b64encode(video).decode("utf-8") + + return { + "type": "video_base64", + "video": f"data:video/{video_format};base64,{video_base64}", + "video_frames": None, + "video_seconds": None, + "video_bytes": len(video_bytes), + } + + +def encode_audio( + audio: AudioDecoder + | bytes + | str + | Path + | np.ndarray + | torch.Tensor + | dict[str, Any], + b64encode: bool = False, + sample_rate: int | None = None, + file_name: str = "audio.wav", + encode_sample_rate: int = 16000, + max_duration: float | None = None, + mono: bool = True, + audio_format: str = "mp3", + bitrate: str = "64k", +) -> dict[ + Literal[ + "type", + "audio", + "format", + "mimetype", + "audio_samples", + "audio_seconds", + "audio_bytes", + ], + str | int | float | None, +]: + """Decode audio (if nessary) and re-encode to specified format.""" + samples = _decode_audio(audio, sample_rate=sample_rate, max_duration=max_duration) + + bitrate_val = ( + int(bitrate.rstrip("k")) * 1000 if bitrate.endswith("k") else int(bitrate) + ) + format_val = audio_format.lower() + + encoded_audio = _encode_audio( + samples=samples, + resample_rate=encode_sample_rate, + bitrate=bitrate_val, + audio_format=format_val, + mono=mono, + ) + + return { + "type": "audio_base64" if b64encode else "audio_file", + "audio": ( + base64.b64encode(encoded_audio).decode("utf-8") + if b64encode + else encoded_audio + ), + "file_name": get_file_name(audio) + if isinstance(audio, str | Path) + else file_name, + "format": audio_format, + "mimetype": f"audio/{format_val}", + "audio_samples": samples.sample_rate, + "audio_seconds": samples.duration_seconds, + "audio_bytes": len(encoded_audio), + } + + +def _decode_audio( # noqa: C901, PLR0912 + audio: AudioDecoder + | bytes + | str + | Path + | np.ndarray + | torch.Tensor + | dict[str, Any], + sample_rate: int | None = None, + max_duration: float | None = None, +) -> AudioSamples: + """Decode audio from various input types into AudioSamples.""" + # If input is a dict, unwrap it into a function call + if isinstance(audio, dict): + sample_rate = audio.get("sample_rate", audio.get("sampling_rate", sample_rate)) + if "data" not in audio and "url" not in audio: + raise ValueError( + f"Audio dict must contain either 'data' or 'url' keys, got {audio}" + ) + return _decode_audio( + audio=audio.get("data") or audio.get("url"), + sample_rate=sample_rate, + max_duration=max_duration, + ) + + # Convert numpy array to torch tensor and re-call + if isinstance(audio, np.ndarray): + return _decode_audio( + audio=torch.from_numpy(audio), + sample_rate=sample_rate, + max_duration=max_duration, + ) + + samples: AudioSamples + + # HF datasets return AudioDecoder for audio column + if isinstance(audio, AudioDecoder): + samples = audio.get_samples_played_in_range(stop_seconds=max_duration) + + elif isinstance(audio, torch.Tensor): + # If float stream assume decoded audio + if torch.is_floating_point(audio): + if sample_rate is None: + raise ValueError("Sample rate must be set for decoded audio") + + full_duration = audio.shape[1] / sample_rate + # If max_duration is set, trim the audio to that duration + if max_duration is not None: + num_samples = int(max_duration * sample_rate) + duration = min(max_duration, full_duration) + data = audio[:, :num_samples] + else: + duration = full_duration + data = audio + + samples = AudioSamples( + data=data, + pts_seconds=0.0, + duration_seconds=duration, + sample_rate=sample_rate, + ) + # If bytes tensor assume encoded audio + elif audio.dtype == torch.uint8: + decoder = AudioDecoder( + source=audio, + sample_rate=sample_rate, + ) + samples = decoder.get_samples_played_in_range(stop_seconds=max_duration) + + else: + raise ValueError(f"Unsupported audio type: {type(audio)}") + + # If bytes, assume encoded audio + elif isinstance(audio, bytes): + decoder = AudioDecoder( + source=audio, + sample_rate=sample_rate, + ) + samples = decoder.get_samples_played_in_range(stop_seconds=max_duration) + + # If str or Path, assume file path or URL to encoded audio + elif isinstance(audio, str | Path): + if isinstance(audio, str) and is_url(audio): + response = httpx.get(audio) + response.raise_for_status() + data = response.content + else: + if not Path(audio).exists(): + raise ValueError(f"Audio file does not exist: {audio}") + data = Path(audio).read_bytes() + decoder = AudioDecoder( + source=data, + ) + samples = decoder.get_samples_played_in_range(stop_seconds=max_duration) + else: + raise ValueError(f"Unsupported audio type: {type(audio)}") + + return samples + + +def _encode_audio( + samples: AudioSamples, + resample_rate: int | None = None, + bitrate: int = 64000, + audio_format: str = "mp3", + mono: bool = True, +) -> bytes: + encoder = AudioEncoder( + samples=samples.data, + sample_rate=samples.sample_rate, + ) + + audio_tensor = encoder.to_tensor( + format=audio_format, + bit_rate=bitrate if audio_format == "mp3" else None, + num_channels=1 if mono else None, + sample_rate=resample_rate, + ) + + return audio_tensor.numpy().tobytes() + + +def get_file_name(path: Path | str) -> str: + """Get file name from path.""" + return Path(path).name + + +def get_file_format(path: Path | str) -> str: + """Get file format from path extension.""" + suffix = Path(path).suffix.lower() + return suffix[1:] if suffix.startswith(".") else "unknown" From 6d036f8e6b3ba18a4d735ddc21a8357463d3425f Mon Sep 17 00:00:00 2001 From: Samuel Monson Date: Thu, 16 Oct 2025 11:27:12 -0400 Subject: [PATCH 44/57] Apply copliot fixes Signed-off-by: Samuel Monson --- src/guidellm/extras/__init__.py | 2 +- src/guidellm/extras/multimodal.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/guidellm/extras/__init__.py b/src/guidellm/extras/__init__.py index b9f178ba..80a9a3ea 100644 --- a/src/guidellm/extras/__init__.py +++ b/src/guidellm/extras/__init__.py @@ -1,4 +1,4 @@ """ Code that depends on optional dependencies. -Each submodule should be defered imported. +Each submodule should be deferred imported. """ diff --git a/src/guidellm/extras/multimodal.py b/src/guidellm/extras/multimodal.py index 80c5e716..5fe0a141 100644 --- a/src/guidellm/extras/multimodal.py +++ b/src/guidellm/extras/multimodal.py @@ -269,7 +269,7 @@ def encode_audio( ], str | int | float | None, ]: - """Decode audio (if nessary) and re-encode to specified format.""" + """Decode audio (if necessary) and re-encode to specified format.""" samples = _decode_audio(audio, sample_rate=sample_rate, max_duration=max_duration) bitrate_val = ( From cf5a2e32845c967a28a44f24d78aaec945a1bfbb Mon Sep 17 00:00:00 2001 From: Samuel Monson Date: Thu, 16 Oct 2025 11:51:59 -0400 Subject: [PATCH 45/57] Bump torchcodec verison Signed-off-by: Samuel Monson --- pylock.toml | 131 ++++++++++++++++++++++--------------------------- pyproject.toml | 5 +- 2 files changed, 63 insertions(+), 73 deletions(-) diff --git a/pylock.toml b/pylock.toml index f995c082..23b5e81d 100644 --- a/pylock.toml +++ b/pylock.toml @@ -13,36 +13,31 @@ created-by = "pdm" [[packages]] name = "torch" -version = "2.8.0+cpu" -requires-python = ">=3.9.0" +version = "2.9.0+cpu" +requires-python = ">=3.10" wheels = [ - {name = "torch-2.8.0+cpu-cp313-cp313-linux_s390x.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-linux_s390x.whl",hashes = {sha256 = "8b5882276633cf91fe3d2d7246c743b94d44a7e660b27f1308007fdb1bb89f7d"}}, - {name = "torch-2.8.0+cpu-cp313-cp313-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl",hashes = {sha256 = "a5064b5e23772c8d164068cc7c12e01a75faf7b948ecd95a0d4007d7487e5f25"}}, - {name = "torch-2.8.0+cpu-cp313-cp313-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl",hashes = {sha256 = "8f81dedb4c6076ec325acc3b47525f9c550e5284a18eae1d9061c543f7b6e7de"}}, - {name = "torch-2.8.0+cpu-cp313-cp313-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-win_amd64.whl",hashes = {sha256 = "e1ee1b2346ade3ea90306dfbec7e8ff17bc220d344109d189ae09078333b0856"}}, - {name = "torch-2.8.0+cpu-cp313-cp313-win_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-win_arm64.whl",hashes = {sha256 = "64c187345509f2b1bb334feed4666e2c781ca381874bde589182f81247e61f88"}}, - {name = "torch-2.8.0+cpu-cp313-cp313t-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl",hashes = {sha256 = "af81283ac671f434b1b25c95ba295f270e72db1fad48831eb5e4748ff9840041"}}, - {name = "torch-2.8.0+cpu-cp313-cp313t-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl",hashes = {sha256 = "a9dbb6f64f63258bc811e2c0c99640a81e5af93c531ad96e95c5ec777ea46dab"}}, - {name = "torch-2.8.0+cpu-cp313-cp313t-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-win_amd64.whl",hashes = {sha256 = "6d93a7165419bc4b2b907e859ccab0dea5deeab261448ae9a5ec5431f14c0e64"}}, - {name = "torch-2.8.0-cp313-cp313t-macosx_14_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0-cp313-cp313t-macosx_14_0_arm64.whl",hashes = {sha256 = "fbe2e149c5174ef90d29a5f84a554dfaf28e003cb4f61fa2c8c024c17ec7ca58"}}, - {name = "torch-2.8.0-cp313-none-macosx_11_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0-cp313-none-macosx_11_0_arm64.whl",hashes = {sha256 = "057efd30a6778d2ee5e2374cd63a63f63311aa6f33321e627c655df60abdd390"}}, - {name = "torch-2.8.0+cpu-cp312-cp312-linux_s390x.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-linux_s390x.whl",hashes = {sha256 = "0e34e276722ab7dd0dffa9e12fe2135a9b34a0e300c456ed7ad6430229404eb5"}}, - {name = "torch-2.8.0+cpu-cp312-cp312-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl",hashes = {sha256 = "610f600c102386e581327d5efc18c0d6edecb9820b4140d26163354a99cd800d"}}, - {name = "torch-2.8.0+cpu-cp312-cp312-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl",hashes = {sha256 = "cb9a8ba8137ab24e36bf1742cb79a1294bd374db570f09fc15a5e1318160db4e"}}, - {name = "torch-2.8.0+cpu-cp312-cp312-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-win_amd64.whl",hashes = {sha256 = "2be20b2c05a0cce10430cc25f32b689259640d273232b2de357c35729132256d"}}, - {name = "torch-2.8.0+cpu-cp312-cp312-win_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-win_arm64.whl",hashes = {sha256 = "99fc421a5d234580e45957a7b02effbf3e1c884a5dd077afc85352c77bf41434"}}, - {name = "torch-2.8.0-cp312-none-macosx_11_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0-cp312-none-macosx_11_0_arm64.whl",hashes = {sha256 = "a47b7986bee3f61ad217d8a8ce24605809ab425baf349f97de758815edd2ef54"}}, - {name = "torch-2.8.0+cpu-cp311-cp311-linux_s390x.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-linux_s390x.whl",hashes = {sha256 = "2bfc013dd6efdc8f8223a0241d3529af9f315dffefb53ffa3bf14d3f10127da6"}}, - {name = "torch-2.8.0+cpu-cp311-cp311-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-manylinux_2_28_aarch64.whl",hashes = {sha256 = "680129efdeeec3db5da3f88ee5d28c1b1e103b774aef40f9d638e2cce8f8d8d8"}}, - {name = "torch-2.8.0+cpu-cp311-cp311-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-manylinux_2_28_x86_64.whl",hashes = {sha256 = "cb06175284673a581dd91fb1965662ae4ecaba6e5c357aa0ea7bb8b84b6b7eeb"}}, - {name = "torch-2.8.0+cpu-cp311-cp311-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-win_amd64.whl",hashes = {sha256 = "7631ef49fbd38d382909525b83696dc12a55d68492ade4ace3883c62b9fc140f"}}, - {name = "torch-2.8.0+cpu-cp311-cp311-win_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-win_arm64.whl",hashes = {sha256 = "41e6fc5ec0914fcdce44ccf338b1d19a441b55cafdd741fd0bf1af3f9e4cfd14"}}, - {name = "torch-2.8.0-cp311-none-macosx_11_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0-cp311-none-macosx_11_0_arm64.whl",hashes = {sha256 = "3d05017d19bc99741288e458888283a44b0ee881d53f05f72f8b1cfea8998122"}}, - {name = "torch-2.8.0+cpu-cp310-cp310-linux_s390x.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp310-cp310-linux_s390x.whl",hashes = {sha256 = "5d255d259fbc65439b671580e40fdb8faea4644761b64fed90d6904ffe71bbc1"}}, - {name = "torch-2.8.0+cpu-cp310-cp310-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp310-cp310-manylinux_2_28_aarch64.whl",hashes = {sha256 = "b2149858b8340aeeb1f3056e0bff5b82b96e43b596fe49a9dba3184522261213"}}, - {name = "torch-2.8.0+cpu-cp310-cp310-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp310-cp310-manylinux_2_28_x86_64.whl",hashes = {sha256 = "16d75fa4e96ea28a785dfd66083ca55eb1058b6d6c5413f01656ca965ee2077e"}}, - {name = "torch-2.8.0+cpu-cp310-cp310-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp310-cp310-win_amd64.whl",hashes = {sha256 = "7cc4af6ba954f36c2163eab98cf113c137fc25aa8bbf1b06ef155968627beed2"}}, - {name = "torch-2.8.0-cp310-none-macosx_11_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.8.0-cp310-none-macosx_11_0_arm64.whl",hashes = {sha256 = "a467b49fe893a6a6cce89e3aee556edfdc64a722d7195fdfdd75cec9dea13779"}}, + {name = "torch-2.9.0+cpu-cp314-cp314-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp314-cp314-manylinux_2_28_aarch64.whl",hashes = {sha256 = "44aadb735774d4a99525d2ec29126b23016c44a07b02ce6c237dfa61a223dd52"}}, + {name = "torch-2.9.0+cpu-cp314-cp314-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp314-cp314-manylinux_2_28_x86_64.whl",hashes = {sha256 = "b355e07b7f0c369cb031adfcbff5c37a609abcea091b918a39886412afd2e07d"}}, + {name = "torch-2.9.0+cpu-cp314-cp314-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp314-cp314-win_amd64.whl",hashes = {sha256 = "c2698999361d73c2d25d7cc8a787130188d49b183abb18b554228daa102e1594"}}, + {name = "torch-2.9.0+cpu-cp314-cp314t-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp314-cp314t-manylinux_2_28_aarch64.whl",hashes = {sha256 = "fa0d1373d04b30ff8f12d542135d292f1a1ddb7c0d852a3d487a320360e5dab9"}}, + {name = "torch-2.9.0+cpu-cp314-cp314t-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp314-cp314t-manylinux_2_28_x86_64.whl",hashes = {sha256 = "2f49bb57a5fe0dc7f8e73ea9e5d36ebda2ea25b8a714a788f0fc2fc47d20a830"}}, + {name = "torch-2.9.0+cpu-cp314-cp314t-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp314-cp314t-win_amd64.whl",hashes = {sha256 = "3a60d1ecf27a9cce839b3aa665b26f0af1b1007b9c9f1e7f597f6b7bdf107617"}}, + {name = "torch-2.9.0-cp314-cp314-macosx_11_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "d8e2ab7f86010330bdcc39c8b2c795590cc75e37df4823cdaee2c98d6e3ff4a3"}}, + {name = "torch-2.9.0-cp314-cp314t-macosx_11_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0-cp314-cp314t-macosx_11_0_arm64.whl",hashes = {sha256 = "a3e859039c985d8e3ea60d7a54ca7e97ea2ae15e31beced4f3260128a161bb01"}}, + {name = "torch-2.9.0+cpu-cp313-cp313-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl",hashes = {sha256 = "be4438d8dad7f0d5a5e54f0feef8a893446894ec87f102bb1d82dcc4518542e4"}}, + {name = "torch-2.9.0+cpu-cp313-cp313-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl",hashes = {sha256 = "6c9b217584400963d5b4daddb3711ec7a3778eab211e18654fba076cce3b8682"}}, + {name = "torch-2.9.0+cpu-cp313-cp313-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp313-cp313-win_amd64.whl",hashes = {sha256 = "728372e3f58c5826445f677746e5311c1935c1a7c59599f73a49ded850e038e8"}}, + {name = "torch-2.9.0+cpu-cp313-cp313-win_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp313-cp313-win_arm64.whl",hashes = {sha256 = "95e56c26f919fbb98f16e7a0b87af494b893f9da9a65a020f17a01c13e520a81"}}, + {name = "torch-2.9.0+cpu-cp313-cp313t-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl",hashes = {sha256 = "6c777160288b08555820781ae0f3a2c67a59bd24b065e88ca1ec20e2f9dc8ac7"}}, + {name = "torch-2.9.0+cpu-cp313-cp313t-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl",hashes = {sha256 = "528fd338311f31c9fb18038cafd00e6eae0bf5ad5577521701acb62510753d18"}}, + {name = "torch-2.9.0+cpu-cp313-cp313t-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp313-cp313t-win_amd64.whl",hashes = {sha256 = "d572863990e7d2762b547735ef589f6350d9eb4e441d38753a1c33636698cf4c"}}, + {name = "torch-2.9.0-cp313-cp313t-macosx_11_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0-cp313-cp313t-macosx_11_0_arm64.whl",hashes = {sha256 = "259548471194ab63d7ea273873053a6e3cc23530c1510f01e9d7ad259187bbd0"}}, + {name = "torch-2.9.0-cp313-none-macosx_11_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0-cp313-none-macosx_11_0_arm64.whl",hashes = {sha256 = "e24836d968b54ef4dfb05594001a61958711ac9224026291e4e3f92f83a6fd7f"}}, + {name = "torch-2.9.0+cpu-cp312-cp312-manylinux_2_28_aarch64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl",hashes = {sha256 = "3a651434ae1248b0568c12b5f9e3acc8942eb28378d9d04a79302938b68c6f24"}}, + {name = "torch-2.9.0+cpu-cp312-cp312-manylinux_2_28_x86_64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl",hashes = {sha256 = "28f6eb31b08180a5c5e98d5bc14eef6909c9f5a1dbff9632c3e02a8773449349"}}, + {name = "torch-2.9.0+cpu-cp312-cp312-win_amd64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp312-cp312-win_amd64.whl",hashes = {sha256 = "e438061b87ec7dd6018fca9f975219889aa0a3f6cdc3ea10dd0ae2bc7f1c47ce"}}, + {name = "torch-2.9.0+cpu-cp312-cp312-win_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp312-cp312-win_arm64.whl",hashes = {sha256 = "eb13ff1c34e338d722e76a4fd83b8d282782505bd1b99af4b3c32da66eba6eb4"}}, + {name = "torch-2.9.0-cp312-none-macosx_11_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0-cp312-none-macosx_11_0_arm64.whl",hashes = {sha256 = "4de0ed8cbc457a506dbca40376e206a29efee10756a00f1f3404bf67ad737d04"}}, ] marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" @@ -50,30 +45,24 @@ marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in ex dependencies = [ "filelock", "typing-extensions>=4.10.0", + "setuptools; python_version >= \"3.12\"", "sympy>=1.13.3", - "networkx", + "networkx>=2.5.1", "jinja2", - "fsspec", - "setuptools; python_version >= \"3.12\"", + "fsspec>=0.8.5", ] [[packages]] name = "torchcodec" -version = "0.7.0" +version = "0.8.0" requires-python = ">=3.8" wheels = [ - {name = "torchcodec-0.7.0-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/3c/82/7c7691d538f67704b2b2444deb0e234ae564f9329bc9becf66d69998bc9b/torchcodec-0.7.0-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "32a0115035a7f0a77fa451f67c101e0273a3a37d33b69e1bcd777f00aceb7340"}}, - {name = "torchcodec-0.7.0-cp313-cp313-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/01/25/177ea01d138598ab68d5e3b000789e8617bf97874bd8f761d89093f419ba/torchcodec-0.7.0-cp313-cp313-manylinux_2_28_x86_64.whl",hashes = {sha256 = "9c70f910f9f48e6625aacaed534f766e13d447b895dc7299e96d4db9a93f1514"}}, - {name = "torchcodec-0.7.0-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/5c/a9/e2b6301fbf4590d352e183bef64927f74ef4d4f660cca3ed7a32dda60484/torchcodec-0.7.0-cp313-cp313-win_amd64.whl",hashes = {sha256 = "31b402c9ae3c6e9f33c41fddf7058f9492c443ad55d02f022395f8fa196b58f6"}}, - {name = "torchcodec-0.7.0-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/c7/b2/6d3e190fcd18c65b35f6da734d4415c72b42c8a72ffc2494d998bad8caf3/torchcodec-0.7.0-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "d9d082bbb599f4f7715bfc3b1afa5bc16d8fb9d852e68084c63f1973cc78a1cb"}}, - {name = "torchcodec-0.7.0-cp312-cp312-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/41/10/4a1a8407d0fad37cb43d1f749e7b422e5a0f6def17f3b90ab9ab9a105e32/torchcodec-0.7.0-cp312-cp312-manylinux_2_28_x86_64.whl",hashes = {sha256 = "3cd23c3296c9b071d56bb2c534a6a98275d65c1a6a7213cdb72a26ec9f9d2fd8"}}, - {name = "torchcodec-0.7.0-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/9b/e7/a2fa7ed9c81d7d683d37ca7204007b421c0537132364a9cfd8d577f19a96/torchcodec-0.7.0-cp312-cp312-win_amd64.whl",hashes = {sha256 = "ac942831bff02e6041d8718b71c6f63e4e37c05dd95e72863725c9dbef0d4a7b"}}, - {name = "torchcodec-0.7.0-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/6a/f1/bb2b5ab929ef3f092cb6508673510ffc2aafd8324493c94a2d41f1c8a683/torchcodec-0.7.0-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "967a14b31e04721901ddbf965f9e9f733f328c5e98a51e22f414e25ac32e20ba"}}, - {name = "torchcodec-0.7.0-cp311-cp311-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/06/14/8ff28247988365fc47e8471e28cdfd8d037232fcf73abb67ee815ac80f1d/torchcodec-0.7.0-cp311-cp311-manylinux_2_28_x86_64.whl",hashes = {sha256 = "afb1c48b52bd4ee8f485f5a427bb4e82380590255a26b8e9e3fe099e0779287f"}}, - {name = "torchcodec-0.7.0-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/1f/80/04f23dff2c7ac406d2d6b24a52be7654a946d2fdfe158b19341a524dae20/torchcodec-0.7.0-cp311-cp311-win_amd64.whl",hashes = {sha256 = "a68765cd29159da3cf36eb5716481c617ad9d168fe06418bcde2a9360cc7eb5e"}}, - {name = "torchcodec-0.7.0-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/d8/23/ca6bd1bc5e22786596e25d1dd62a6a4e733802940b54726a54fcf5a8795b/torchcodec-0.7.0-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "6ae0b7acbc0c1a755ae817a8843715131f24be7807ddc46092d8b49a0fc970eb"}}, - {name = "torchcodec-0.7.0-cp310-cp310-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/0d/81/cff42793544b7d3e2ff9a4912542c6d1c7a617aabe8404f8fd3d52453f20/torchcodec-0.7.0-cp310-cp310-manylinux_2_28_x86_64.whl",hashes = {sha256 = "a0071096724e8ded6a171457ce4680f646499b4a4d285cdb46e130983f965ce4"}}, - {name = "torchcodec-0.7.0-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/ad/b9/7f03bf7d42e0f7ab5598d400cb1133d3f227b52aad15d88b2ab9c97fe1ff/torchcodec-0.7.0-cp310-cp310-win_amd64.whl",hashes = {sha256 = "737da9212594bf2f205582512a7a4f56d39591b357bf5a30e72e858cfcedc2ac"}}, + {name = "torchcodec-0.8.0-cp313-cp313-macosx_12_0_arm64.whl",url = "https://files.pythonhosted.org/packages/9b/1c/40fd9358e5dd958775b8d0a01c962a022884810f441ac28229ed0e811599/torchcodec-0.8.0-cp313-cp313-macosx_12_0_arm64.whl",hashes = {sha256 = "1f3309252d035c888e6ae4518f5aca24f1c38f163124792d8a29a6872bf457f2"}}, + {name = "torchcodec-0.8.0-cp313-cp313-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/27/81/2e8f8657aed983f20f9ce842b19016d4aff05dd608ac0def94e013602814/torchcodec-0.8.0-cp313-cp313-manylinux_2_28_x86_64.whl",hashes = {sha256 = "253cc3c7a17c7be26abfcf2470e8eab3803ff3108f70be060a7efdcb49d917bc"}}, + {name = "torchcodec-0.8.0-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/09/1f/b09f028822991241eb1a31931749d034aee2c654d00f1930f4cecce595bc/torchcodec-0.8.0-cp313-cp313-win_amd64.whl",hashes = {sha256 = "c69285cb393c3b36c7bcc4e59e304076ea22b350ff6adca4a2a09b5f3f81f15c"}}, + {name = "torchcodec-0.8.0-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/17/ae/8b1d69e653894243fa66e2fec511cf203107dd146d161c9f095893c13bbc/torchcodec-0.8.0-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "af82d1fac3667335e089dc958b5e8eef5458e37d65cb3a94ebf81f45f00f7805"}}, + {name = "torchcodec-0.8.0-cp312-cp312-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/f6/fd/eec92c82545038a90ffd24e3626bb3a85f7d51577b04819c1c753d380a9b/torchcodec-0.8.0-cp312-cp312-manylinux_2_28_x86_64.whl",hashes = {sha256 = "2ec2e874dfb6fbf9bbeb792bea56317529636e78db175f56aad1e4efd6e12502"}}, + {name = "torchcodec-0.8.0-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/fe/09/ce7436151a3825f27c00263d722b0cf093609921da6cf24b0fa8133cc415/torchcodec-0.8.0-cp312-cp312-win_amd64.whl",hashes = {sha256 = "318da9af9179d156be0a84296e909d51e4cd758598eaaea08c828790c80bf977"}}, ] marker = "\"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" @@ -3348,6 +3337,19 @@ marker = "\"dev\" in extras" [packages.tool.pdm] dependencies = [] +[[packages]] +name = "networkx" +version = "3.5" +requires-python = ">=3.11" +sdist = {name = "networkx-3.5.tar.gz", url = "https://files.pythonhosted.org/packages/6c/4f/ccdb8ad3a38e583f214547fd2f7ff1fc160c43a75af88e6aec213404b96a/networkx-3.5.tar.gz", hashes = {sha256 = "d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037"}} +wheels = [ + {name = "networkx-3.5-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl",hashes = {sha256 = "0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec"}}, +] +marker = "\"default\" in dependency_groups and python_version ~= \"3.12\" or \"all\" in extras and python_version ~= \"3.12\" or \"dev\" in extras and python_version ~= \"3.12\" or \"multimodal\" in extras and python_version ~= \"3.12\"" + +[packages.tool.pdm] +dependencies = [] + [[packages]] name = "nodeenv" version = "1.9.1" @@ -3899,19 +3901,6 @@ dependencies = [ "uc-micro-py", ] -[[packages]] -name = "networkx" -version = "3.5" -requires-python = ">=3.11" -sdist = {name = "networkx-3.5.tar.gz", url = "https://files.pythonhosted.org/packages/6c/4f/ccdb8ad3a38e583f214547fd2f7ff1fc160c43a75af88e6aec213404b96a/networkx-3.5.tar.gz", hashes = {sha256 = "d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037"}} -wheels = [ - {name = "networkx-3.5-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl",hashes = {sha256 = "0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec"}}, -] -marker = "\"default\" in dependency_groups and python_version ~= \"3.12\" or \"all\" in extras and python_version ~= \"3.12\" or \"dev\" in extras and python_version ~= \"3.12\" or \"multimodal\" in extras and python_version ~= \"3.12\"" - -[packages.tool.pdm] -dependencies = [] - [[packages]] name = "pandas" version = "2.3.3" @@ -4449,33 +4438,33 @@ dependencies = [ ] [[packages]] -name = "zipp" -version = "3.23.0" -requires-python = ">=3.9" -sdist = {name = "zipp-3.23.0.tar.gz", url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hashes = {sha256 = "a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}} +name = "networkx" +version = "3.4.2" +requires-python = ">=3.10" +sdist = {name = "networkx-3.4.2.tar.gz", url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hashes = {sha256 = "307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1"}} wheels = [ - {name = "zipp-3.23.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl",hashes = {sha256 = "071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}}, + {name = "networkx-3.4.2-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl",hashes = {sha256 = "df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f"}}, ] -marker = "python_full_version >= \"3.10.0\" and python_full_version < \"3.10.2\" and \"dev\" in extras" +marker = "python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"default\" in dependency_groups or python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"all\" in extras or python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"dev\" in extras or python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"multimodal\" in extras" [packages.tool.pdm] dependencies = [] [[packages]] -name = "networkx" -version = "3.4.2" -requires-python = ">=3.10" -sdist = {name = "networkx-3.4.2.tar.gz", url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hashes = {sha256 = "307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1"}} +name = "zipp" +version = "3.23.0" +requires-python = ">=3.9" +sdist = {name = "zipp-3.23.0.tar.gz", url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hashes = {sha256 = "a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}} wheels = [ - {name = "networkx-3.4.2-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl",hashes = {sha256 = "df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f"}}, + {name = "zipp-3.23.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl",hashes = {sha256 = "071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}}, ] -marker = "python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"default\" in dependency_groups or python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"all\" in extras or python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"dev\" in extras or python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"multimodal\" in extras" +marker = "python_full_version >= \"3.10.0\" and python_full_version < \"3.10.2\" and \"dev\" in extras" [packages.tool.pdm] dependencies = [] [tool.pdm] -hashes = {sha256 = "e1e5eefffb30a72d8f9d4ec126e806fe0464e9453bfcca5adb26cf2e8b49103f"} +hashes = {sha256 = "ef37fbd12acf242b3c4a4726995f99e234a785db8f38c2a37851a2948023ea96"} strategy = ["inherit_metadata", "static_urls"] [[tool.pdm.targets]] diff --git a/pyproject.toml b/pyproject.toml index 1297881b..2effa113 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -84,8 +84,9 @@ openai = ["tiktoken>=0.11.0", "blobfile>=3.1.0"] multimodal = [ "datasets[audio,vision]>=4.1.0", "pillow", - "torch==2.8.*", - "torchcodec==0.7", + # Torchcodec needs specific torch version + "torch==2.9.*", + "torchcodec==0.8", ] # Dev Tooling dev = [ From ad192cb5f090cc6f7d3316cfaef9d02016fd0761 Mon Sep 17 00:00:00 2001 From: Samuel Monson Date: Fri, 17 Oct 2025 11:14:15 -0400 Subject: [PATCH 46/57] Add tox lockfile updater Signed-off-by: Samuel Monson --- tox.ini | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tox.ini b/tox.ini index 723d9382..7db2b34e 100644 --- a/tox.ini +++ b/tox.ini @@ -98,3 +98,13 @@ commands = rm -rf .tox rm -rf .ruff_cache rm -rf .coverage + + +[testenv:lock] +description = Update pylock +skip_install = true +setenv = + PDM_IGNORE_SAVED_PYTHON="1" +deps = pdm[all] +commands = + pdm lock --update-reuse From b65c6abac3d359a1f492ecf45cb19cb826d4d8dc Mon Sep 17 00:00:00 2001 From: Samuel Monson Date: Fri, 17 Oct 2025 11:17:57 -0400 Subject: [PATCH 47/57] Allow arguments for tox type checks Signed-off-by: Samuel Monson --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 7db2b34e..224e5ef3 100644 --- a/tox.ini +++ b/tox.ini @@ -58,7 +58,7 @@ description = Run type checks deps = .[dev] commands = - mypy --check-untyped-defs + mypy --check-untyped-defs {posargs} [testenv:links] From 5b38f40a0a8bdeda58ca1d2f6181cb2bc8bf566c Mon Sep 17 00:00:00 2001 From: Jared O'Connell Date: Tue, 14 Oct 2025 14:26:02 -0400 Subject: [PATCH 48/57] Fix mock server type errors Signed-off-by: Jared O'Connell --- .../mock_server/handlers/chat_completions.py | 4 +-- src/guidellm/mock_server/utils.py | 33 ++++++++----------- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/src/guidellm/mock_server/handlers/chat_completions.py b/src/guidellm/mock_server/handlers/chat_completions.py index de2781b0..5f198a31 100644 --- a/src/guidellm/mock_server/handlers/chat_completions.py +++ b/src/guidellm/mock_server/handlers/chat_completions.py @@ -136,7 +136,7 @@ async def _handle_non_stream(self, req: ChatCompletionsRequest) -> HTTPResponse: # Token counts prompt_text = self.tokenizer.apply_chat_template(req.messages) - prompt_tokens = len(self.tokenizer(prompt_text)) + prompt_tokens = len(self.tokenizer(prompt_text)) # type: ignore[arg-type] max_tokens = req.max_completion_tokens or req.max_tokens or math.inf completion_tokens_count = min( sample_number(self.config.output_tokens, self.config.output_tokens_std), @@ -197,7 +197,7 @@ async def generate_stream(stream_response): # Token counts prompt_text = self.tokenizer.apply_chat_template(req.messages) - prompt_tokens = len(self.tokenizer(prompt_text)) + prompt_tokens = len(self.tokenizer(prompt_text)) # type: ignore[arg-type] max_tokens = req.max_completion_tokens or req.max_tokens or math.inf completion_tokens_count = int( min( diff --git a/src/guidellm/mock_server/utils.py b/src/guidellm/mock_server/utils.py index 8348d0a6..a839f484 100644 --- a/src/guidellm/mock_server/utils.py +++ b/src/guidellm/mock_server/utils.py @@ -58,12 +58,15 @@ def __call__(self, text: str | list[str], **kwargs) -> list[int]: # noqa: ARG00 return self.convert_tokens_to_ids(tokens) elif isinstance(text, list): # Handle batch processing - return [self.__call__(t) for t in text] + result = [] + for t in text: + result.extend(self.__call__(t)) + return result else: msg = f"text input must be of type `str` or `list[str]`, got {type(text)}" raise ValueError(msg) - def tokenize(self, text: TextInput, **_kwargs) -> list[str]: + def tokenize(self, text: TextInput, **_kwargs) -> list[str]: # type: ignore[override] """ Tokenize input text into a list of token strings. @@ -76,7 +79,7 @@ def tokenize(self, text: TextInput, **_kwargs) -> list[str]: # Split text into tokens: words, spaces, and punctuation return re.findall(r"\w+|[^\w\s]|\s+", text) - def convert_tokens_to_ids(self, tokens: str | list[str]) -> int | list[int]: + def convert_tokens_to_ids(self, tokens: str | list[str]) -> list[int]: """ Convert token strings to numeric token IDs. @@ -87,12 +90,12 @@ def convert_tokens_to_ids(self, tokens: str | list[str]) -> int | list[int]: :return: Single token ID or list of token IDs """ if isinstance(tokens, str): - return hash(tokens) % self.VocabSize + return [hash(tokens) % self.VocabSize] return [hash(token) % self.VocabSize for token in tokens] - def convert_ids_to_tokens( - self, ids: int | list[int], _skip_special_tokens: bool = False - ) -> str | list[str]: + def convert_ids_to_tokens( # type: ignore[override] + self, ids: list[int], _skip_special_tokens: bool = False + ) -> list[str]: """ Convert numeric token IDs back to token strings. @@ -102,17 +105,9 @@ def convert_ids_to_tokens( :param ids: Single token ID or list of token IDs to convert :return: Single token string or list of token strings """ - if not ids and not isinstance(ids, list): - return "" - elif not ids: + if not ids: return [""] - if isinstance(ids, int): - fake = Faker() - fake.seed_instance(ids % self.VocabSize) - - return fake.word() - fake = Faker() fake.seed_instance(sum(ids) % self.VocabSize) @@ -162,7 +157,7 @@ def _add_tokens( """ return 0 - def apply_chat_template( + def apply_chat_template( # type: ignore[override] self, conversation: list, tokenize: bool = False, # Changed default to False to match transformers @@ -193,7 +188,7 @@ def apply_chat_template( return self.convert_tokens_to_ids(self.tokenize(formatted_text)) return formatted_text - def decode( + def decode( # type: ignore[override] self, token_ids: list[int], skip_special_tokens: bool = True, @@ -255,7 +250,7 @@ def create_fake_tokens_str( fake = Faker() fake.seed_instance(seed) - tokens = [] + tokens: list[str] = [] while len(tokens) < num_tokens: text = fake.text( From cb36c6a1fc7a1eef517211b66f103b168a48bddb Mon Sep 17 00:00:00 2001 From: Jared O'Connell Date: Thu, 16 Oct 2025 12:39:14 -0400 Subject: [PATCH 49/57] More type fixes Signed-off-by: Jared O'Connell --- src/guidellm/presentation/data_models.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/guidellm/presentation/data_models.py b/src/guidellm/presentation/data_models.py index 62bf97d8..e49fed55 100644 --- a/src/guidellm/presentation/data_models.py +++ b/src/guidellm/presentation/data_models.py @@ -123,19 +123,17 @@ def from_benchmarks(cls, benchmarks: list["GenerativeBenchmark"]): for i in sample_indices ] sample_outputs = [ - successful_requests[i].output.replace("\n", " ").replace('"', "'") - if successful_requests[i].output is not None - else "" + req.output.replace("\n", " ").replace('"', "'") if (req := successful_requests[i]).output else "" for i in sample_indices ] prompt_tokens = [ - float(req.prompt_tokens) + float(req.prompt_tokens) if req.prompt_tokens is not None else -1 for bm in benchmarks for req in bm.requests.successful ] output_tokens = [ - float(req.output_tokens) + float(req.output_tokens) if req.output_tokens is not None else -1 for bm in benchmarks for req in bm.requests.successful ] From 1ffe53a6d77b3ac1116d245dee98814b91754a48 Mon Sep 17 00:00:00 2001 From: Jared O'Connell Date: Fri, 17 Oct 2025 14:10:59 -0400 Subject: [PATCH 50/57] Address utility and presentation type errors Signed-off-by: Jared O'Connell --- src/guidellm/presentation/data_models.py | 8 +-- src/guidellm/utils/encoding.py | 2 +- src/guidellm/utils/imports.py | 2 +- src/guidellm/utils/registry.py | 2 +- src/guidellm/utils/statistics.py | 83 +++++++++++++++--------- 5 files changed, 58 insertions(+), 39 deletions(-) diff --git a/src/guidellm/presentation/data_models.py b/src/guidellm/presentation/data_models.py index e49fed55..f98dd5a2 100644 --- a/src/guidellm/presentation/data_models.py +++ b/src/guidellm/presentation/data_models.py @@ -117,13 +117,13 @@ def from_benchmarks(cls, benchmarks: list["GenerativeBenchmark"]): range(len(successful_requests)), min(5, len(successful_requests)) ) sample_prompts = [ - successful_requests[i].request_args.replace("\n", " ").replace('"', "'") - if successful_requests[i].request_args is not None - else "" + req.request_args.replace("\n", " ").replace('"', "'") + if (req := successful_requests[i]).request_args else "" for i in sample_indices ] sample_outputs = [ - req.output.replace("\n", " ").replace('"', "'") if (req := successful_requests[i]).output else "" + req.output.replace("\n", " ").replace('"', "'") + if (req := successful_requests[i]).output else "" for i in sample_indices ] diff --git a/src/guidellm/utils/encoding.py b/src/guidellm/utils/encoding.py index 7ececef5..50e7dce0 100644 --- a/src/guidellm/utils/encoding.py +++ b/src/guidellm/utils/encoding.py @@ -32,7 +32,7 @@ HAS_MSGSPEC = True except ImportError: - MsgspecDecoder = MsgspecEncoder = None + MsgspecDecoder = MsgspecEncoder = None # type: ignore[misc, assignment] # HAS_MSGSPEC will be checked at runtime HAS_MSGSPEC = False diff --git a/src/guidellm/utils/imports.py b/src/guidellm/utils/imports.py index 9a6b82d1..8b7ad5f6 100644 --- a/src/guidellm/utils/imports.py +++ b/src/guidellm/utils/imports.py @@ -3,7 +3,7 @@ try: import orjson as json except ImportError: - import json + import json # type: ignore[no-redef] # Done only after a failure. __all__ = ["json"] diff --git a/src/guidellm/utils/registry.py b/src/guidellm/utils/registry.py index 1a1a213f..cf450bd1 100644 --- a/src/guidellm/utils/registry.py +++ b/src/guidellm/utils/registry.py @@ -65,7 +65,7 @@ class TokenProposal(RegistryMixin): :cvar registry_populated: Track whether auto-discovery has completed """ - registry: ClassVar[dict[str, RegistryObjT] | None] = None + registry: ClassVar[dict[str, RegistryObjT] | None] = None # type: ignore[misc] registry_auto_discovery: ClassVar[bool] = False registry_populated: ClassVar[bool] = False diff --git a/src/guidellm/utils/statistics.py b/src/guidellm/utils/statistics.py index 0529cb0c..a8403c72 100644 --- a/src/guidellm/utils/statistics.py +++ b/src/guidellm/utils/statistics.py @@ -283,40 +283,12 @@ def from_request_times( ) # First convert to timing events based on type - events: list[tuple[float, float]] = [] - - if distribution_type == "concurrency": - # For concurrency, each request adds to concurrency at start - # and subtracts at end - for (start, end), weight in zip(requests, weights, strict=False): - events.append((start, weight)) - events.append((end, -1 * weight)) - elif distribution_type == "rate": - # For rate, each request is added at the end time only - global_start = min(start for start, _ in requests) if requests else 0.0 - events.append((global_start, 0.0)) - for (_, end), weight in zip(requests, weights, strict=False): - events.append((end, weight)) - else: - raise ValueError( - f"Invalid distribution_type '{distribution_type}'. " - "Must be 'concurrency' or 'rate'." - ) - - # Combine any events within epsilon of each other for stability - sorted_events = sorted(events, key=lambda event: event[0]) - flattened_events: list[tuple[float, float]] = ( - [sorted_events.pop(0)] if sorted_events else [] + events = DistributionSummary._convert_to_timing_events( + requests, distribution_type, weights ) - last_time = flattened_events[0][0] if flattened_events else 0.0 - for time, val in sorted_events: - if abs(time - last_time) <= epsilon: - last_val = flattened_events[-1][1] - flattened_events[-1] = (last_time, last_val + val) - else: - last_time = time - flattened_events.append((time, val)) + # Combine any events within epsilon of each other for stability + flattened_events = DistributionSummary._combine_events(events, epsilon) # Convert events to value distribution function distribution: dict[float, float] = defaultdict(float) @@ -357,6 +329,53 @@ def from_request_times( include_cdf=include_cdf, ) + @staticmethod + def _convert_to_timing_events( + requests: list[tuple[float, float]], + distribution_type: Literal["concurrency", "rate"], + weights: list[float], + ) -> list[tuple[float, float]]: + events: list[tuple[float, float]] = [] + + if distribution_type == "concurrency": + # For concurrency, each request adds to concurrency at start + # and subtracts at end + for (start, end), weight in zip(requests, weights, strict=False): + events.append((start, weight)) + events.append((end, -1 * weight)) + elif distribution_type == "rate": + # For rate, each request is added at the end time only + global_start = min(start for start, _ in requests) if requests else 0.0 + events.append((global_start, 0.0)) + for (_, end), weight in zip(requests, weights, strict=False): + events.append((end, weight)) + else: + raise ValueError( + f"Invalid distribution_type '{distribution_type}'. " + "Must be 'concurrency' or 'rate'." + ) + return events + + @staticmethod + def _combine_events( + events: list[tuple[float, float]], + epsilon: float, + ) -> list[tuple[float, float]]: + sorted_events = sorted(events, key=lambda event: event[0]) + flattened_events: list[tuple[float, float]] = ( + [sorted_events.pop(0)] if sorted_events else [] + ) + last_time = flattened_events[0][0] if flattened_events else 0.0 + + for time, val in sorted_events: + if abs(time - last_time) <= epsilon: + last_val = flattened_events[-1][1] + flattened_events[-1] = (last_time, last_val + val) + else: + last_time = time + flattened_events.append((time, val)) + return flattened_events + @staticmethod def from_iterable_request_times( requests: list[tuple[float, float]], From 48769c200d4aaadd4b08c67321ef5098d092a0f4 Mon Sep 17 00:00:00 2001 From: Jared O'Connell Date: Fri, 17 Oct 2025 15:12:28 -0400 Subject: [PATCH 51/57] Fix type errors in extras Signed-off-by: Jared O'Connell --- pyproject.toml | 1 + src/guidellm/extras/multimodal.py | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2effa113..f51560dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -174,6 +174,7 @@ module = [ "transformers.*", "setuptools.*", "setuptools_git_versioning.*", + "torchcodec.*" ] ignore_missing_imports = true diff --git a/src/guidellm/extras/multimodal.py b/src/guidellm/extras/multimodal.py index 5fe0a141..37d64f96 100644 --- a/src/guidellm/extras/multimodal.py +++ b/src/guidellm/extras/multimodal.py @@ -230,7 +230,7 @@ def encode_video( else: raise ValueError(f"Unsupported video type: {type(video)} for {video}") - video_base64 = base64.b64encode(video).decode("utf-8") + video_base64 = base64.b64encode(video_bytes).decode("utf-8") return { "type": "video_base64", @@ -266,8 +266,9 @@ def encode_audio( "audio_samples", "audio_seconds", "audio_bytes", + "file_name", ], - str | int | float | None, + str | int | float | bytes | None, ]: """Decode audio (if necessary) and re-encode to specified format.""" samples = _decode_audio(audio, sample_rate=sample_rate, max_duration=max_duration) @@ -338,10 +339,10 @@ def _decode_audio( # noqa: C901, PLR0912 samples: AudioSamples + data: torch.Tensor | bytes # HF datasets return AudioDecoder for audio column if isinstance(audio, AudioDecoder): samples = audio.get_samples_played_in_range(stop_seconds=max_duration) - elif isinstance(audio, torch.Tensor): # If float stream assume decoded audio if torch.is_floating_point(audio): From af6b6b894512fb0c9b7ab645ef6aac480b0087bc Mon Sep 17 00:00:00 2001 From: Samuel Monson Date: Fri, 17 Oct 2025 17:34:41 -0400 Subject: [PATCH 52/57] Split multimodal group into vision and audio Signed-off-by: Samuel Monson --- pylock.toml | 418 +++++++++--------- pyproject.toml | 12 +- src/guidellm/data/preprocessors/formatters.py | 6 +- src/guidellm/extras/audio.py | 215 +++++++++ .../extras/{multimodal.py => vision.py} | 197 +-------- 5 files changed, 436 insertions(+), 412 deletions(-) create mode 100644 src/guidellm/extras/audio.py rename src/guidellm/extras/{multimodal.py => vision.py} (55%) diff --git a/pylock.toml b/pylock.toml index 23b5e81d..c4a1545d 100644 --- a/pylock.toml +++ b/pylock.toml @@ -6,7 +6,7 @@ environments = [ "python_version ~= \"3.12\"", "python_full_version >= \"3.10.0\" and python_version < \"3.12\"", ] -extras = ["all", "dev", "multimodal", "openai", "perf", "recommended"] +extras = ["all", "audio", "dev", "openai", "perf", "recommended", "vision"] dependency-groups = ["default"] default-groups = ["default"] created-by = "pdm" @@ -39,7 +39,7 @@ wheels = [ {name = "torch-2.9.0+cpu-cp312-cp312-win_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp312-cp312-win_arm64.whl",hashes = {sha256 = "eb13ff1c34e338d722e76a4fd83b8d282782505bd1b99af4b3c32da66eba6eb4"}}, {name = "torch-2.9.0-cp312-none-macosx_11_0_arm64.whl",url = "https://download.pytorch.org/whl/cpu/torch-2.9.0-cp312-none-macosx_11_0_arm64.whl",hashes = {sha256 = "4de0ed8cbc457a506dbca40376e206a29efee10756a00f1f3404bf67ad737d04"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras" [packages.tool.pdm] dependencies = [ @@ -64,7 +64,7 @@ wheels = [ {name = "torchcodec-0.8.0-cp312-cp312-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/f6/fd/eec92c82545038a90ffd24e3626bb3a85f7d51577b04819c1c753d380a9b/torchcodec-0.8.0-cp312-cp312-manylinux_2_28_x86_64.whl",hashes = {sha256 = "2ec2e874dfb6fbf9bbeb792bea56317529636e78db175f56aad1e4efd6e12502"}}, {name = "torchcodec-0.8.0-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/fe/09/ce7436151a3825f27c00263d722b0cf093609921da6cf24b0fa8133cc415/torchcodec-0.8.0-cp312-cp312-win_amd64.whl",hashes = {sha256 = "318da9af9179d156be0a84296e909d51e4cd758598eaaea08c828790c80bf977"}}, ] -marker = "\"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" +marker = "\"all\" in extras or \"audio\" in extras or \"dev\" in extras" [packages.tool.pdm] dependencies = [] @@ -167,6 +167,57 @@ dependencies = [ "importlib-metadata; python_version < \"3.8\"", ] +[[packages]] +name = "setuptools-git-versioning" +version = "2.1.0" +requires-python = ">=3.7" +sdist = {name = "setuptools_git_versioning-2.1.0.tar.gz", url = "https://files.pythonhosted.org/packages/f0/72/507b0b459b1fdbf5705aecbc5330c32d62dd41560718d2720bb6d94607f5/setuptools_git_versioning-2.1.0.tar.gz", hashes = {sha256 = "6aef5b8bb1cfb953b6b343d27cbfc561d96cf2a2ee23c2e0dd3591042a059921"}} +wheels = [ + {name = "setuptools_git_versioning-2.1.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/c0/ba/daf16c2d1965bf6237fb696639e3e93645ac6801f7dcaf9ec694a74e9326/setuptools_git_versioning-2.1.0-py3-none-any.whl",hashes = {sha256 = "09a15cbb9a00884e91a3591a4c9ec1ff93c24b1b4a40de39a44815196beb7ebf"}}, +] +marker = "\"dev\" in extras" + +[packages.tool.pdm] +dependencies = [ + "packaging", + "setuptools", + "tomli>=2.0.1; python_version < \"3.11\"", +] + +[[packages]] +name = "build" +version = "1.3.0" +requires-python = ">=3.9" +sdist = {name = "build-1.3.0.tar.gz", url = "https://files.pythonhosted.org/packages/25/1c/23e33405a7c9eac261dff640926b8b5adaed6a6eb3e1767d441ed611d0c0/build-1.3.0.tar.gz", hashes = {sha256 = "698edd0ea270bde950f53aed21f3a0135672206f3911e0176261a31e0e07b397"}} +wheels = [ + {name = "build-1.3.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/cb/8c/2b30c12155ad8de0cf641d76a8b396a16d2c36bc6d50b621a62b7c4567c1/build-1.3.0-py3-none-any.whl",hashes = {sha256 = "7145f0b5061ba90a1500d60bd1b13ca0a8a4cebdd0cc16ed8adf1c0e739f43b4"}}, +] +marker = "\"dev\" in extras" + +[packages.tool.pdm] +dependencies = [ + "packaging>=19.1", + "pyproject-hooks", + "colorama; os_name == \"nt\"", + "importlib-metadata>=4.6; python_full_version < \"3.10.2\"", + "tomli>=1.1.0; python_version < \"3.11\"", +] + +[[packages]] +name = "culsans" +version = "0.9.0" +requires-python = ">=3.8" +sdist = {name = "culsans-0.9.0.tar.gz", url = "https://files.pythonhosted.org/packages/90/5d/12e7e16b0caafaa8cca0728dd817204afd1274ddb35531b029b1c5cf7b2a/culsans-0.9.0.tar.gz", hashes = {sha256 = "942dd3c3c77f20e9ac3383d9a5ef8b7b24c0dac1a593bdb20d46c8a38720a5f3"}} +wheels = [ + {name = "culsans-0.9.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/6f/b4/1e3cccb48f09e89e0cfc06925182cbcd36abf80b8eda2489430b41c7eaff/culsans-0.9.0-py3-none-any.whl",hashes = {sha256 = "d3537b65bbb341c2ac72e7d152deb8ab893b2a00452d2a68702a1a1a41619d6f"}}, +] +marker = "\"default\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [ + "aiologic>=0.13.0", +] + [[packages]] name = "datasets" version = "4.2.0" @@ -175,7 +226,7 @@ sdist = {name = "datasets-4.2.0.tar.gz", url = "https://files.pythonhosted.org/p wheels = [ {name = "datasets-4.2.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/91/9e/0bbbd09b116fd8ee2d3617e28e6598551d2f0f24d3a2ce99cc87ec85aeb0/datasets-4.2.0-py3-none-any.whl",hashes = {sha256 = "fdc43aaf4a73b31f64f80f72f195ab413a1141ed15555d675b2fd17926f8b026"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [ @@ -257,7 +308,7 @@ wheels = [ {name = "numpy-2.3.3-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/32/ee/de999f2625b80d043d6d2d628c07d0d5555a677a3cf78fdf868d409b8766/numpy-2.3.3-cp312-cp312-win_amd64.whl",hashes = {sha256 = "497d7cad08e7092dba36e3d296fe4c97708c93daf26643a1ae4b03f6294d30eb"}}, {name = "numpy-2.3.3-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/49/6e/b479032f8a43559c383acb20816644f5f91c88f633d9271ee84f3b3a996c/numpy-2.3.3-cp312-cp312-win_arm64.whl",hashes = {sha256 = "ca0309a18d4dfea6fc6262a66d06c26cfe4640c3926ceec90e57791a82b6eee5"}}, ] -marker = "\"default\" in dependency_groups and python_version ~= \"3.12\" or \"all\" in extras and python_version ~= \"3.12\" or \"dev\" in extras and python_version ~= \"3.12\" or \"multimodal\" in extras and python_version ~= \"3.12\"" +marker = "\"default\" in dependency_groups and python_version ~= \"3.12\" or \"all\" in extras and python_version ~= \"3.12\" or \"audio\" in extras and python_version ~= \"3.12\" or \"dev\" in extras and python_version ~= \"3.12\" or \"vision\" in extras and python_version ~= \"3.12\"" [packages.tool.pdm] dependencies = [] @@ -325,62 +376,11 @@ wheels = [ {name = "pyyaml-6.0.3-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl",hashes = {sha256 = "28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"}}, {name = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl",hashes = {sha256 = "bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [] -[[packages]] -name = "setuptools-git-versioning" -version = "2.1.0" -requires-python = ">=3.7" -sdist = {name = "setuptools_git_versioning-2.1.0.tar.gz", url = "https://files.pythonhosted.org/packages/f0/72/507b0b459b1fdbf5705aecbc5330c32d62dd41560718d2720bb6d94607f5/setuptools_git_versioning-2.1.0.tar.gz", hashes = {sha256 = "6aef5b8bb1cfb953b6b343d27cbfc561d96cf2a2ee23c2e0dd3591042a059921"}} -wheels = [ - {name = "setuptools_git_versioning-2.1.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/c0/ba/daf16c2d1965bf6237fb696639e3e93645ac6801f7dcaf9ec694a74e9326/setuptools_git_versioning-2.1.0-py3-none-any.whl",hashes = {sha256 = "09a15cbb9a00884e91a3591a4c9ec1ff93c24b1b4a40de39a44815196beb7ebf"}}, -] -marker = "\"dev\" in extras" - -[packages.tool.pdm] -dependencies = [ - "packaging", - "setuptools", - "tomli>=2.0.1; python_version < \"3.11\"", -] - -[[packages]] -name = "build" -version = "1.3.0" -requires-python = ">=3.9" -sdist = {name = "build-1.3.0.tar.gz", url = "https://files.pythonhosted.org/packages/25/1c/23e33405a7c9eac261dff640926b8b5adaed6a6eb3e1767d441ed611d0c0/build-1.3.0.tar.gz", hashes = {sha256 = "698edd0ea270bde950f53aed21f3a0135672206f3911e0176261a31e0e07b397"}} -wheels = [ - {name = "build-1.3.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/cb/8c/2b30c12155ad8de0cf641d76a8b396a16d2c36bc6d50b621a62b7c4567c1/build-1.3.0-py3-none-any.whl",hashes = {sha256 = "7145f0b5061ba90a1500d60bd1b13ca0a8a4cebdd0cc16ed8adf1c0e739f43b4"}}, -] -marker = "\"dev\" in extras" - -[packages.tool.pdm] -dependencies = [ - "packaging>=19.1", - "pyproject-hooks", - "colorama; os_name == \"nt\"", - "importlib-metadata>=4.6; python_full_version < \"3.10.2\"", - "tomli>=1.1.0; python_version < \"3.11\"", -] - -[[packages]] -name = "culsans" -version = "0.9.0" -requires-python = ">=3.8" -sdist = {name = "culsans-0.9.0.tar.gz", url = "https://files.pythonhosted.org/packages/90/5d/12e7e16b0caafaa8cca0728dd817204afd1274ddb35531b029b1c5cf7b2a/culsans-0.9.0.tar.gz", hashes = {sha256 = "942dd3c3c77f20e9ac3383d9a5ef8b7b24c0dac1a593bdb20d46c8a38720a5f3"}} -wheels = [ - {name = "culsans-0.9.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/6f/b4/1e3cccb48f09e89e0cfc06925182cbcd36abf80b8eda2489430b41c7eaff/culsans-0.9.0-py3-none-any.whl",hashes = {sha256 = "d3537b65bbb341c2ac72e7d152deb8ab893b2a00452d2a68702a1a1a41619d6f"}}, -] -marker = "\"default\" in dependency_groups" - -[packages.tool.pdm] -dependencies = [ - "aiologic>=0.13.0", -] - [[packages]] name = "ftfy" version = "6.3.1" @@ -532,112 +532,6 @@ dependencies = [ "tomli>=1.1.0; python_version < \"3.11\"", ] -[[packages]] -name = "pillow" -version = "11.3.0" -requires-python = ">=3.9" -sdist = {name = "pillow-11.3.0.tar.gz", url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hashes = {sha256 = "3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523"}} -wheels = [ - {name = "pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl",hashes = {sha256 = "d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12"}}, - {name = "pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a"}}, - {name = "pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632"}}, - {name = "pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673"}}, - {name = "pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027"}}, - {name = "pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77"}}, - {name = "pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl",hashes = {sha256 = "7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874"}}, - {name = "pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl",hashes = {sha256 = "98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a"}}, - {name = "pillow-11.3.0-cp314-cp314-win32.whl",url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl",hashes = {sha256 = "02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214"}}, - {name = "pillow-11.3.0-cp314-cp314-win_amd64.whl",url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl",hashes = {sha256 = "a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635"}}, - {name = "pillow-11.3.0-cp314-cp314-win_arm64.whl",url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl",hashes = {sha256 = "155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6"}}, - {name = "pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl",hashes = {sha256 = "59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae"}}, - {name = "pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl",hashes = {sha256 = "f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653"}}, - {name = "pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6"}}, - {name = "pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36"}}, - {name = "pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b"}}, - {name = "pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477"}}, - {name = "pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50"}}, - {name = "pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b"}}, - {name = "pillow-11.3.0-cp314-cp314t-win32.whl",url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl",hashes = {sha256 = "118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12"}}, - {name = "pillow-11.3.0-cp314-cp314t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl",hashes = {sha256 = "8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db"}}, - {name = "pillow-11.3.0-cp314-cp314t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl",hashes = {sha256 = "79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa"}}, - {name = "pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl",url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl",hashes = {sha256 = "1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd"}}, - {name = "pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl",url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl",hashes = {sha256 = "30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8"}}, - {name = "pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl",url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl",hashes = {sha256 = "7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f"}}, - {name = "pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl",hashes = {sha256 = "ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c"}}, - {name = "pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd"}}, - {name = "pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e"}}, - {name = "pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1"}}, - {name = "pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805"}}, - {name = "pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8"}}, - {name = "pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2"}}, - {name = "pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b"}}, - {name = "pillow-11.3.0-cp313-cp313-win32.whl",url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl",hashes = {sha256 = "a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3"}}, - {name = "pillow-11.3.0-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl",hashes = {sha256 = "0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51"}}, - {name = "pillow-11.3.0-cp313-cp313-win_arm64.whl",url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl",hashes = {sha256 = "1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580"}}, - {name = "pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl",hashes = {sha256 = "4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e"}}, - {name = "pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl",hashes = {sha256 = "5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d"}}, - {name = "pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced"}}, - {name = "pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c"}}, - {name = "pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8"}}, - {name = "pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59"}}, - {name = "pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe"}}, - {name = "pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c"}}, - {name = "pillow-11.3.0-cp313-cp313t-win32.whl",url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl",hashes = {sha256 = "2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788"}}, - {name = "pillow-11.3.0-cp313-cp313t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl",hashes = {sha256 = "857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31"}}, - {name = "pillow-11.3.0-cp313-cp313t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl",hashes = {sha256 = "8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e"}}, - {name = "pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl",hashes = {sha256 = "fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4"}}, - {name = "pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69"}}, - {name = "pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d"}}, - {name = "pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6"}}, - {name = "pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7"}}, - {name = "pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024"}}, - {name = "pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809"}}, - {name = "pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d"}}, - {name = "pillow-11.3.0-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl",hashes = {sha256 = "7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149"}}, - {name = "pillow-11.3.0-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl",hashes = {sha256 = "a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d"}}, - {name = "pillow-11.3.0-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl",hashes = {sha256 = "2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542"}}, - {name = "pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl",url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl",hashes = {sha256 = "1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722"}}, - {name = "pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288"}}, - {name = "pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d"}}, - {name = "pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494"}}, - {name = "pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58"}}, - {name = "pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f"}}, - {name = "pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e"}}, - {name = "pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94"}}, - {name = "pillow-11.3.0-cp311-cp311-win32.whl",url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl",hashes = {sha256 = "b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0"}}, - {name = "pillow-11.3.0-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl",hashes = {sha256 = "1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac"}}, - {name = "pillow-11.3.0-cp311-cp311-win_arm64.whl",url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl",hashes = {sha256 = "30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd"}}, - {name = "pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl",url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl",hashes = {sha256 = "7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6"}}, - {name = "pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl",hashes = {sha256 = "9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438"}}, - {name = "pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3"}}, - {name = "pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c"}}, - {name = "pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361"}}, - {name = "pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7"}}, - {name = "pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl",url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl",hashes = {sha256 = "c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8"}}, - {name = "pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl",url = "https://files.pythonhosted.org/packages/4c/5d/45a3553a253ac8763f3561371432a90bdbe6000fbdcf1397ffe502aa206c/pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl",hashes = {sha256 = "1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860"}}, - {name = "pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/7c/c8/67c12ab069ef586a25a4a79ced553586748fad100c77c0ce59bb4983ac98/pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad"}}, - {name = "pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/2f/bd/6741ebd56263390b382ae4c5de02979af7f8bd9807346d068700dd6d5cf9/pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0"}}, - {name = "pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/ca/0b/c412a9e27e1e6a829e6ab6c2dca52dd563efbedf4c9c6aa453d9a9b77359/pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b"}}, - {name = "pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/59/9d/9b7076aaf30f5dd17e5e5589b2d2f5a5d7e30ff67a171eb686e4eecc2adf/pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50"}}, - {name = "pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/f0/16/1a6bf01fb622fb9cf5c91683823f073f053005c849b1f52ed613afcf8dae/pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae"}}, - {name = "pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/7b/e6/6ff7077077eb47fde78739e7d570bdcd7c10495666b6afcd23ab56b19a43/pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9"}}, - {name = "pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/c3/3a/b13f36832ea6d279a697231658199e0a03cd87ef12048016bdcc84131601/pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e"}}, - {name = "pillow-11.3.0-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/6c/e4/61b2e1a7528740efbc70b3d581f33937e38e98ef3d50b05007267a55bcb2/pillow-11.3.0-cp310-cp310-win32.whl",hashes = {sha256 = "89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6"}}, - {name = "pillow-11.3.0-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/a9/d3/60c781c83a785d6afbd6a326ed4d759d141de43aa7365725cbcd65ce5e54/pillow-11.3.0-cp310-cp310-win_amd64.whl",hashes = {sha256 = "19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f"}}, - {name = "pillow-11.3.0-cp310-cp310-win_arm64.whl",url = "https://files.pythonhosted.org/packages/9f/28/4f4a0203165eefb3763939c6789ba31013a2e90adffb456610f30f613850/pillow-11.3.0-cp310-cp310-win_arm64.whl",hashes = {sha256 = "819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f"}}, - {name = "pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl",url = "https://files.pythonhosted.org/packages/6f/8b/209bd6b62ce8367f47e68a218bffac88888fdf2c9fcf1ecadc6c3ec1ebc7/pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl",hashes = {sha256 = "3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967"}}, - {name = "pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/2e/e6/231a0b76070c2cfd9e260a7a5b504fb72da0a95279410fa7afd99d9751d6/pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl",hashes = {sha256 = "b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe"}}, - {name = "pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/13/f4/10cf94fda33cb12765f2397fc285fa6d8eb9c29de7f3185165b702fc7386/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c"}}, - {name = "pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/72/c9/583821097dc691880c92892e8e2d41fe0a5a3d6021f4963371d2f6d57250/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25"}}, - {name = "pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/3b/8e/5c9d410f9217b12320efc7c413e72693f48468979a013ad17fd690397b9a/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27"}}, - {name = "pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/62/bb/78347dbe13219991877ffb3a91bf09da8317fbfcd4b5f9140aeae020ad71/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a"}}, - {name = "pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl",url = "https://files.pythonhosted.org/packages/d9/28/1000353d5e61498aaeaaf7f1e4b49ddb05f2c6575f9d4f9f914a3538b6e1/pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl",hashes = {sha256 = "8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f"}}, -] -marker = "\"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" - -[packages.tool.pdm] -dependencies = [] - [[packages]] name = "pre-commit" version = "3.5.0" @@ -892,7 +786,7 @@ sdist = {name = "setuptools-80.9.0.tar.gz", url = "https://files.pythonhosted.or wheels = [ {name = "setuptools-80.9.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl",hashes = {sha256 = "062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras" [packages.tool.pdm] dependencies = [] @@ -1016,6 +910,112 @@ marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in ex [packages.tool.pdm] dependencies = [] +[[packages]] +name = "pillow" +version = "11.3.0" +requires-python = ">=3.9" +sdist = {name = "pillow-11.3.0.tar.gz", url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hashes = {sha256 = "3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523"}} +wheels = [ + {name = "pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl",hashes = {sha256 = "d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12"}}, + {name = "pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a"}}, + {name = "pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632"}}, + {name = "pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673"}}, + {name = "pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027"}}, + {name = "pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77"}}, + {name = "pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl",hashes = {sha256 = "7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874"}}, + {name = "pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl",hashes = {sha256 = "98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a"}}, + {name = "pillow-11.3.0-cp314-cp314-win32.whl",url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl",hashes = {sha256 = "02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214"}}, + {name = "pillow-11.3.0-cp314-cp314-win_amd64.whl",url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl",hashes = {sha256 = "a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635"}}, + {name = "pillow-11.3.0-cp314-cp314-win_arm64.whl",url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl",hashes = {sha256 = "155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6"}}, + {name = "pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl",hashes = {sha256 = "59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae"}}, + {name = "pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl",hashes = {sha256 = "f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653"}}, + {name = "pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6"}}, + {name = "pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36"}}, + {name = "pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b"}}, + {name = "pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477"}}, + {name = "pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50"}}, + {name = "pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b"}}, + {name = "pillow-11.3.0-cp314-cp314t-win32.whl",url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl",hashes = {sha256 = "118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12"}}, + {name = "pillow-11.3.0-cp314-cp314t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl",hashes = {sha256 = "8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db"}}, + {name = "pillow-11.3.0-cp314-cp314t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl",hashes = {sha256 = "79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa"}}, + {name = "pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl",url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl",hashes = {sha256 = "1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd"}}, + {name = "pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl",url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl",hashes = {sha256 = "30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8"}}, + {name = "pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl",url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl",hashes = {sha256 = "7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f"}}, + {name = "pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl",hashes = {sha256 = "ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c"}}, + {name = "pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd"}}, + {name = "pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e"}}, + {name = "pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1"}}, + {name = "pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805"}}, + {name = "pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8"}}, + {name = "pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2"}}, + {name = "pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b"}}, + {name = "pillow-11.3.0-cp313-cp313-win32.whl",url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl",hashes = {sha256 = "a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3"}}, + {name = "pillow-11.3.0-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl",hashes = {sha256 = "0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51"}}, + {name = "pillow-11.3.0-cp313-cp313-win_arm64.whl",url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl",hashes = {sha256 = "1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580"}}, + {name = "pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl",hashes = {sha256 = "4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e"}}, + {name = "pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl",hashes = {sha256 = "5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d"}}, + {name = "pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced"}}, + {name = "pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c"}}, + {name = "pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8"}}, + {name = "pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59"}}, + {name = "pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe"}}, + {name = "pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c"}}, + {name = "pillow-11.3.0-cp313-cp313t-win32.whl",url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl",hashes = {sha256 = "2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788"}}, + {name = "pillow-11.3.0-cp313-cp313t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl",hashes = {sha256 = "857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31"}}, + {name = "pillow-11.3.0-cp313-cp313t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl",hashes = {sha256 = "8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e"}}, + {name = "pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl",hashes = {sha256 = "fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4"}}, + {name = "pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69"}}, + {name = "pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d"}}, + {name = "pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6"}}, + {name = "pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7"}}, + {name = "pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024"}}, + {name = "pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809"}}, + {name = "pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d"}}, + {name = "pillow-11.3.0-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl",hashes = {sha256 = "7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149"}}, + {name = "pillow-11.3.0-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl",hashes = {sha256 = "a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d"}}, + {name = "pillow-11.3.0-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl",hashes = {sha256 = "2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542"}}, + {name = "pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl",url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl",hashes = {sha256 = "1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722"}}, + {name = "pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288"}}, + {name = "pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d"}}, + {name = "pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494"}}, + {name = "pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58"}}, + {name = "pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f"}}, + {name = "pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e"}}, + {name = "pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94"}}, + {name = "pillow-11.3.0-cp311-cp311-win32.whl",url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl",hashes = {sha256 = "b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0"}}, + {name = "pillow-11.3.0-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl",hashes = {sha256 = "1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac"}}, + {name = "pillow-11.3.0-cp311-cp311-win_arm64.whl",url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl",hashes = {sha256 = "30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd"}}, + {name = "pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl",url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl",hashes = {sha256 = "7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6"}}, + {name = "pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl",hashes = {sha256 = "9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438"}}, + {name = "pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3"}}, + {name = "pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c"}}, + {name = "pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361"}}, + {name = "pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7"}}, + {name = "pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl",url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl",hashes = {sha256 = "c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8"}}, + {name = "pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl",url = "https://files.pythonhosted.org/packages/4c/5d/45a3553a253ac8763f3561371432a90bdbe6000fbdcf1397ffe502aa206c/pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl",hashes = {sha256 = "1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860"}}, + {name = "pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/7c/c8/67c12ab069ef586a25a4a79ced553586748fad100c77c0ce59bb4983ac98/pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad"}}, + {name = "pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/2f/bd/6741ebd56263390b382ae4c5de02979af7f8bd9807346d068700dd6d5cf9/pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0"}}, + {name = "pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/ca/0b/c412a9e27e1e6a829e6ab6c2dca52dd563efbedf4c9c6aa453d9a9b77359/pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b"}}, + {name = "pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/59/9d/9b7076aaf30f5dd17e5e5589b2d2f5a5d7e30ff67a171eb686e4eecc2adf/pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50"}}, + {name = "pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/f0/16/1a6bf01fb622fb9cf5c91683823f073f053005c849b1f52ed613afcf8dae/pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae"}}, + {name = "pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/7b/e6/6ff7077077eb47fde78739e7d570bdcd7c10495666b6afcd23ab56b19a43/pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9"}}, + {name = "pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/c3/3a/b13f36832ea6d279a697231658199e0a03cd87ef12048016bdcc84131601/pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e"}}, + {name = "pillow-11.3.0-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/6c/e4/61b2e1a7528740efbc70b3d581f33937e38e98ef3d50b05007267a55bcb2/pillow-11.3.0-cp310-cp310-win32.whl",hashes = {sha256 = "89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6"}}, + {name = "pillow-11.3.0-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/a9/d3/60c781c83a785d6afbd6a326ed4d759d141de43aa7365725cbcd65ce5e54/pillow-11.3.0-cp310-cp310-win_amd64.whl",hashes = {sha256 = "19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f"}}, + {name = "pillow-11.3.0-cp310-cp310-win_arm64.whl",url = "https://files.pythonhosted.org/packages/9f/28/4f4a0203165eefb3763939c6789ba31013a2e90adffb456610f30f613850/pillow-11.3.0-cp310-cp310-win_arm64.whl",hashes = {sha256 = "819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f"}}, + {name = "pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl",url = "https://files.pythonhosted.org/packages/6f/8b/209bd6b62ce8367f47e68a218bffac88888fdf2c9fcf1ecadc6c3ec1ebc7/pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl",hashes = {sha256 = "3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967"}}, + {name = "pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/2e/e6/231a0b76070c2cfd9e260a7a5b504fb72da0a95279410fa7afd99d9751d6/pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl",hashes = {sha256 = "b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe"}}, + {name = "pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",url = "https://files.pythonhosted.org/packages/13/f4/10cf94fda33cb12765f2397fc285fa6d8eb9c29de7f3185165b702fc7386/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",hashes = {sha256 = "e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c"}}, + {name = "pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",url = "https://files.pythonhosted.org/packages/72/c9/583821097dc691880c92892e8e2d41fe0a5a3d6021f4963371d2f6d57250/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",hashes = {sha256 = "d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25"}}, + {name = "pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/3b/8e/5c9d410f9217b12320efc7c413e72693f48468979a013ad17fd690397b9a/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27"}}, + {name = "pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/62/bb/78347dbe13219991877ffb3a91bf09da8317fbfcd4b5f9140aeae020ad71/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a"}}, + {name = "pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl",url = "https://files.pythonhosted.org/packages/d9/28/1000353d5e61498aaeaaf7f1e4b49ddb05f2c6575f9d4f9f914a3538b6e1/pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl",hashes = {sha256 = "8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f"}}, +] +marker = "\"all\" in extras or \"dev\" in extras or \"vision\" in extras" + +[packages.tool.pdm] +dependencies = [] + [[packages]] name = "eval-type-backport" version = "0.2.2" @@ -1351,7 +1351,7 @@ sdist = {name = "httpx-0.28.1.tar.gz", url = "https://files.pythonhosted.org/pac wheels = [ {name = "httpx-0.28.1-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl",hashes = {sha256 = "d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [ @@ -1369,7 +1369,7 @@ sdist = {name = "httpcore-1.0.9.tar.gz", url = "https://files.pythonhosted.org/p wheels = [ {name = "httpcore-1.0.9-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl",hashes = {sha256 = "2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [ @@ -1494,7 +1494,7 @@ sdist = {name = "packaging-25.0.tar.gz", url = "https://files.pythonhosted.org/p wheels = [ {name = "packaging-25.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl",hashes = {sha256 = "29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [] @@ -1507,7 +1507,7 @@ sdist = {name = "typing_extensions-4.15.0.tar.gz", url = "https://files.pythonho wheels = [ {name = "typing_extensions-4.15.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl",hashes = {sha256 = "f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [] @@ -1520,7 +1520,7 @@ sdist = {name = "huggingface_hub-0.35.3.tar.gz", url = "https://files.pythonhost wheels = [ {name = "huggingface_hub-0.35.3-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/31/a0/651f93d154cb72323358bf2bbae3e642bdb5d2f1bfc874d096f7cb159fa0/huggingface_hub-0.35.3-py3-none-any.whl",hashes = {sha256 = "0e3a01829c19d86d03793e4577816fe3bdfc1602ac62c7fb220d593d351224ba"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [ @@ -1542,7 +1542,7 @@ sdist = {name = "colorama-0.4.6.tar.gz", url = "https://files.pythonhosted.org/p wheels = [ {name = "colorama-0.4.6-py2.py3-none-any.whl",url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl",hashes = {sha256 = "4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [] @@ -1596,7 +1596,7 @@ sdist = {name = "requests-2.32.5.tar.gz", url = "https://files.pythonhosted.org/ wheels = [ {name = "requests-2.32.5-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl",hashes = {sha256 = "2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras or \"openai\" in extras or \"recommended\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [ @@ -1614,7 +1614,7 @@ sdist = {name = "urllib3-2.5.0.tar.gz", url = "https://files.pythonhosted.org/pa wheels = [ {name = "urllib3-2.5.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl",hashes = {sha256 = "e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras or \"openai\" in extras or \"recommended\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [] @@ -1627,7 +1627,7 @@ sdist = {name = "tqdm-4.67.1.tar.gz", url = "https://files.pythonhosted.org/pack wheels = [ {name = "tqdm-4.67.1-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl",hashes = {sha256 = "26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [ @@ -1735,7 +1735,7 @@ wheels = [ {name = "charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl",hashes = {sha256 = "a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d"}}, {name = "charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl",url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl",hashes = {sha256 = "cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras or \"openai\" in extras or \"recommended\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [] @@ -1748,7 +1748,7 @@ sdist = {name = "dill-0.4.0.tar.gz", url = "https://files.pythonhosted.org/packa wheels = [ {name = "dill-0.4.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-py3-none-any.whl",hashes = {sha256 = "44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [] @@ -1774,7 +1774,7 @@ sdist = {name = "filelock-3.20.0.tar.gz", url = "https://files.pythonhosted.org/ wheels = [ {name = "filelock-3.20.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl",hashes = {sha256 = "339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras or \"openai\" in extras or \"recommended\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [] @@ -1787,7 +1787,7 @@ sdist = {name = "fsspec-2025.9.0.tar.gz", url = "https://files.pythonhosted.org/ wheels = [ {name = "fsspec-2025.9.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/47/71/70db47e4f6ce3e5c37a607355f80da8860a33226be640226ac52cb05ef2e/fsspec-2025.9.0-py3-none-any.whl",hashes = {sha256 = "530dc2a2af60a414a832059574df4a6e10cce927f6f4a78209390fe38955cfb7"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [] @@ -1901,7 +1901,7 @@ wheels = [ {name = "aiohttp-3.13.0-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/23/ba/47fd065510a8bfab5d5f6e1d97c0de672447c0a941c5021298bd7210afc3/aiohttp-3.13.0-cp310-cp310-win32.whl",hashes = {sha256 = "3b64f22fbb6dcd5663de5ef2d847a5638646ef99112503e6f7704bdecb0d1c4d"}}, {name = "aiohttp-3.13.0-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/c4/38/f5385cb79afa1f31bcaa3625a9e8d849b782edaeac09f894f46439e006a1/aiohttp-3.13.0-cp310-cp310-win_amd64.whl",hashes = {sha256 = "f8d877aa60d80715b2afc565f0f1aea66565824c229a2d065b31670e09fed6d7"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [ @@ -2049,7 +2049,7 @@ wheels = [ {name = "multidict-6.7.0-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/00/35/f6a637ea2c75f0d3b7c7d41b1189189acff0d9deeb8b8f35536bb30f5e33/multidict-6.7.0-cp310-cp310-win_amd64.whl",hashes = {sha256 = "b15b3afff74f707b9275d5ba6a91ae8f6429c3ffb29bbfd216b0b375a56f13d7"}}, {name = "multidict-6.7.0-cp310-cp310-win_arm64.whl",url = "https://files.pythonhosted.org/packages/e7/b8/f7bf8329b39893d02d9d95cf610c75885d12fc0f402b1c894e1c8e01c916/multidict-6.7.0-cp310-cp310-win_arm64.whl",hashes = {sha256 = "4b73189894398d59131a66ff157837b1fafea9974be486d036bb3d32331fdbf0"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [ @@ -2086,7 +2086,7 @@ wheels = [ {name = "hf_xet-1.1.10-cp37-abi3-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/2c/3d/ab7109e607ed321afaa690f557a9ada6d6d164ec852fd6bf9979665dc3d6/hf_xet-1.1.10-cp37-abi3-musllinux_1_2_x86_64.whl",hashes = {sha256 = "f900481cf6e362a6c549c61ff77468bd59d6dd082f3170a36acfef2eb6a6793f"}}, {name = "hf_xet-1.1.10-cp37-abi3-win_amd64.whl",url = "https://files.pythonhosted.org/packages/ee/0e/471f0a21db36e71a2f1752767ad77e92d8cde24e974e03d662931b1305ec/hf_xet-1.1.10-cp37-abi3-win_amd64.whl",hashes = {sha256 = "5f54b19cc347c13235ae7ee98b330c26dd65ef1df47e5316ffb1e87713ca7045"}}, ] -marker = "\"default\" in dependency_groups and (platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"arm64\" or platform_machine == \"aarch64\") or \"all\" in extras and (platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"arm64\" or platform_machine == \"aarch64\") or \"dev\" in extras and (platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"arm64\" or platform_machine == \"aarch64\") or \"multimodal\" in extras and (platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"arm64\" or platform_machine == \"aarch64\")" +marker = "\"default\" in dependency_groups and (platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"arm64\" or platform_machine == \"aarch64\") or \"all\" in extras and (platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"arm64\" or platform_machine == \"aarch64\") or \"audio\" in extras and (platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"arm64\" or platform_machine == \"aarch64\") or \"dev\" in extras and (platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"arm64\" or platform_machine == \"aarch64\") or \"vision\" in extras and (platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"arm64\" or platform_machine == \"aarch64\")" [packages.tool.pdm] dependencies = [] @@ -2125,7 +2125,7 @@ sdist = {name = "idna-3.11.tar.gz", url = "https://files.pythonhosted.org/packag wheels = [ {name = "idna-3.11-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl",hashes = {sha256 = "771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras or \"openai\" in extras or \"recommended\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [] @@ -2463,7 +2463,7 @@ wheels = [ {name = "yarl-1.22.0-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/62/46/94c76196642dbeae634c7a61ba3da88cd77bed875bf6e4a8bed037505aa6/yarl-1.22.0-cp310-cp310-win_amd64.whl",hashes = {sha256 = "cb95a9b1adaa48e41815a55ae740cfda005758104049a640a398120bf02515ca"}}, {name = "yarl-1.22.0-cp310-cp310-win_arm64.whl",url = "https://files.pythonhosted.org/packages/af/af/7df4f179d3b1a6dcb9a4bd2ffbc67642746fcafdb62580e66876ce83fff4/yarl-1.22.0-cp310-cp310-win_arm64.whl",hashes = {sha256 = "b85b982afde6df99ecc996990d4ad7ccbdbb70e2a4ba4de0aecde5922ba98a0b"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [ @@ -2585,7 +2585,7 @@ wheels = [ {name = "propcache-0.4.1-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/a6/67/354aac4e0603a15f76439caf0427781bcd6797f370377f75a642133bc954/propcache-0.4.1-cp310-cp310-win_amd64.whl",hashes = {sha256 = "1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37"}}, {name = "propcache-0.4.1-cp310-cp310-win_arm64.whl",url = "https://files.pythonhosted.org/packages/e0/e1/74e55b9fd1a4c209ff1a9a824bf6c8b3d1fc5a1ac3eabe23462637466785/propcache-0.4.1-cp310-cp310-win_arm64.whl",hashes = {sha256 = "d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [] @@ -2611,7 +2611,7 @@ sdist = {name = "aiohappyeyeballs-2.6.1.tar.gz", url = "https://files.pythonhost wheels = [ {name = "aiohappyeyeballs-2.6.1-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl",hashes = {sha256 = "f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [] @@ -2639,7 +2639,7 @@ sdist = {name = "aiosignal-1.4.0.tar.gz", url = "https://files.pythonhosted.org/ wheels = [ {name = "aiosignal-1.4.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl",hashes = {sha256 = "053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [ @@ -2767,7 +2767,7 @@ wheels = [ {name = "frozenlist-1.8.0-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/66/aa/3f04523fb189a00e147e60c5b2205126118f216b0aa908035c45336e27e4/frozenlist-1.8.0-cp310-cp310-win_amd64.whl",hashes = {sha256 = "667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6"}}, {name = "frozenlist-1.8.0-cp310-cp310-win_arm64.whl",url = "https://files.pythonhosted.org/packages/39/75/1135feecdd7c336938bd55b4dc3b0dfc46d85b9be12ef2628574b28de776/frozenlist-1.8.0-cp310-cp310-win_arm64.whl",hashes = {sha256 = "80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [] @@ -2795,7 +2795,7 @@ sdist = {name = "attrs-25.4.0.tar.gz", url = "https://files.pythonhosted.org/pac wheels = [ {name = "attrs-25.4.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl",hashes = {sha256 = "adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [] @@ -2836,7 +2836,7 @@ sdist = {name = "certifi-2025.10.5.tar.gz", url = "https://files.pythonhosted.or wheels = [ {name = "certifi-2025.10.5-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl",hashes = {sha256 = "0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras or \"openai\" in extras or \"recommended\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"openai\" in extras or \"recommended\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [] @@ -2953,7 +2953,7 @@ sdist = {name = "h11-0.16.0.tar.gz", url = "https://files.pythonhosted.org/packa wheels = [ {name = "h11-0.16.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl",hashes = {sha256 = "63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [] @@ -3052,7 +3052,7 @@ sdist = {name = "jinja2-3.1.6.tar.gz", url = "https://files.pythonhosted.org/pac wheels = [ {name = "jinja2-3.1.6-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl",hashes = {sha256 = "85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras" [packages.tool.pdm] dependencies = [ @@ -3271,7 +3271,7 @@ wheels = [ {name = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl",hashes = {sha256 = "c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"}}, {name = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl",url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl",hashes = {sha256 = "e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras" [packages.tool.pdm] dependencies = [] @@ -3317,7 +3317,7 @@ wheels = [ {name = "multiprocess-0.70.16-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/0f/ab/1e6e8009e380e22254ff539ebe117861e5bdb3bff1fc977920972237c6c7/multiprocess-0.70.16-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl",hashes = {sha256 = "d951bed82c8f73929ac82c61f01a7b5ce8f3e5ef40f5b52553b4f547ce2b08ec"}}, {name = "multiprocess-0.70.16-py310-none-any.whl",url = "https://files.pythonhosted.org/packages/bc/f7/7ec7fddc92e50714ea3745631f79bd9c96424cb2702632521028e57d3a36/multiprocess-0.70.16-py310-none-any.whl",hashes = {sha256 = "c4a9944c67bd49f823687463660a2d6daae94c289adff97e0f9d696ba6371d02"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [ @@ -3345,7 +3345,7 @@ sdist = {name = "networkx-3.5.tar.gz", url = "https://files.pythonhosted.org/pac wheels = [ {name = "networkx-3.5-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl",hashes = {sha256 = "0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec"}}, ] -marker = "\"default\" in dependency_groups and python_version ~= \"3.12\" or \"all\" in extras and python_version ~= \"3.12\" or \"dev\" in extras and python_version ~= \"3.12\" or \"multimodal\" in extras and python_version ~= \"3.12\"" +marker = "\"default\" in dependency_groups and python_version ~= \"3.12\" or \"all\" in extras and python_version ~= \"3.12\" or \"audio\" in extras and python_version ~= \"3.12\" or \"dev\" in extras and python_version ~= \"3.12\"" [packages.tool.pdm] dependencies = [] @@ -3405,7 +3405,7 @@ wheels = [ {name = "pyarrow-21.0.0-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/a6/3e/99473332ac40278f196e105ce30b79ab8affab12f6194802f2593d6b0be2/pyarrow-21.0.0-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "9b0b14b49ac10654332a805aedfc0147fb3469cbf8ea951b3d040dab12372594"}}, {name = "pyarrow-21.0.0-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/7b/f5/c372ef60593d713e8bfbb7e0c743501605f0ad00719146dc075faf11172b/pyarrow-21.0.0-cp310-cp310-win_amd64.whl",hashes = {sha256 = "9d9f8bcb4c3be7738add259738abdeddc363de1b80e3310e04067aa1ca596634"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [] @@ -3563,7 +3563,7 @@ sdist = {name = "sympy-1.14.0.tar.gz", url = "https://files.pythonhosted.org/pac wheels = [ {name = "sympy-1.14.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl",hashes = {sha256 = "e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras" [packages.tool.pdm] dependencies = [ @@ -3577,7 +3577,7 @@ sdist = {name = "mpmath-1.3.0.tar.gz", url = "https://files.pythonhosted.org/pac wheels = [ {name = "mpmath-1.3.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl",hashes = {sha256 = "a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras" [packages.tool.pdm] dependencies = [] @@ -3850,7 +3850,7 @@ sdist = {name = "anyio-4.11.0.tar.gz", url = "https://files.pythonhosted.org/pac wheels = [ {name = "anyio-4.11.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl",hashes = {sha256 = "0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [ @@ -3868,7 +3868,7 @@ sdist = {name = "sniffio-1.3.1.tar.gz", url = "https://files.pythonhosted.org/pa wheels = [ {name = "sniffio-1.3.1-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl",hashes = {sha256 = "2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [] @@ -3955,7 +3955,7 @@ wheels = [ {name = "pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/10/ae/89b3283800ab58f7af2952704078555fa60c807fff764395bb57ea0b0dbd/pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4"}}, {name = "pandas-2.3.3-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/85/72/530900610650f54a35a19476eca5104f38555afccda1aa11a92ee14cb21d/pandas-2.3.3-cp310-cp310-win_amd64.whl",hashes = {sha256 = "503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [ @@ -3975,7 +3975,7 @@ sdist = {name = "python-dateutil-2.9.0.post0.tar.gz", url = "https://files.pytho wheels = [ {name = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl",url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl",hashes = {sha256 = "a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [ @@ -3989,7 +3989,7 @@ sdist = {name = "pytz-2025.2.tar.gz", url = "https://files.pythonhosted.org/pack wheels = [ {name = "pytz-2025.2-py2.py3-none-any.whl",url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl",hashes = {sha256 = "5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [] @@ -4002,7 +4002,7 @@ sdist = {name = "six-1.17.0.tar.gz", url = "https://files.pythonhosted.org/packa wheels = [ {name = "six-1.17.0-py2.py3-none-any.whl",url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl",hashes = {sha256 = "4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [] @@ -4015,7 +4015,7 @@ sdist = {name = "tzdata-2025.2.tar.gz", url = "https://files.pythonhosted.org/pa wheels = [ {name = "tzdata-2025.2-py2.py3-none-any.whl",url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl",hashes = {sha256 = "1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [] @@ -4286,7 +4286,7 @@ wheels = [ {name = "xxhash-3.6.0-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/bb/cc/53f87e8b5871a6eb2ff7e89c48c66093bda2be52315a8161ddc54ea550c4/xxhash-3.6.0-cp310-cp310-win_amd64.whl",hashes = {sha256 = "e82da5670f2d0d98950317f82a0e4a0197150ff19a6df2ba40399c2a3b9ae5fb"}}, {name = "xxhash-3.6.0-cp310-cp310-win_arm64.whl",url = "https://files.pythonhosted.org/packages/9f/00/60f9ea3bb697667a14314d7269956f58bf56bb73864f8f8d52a3c2535e9a/xxhash-3.6.0-cp310-cp310-win_arm64.whl",hashes = {sha256 = "4a082ffff8c6ac07707fb6b671caf7c6e020c75226c561830b73d862060f281d"}}, ] -marker = "\"default\" in dependency_groups or \"all\" in extras or \"dev\" in extras or \"multimodal\" in extras" +marker = "\"default\" in dependency_groups or \"all\" in extras or \"audio\" in extras or \"dev\" in extras or \"vision\" in extras" [packages.tool.pdm] dependencies = [] @@ -4322,7 +4322,7 @@ wheels = [ {name = "numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543"}}, {name = "numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl",url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl",hashes = {sha256 = "d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00"}}, ] -marker = "python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"default\" in dependency_groups or python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"all\" in extras or python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"dev\" in extras or python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"multimodal\" in extras" +marker = "python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"default\" in dependency_groups or python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"all\" in extras or python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"audio\" in extras or python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"dev\" in extras or python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"vision\" in extras" [packages.tool.pdm] dependencies = [] @@ -4401,7 +4401,7 @@ sdist = {name = "async_timeout-5.0.1.tar.gz", url = "https://files.pythonhosted. wheels = [ {name = "async_timeout-5.0.1-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl",hashes = {sha256 = "39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}}, ] -marker = "\"default\" in dependency_groups and python_full_version ~= \"3.10.0\" or \"all\" in extras and python_full_version ~= \"3.10.0\" or \"dev\" in extras and python_full_version ~= \"3.10.0\" or \"multimodal\" in extras and python_full_version ~= \"3.10.0\"" +marker = "\"default\" in dependency_groups and python_full_version ~= \"3.10.0\" or \"all\" in extras and python_full_version ~= \"3.10.0\" or \"audio\" in extras and python_full_version ~= \"3.10.0\" or \"dev\" in extras and python_full_version ~= \"3.10.0\" or \"vision\" in extras and python_full_version ~= \"3.10.0\"" [packages.tool.pdm] dependencies = [] @@ -4414,7 +4414,7 @@ sdist = {name = "exceptiongroup-1.3.0.tar.gz", url = "https://files.pythonhosted wheels = [ {name = "exceptiongroup-1.3.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl",hashes = {sha256 = "4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}}, ] -marker = "\"default\" in dependency_groups and python_full_version ~= \"3.10.0\" or \"all\" in extras and python_full_version ~= \"3.10.0\" or \"dev\" in extras and python_full_version ~= \"3.10.0\" or \"multimodal\" in extras and python_full_version ~= \"3.10.0\"" +marker = "\"default\" in dependency_groups and python_full_version ~= \"3.10.0\" or \"all\" in extras and python_full_version ~= \"3.10.0\" or \"audio\" in extras and python_full_version ~= \"3.10.0\" or \"dev\" in extras and python_full_version ~= \"3.10.0\" or \"vision\" in extras and python_full_version ~= \"3.10.0\"" [packages.tool.pdm] dependencies = [ @@ -4445,7 +4445,7 @@ sdist = {name = "networkx-3.4.2.tar.gz", url = "https://files.pythonhosted.org/p wheels = [ {name = "networkx-3.4.2-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl",hashes = {sha256 = "df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f"}}, ] -marker = "python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"default\" in dependency_groups or python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"all\" in extras or python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"dev\" in extras or python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"multimodal\" in extras" +marker = "python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"default\" in dependency_groups or python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"all\" in extras or python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"audio\" in extras or python_full_version >= \"3.10.0\" and python_version < \"3.12\" and \"dev\" in extras" [packages.tool.pdm] dependencies = [] @@ -4464,7 +4464,7 @@ marker = "python_full_version >= \"3.10.0\" and python_full_version < \"3.10.2\" dependencies = [] [tool.pdm] -hashes = {sha256 = "ef37fbd12acf242b3c4a4726995f99e234a785db8f38c2a37851a2948023ea96"} +hashes = {sha256 = "a61aad0c4563f9e4a33622000214136c2a7aa01d28a2e89e220a415039e7e3eb"} strategy = ["inherit_metadata", "static_urls"] [[tool.pdm.targets]] diff --git a/pyproject.toml b/pyproject.toml index f51560dd..b117ee15 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,18 +76,22 @@ dependencies = [ [project.optional-dependencies] # Meta Extras -all = ["guidellm[perf,openai,multimodal]"] +all = ["guidellm[perf,openai,audio,vision]"] recommended = ["guidellm[perf,openai]"] # Feature Extras perf = ["orjson", "msgpack", "msgspec", "uvloop"] openai = ["tiktoken>=0.11.0", "blobfile>=3.1.0"] -multimodal = [ - "datasets[audio,vision]>=4.1.0", - "pillow", +audio = [ + # Lowest version with full torchcodec support + "datasets[audio]>=4.1.0", # Torchcodec needs specific torch version "torch==2.9.*", "torchcodec==0.8", ] +vision = [ + "datasets[vision]", + "pillow", +] # Dev Tooling dev = [ # Install all optional dependencies diff --git a/src/guidellm/data/preprocessors/formatters.py b/src/guidellm/data/preprocessors/formatters.py index 0b216085..a5d3d0bc 100644 --- a/src/guidellm/data/preprocessors/formatters.py +++ b/src/guidellm/data/preprocessors/formatters.py @@ -22,19 +22,19 @@ class RequestFormatter(DatasetPreprocessor, metaclass=ABCMeta): @staticmethod def encode_audio(*args, **kwargs): - from guidellm.extras.multimodal import encode_audio + from guidellm.extras.audio import encode_audio return encode_audio(*args, **kwargs) @staticmethod def encode_image(*args, **kwargs): - from guidellm.extras.multimodal import encode_image + from guidellm.extras.vision import encode_image return encode_image(*args, **kwargs) @staticmethod def encode_video(*args, **kwargs): - from guidellm.extras.multimodal import encode_video + from guidellm.extras.vision import encode_video return encode_video(*args, **kwargs) diff --git a/src/guidellm/extras/audio.py b/src/guidellm/extras/audio.py new file mode 100644 index 00000000..8d7e7de9 --- /dev/null +++ b/src/guidellm/extras/audio.py @@ -0,0 +1,215 @@ +from __future__ import annotations + +import base64 +from pathlib import Path +from typing import Any, Literal + +import httpx +import numpy as np +import torch + +try: + from torchcodec import AudioSamples + from torchcodec.decoders import AudioDecoder + from torchcodec.encoders import AudioEncoder +except ImportError as e: + raise ImportError("Please install guidellm[audio] to use audio features") from e + +__all__ = [ + "encode_audio", + "is_url", +] + + +def is_url(text: Any) -> bool: + return isinstance(text, str) and text.startswith(("http://", "https://")) + + +def encode_audio( + audio: AudioDecoder + | bytes + | str + | Path + | np.ndarray + | torch.Tensor + | dict[str, Any], + b64encode: bool = False, + sample_rate: int | None = None, + file_name: str = "audio.wav", + encode_sample_rate: int = 16000, + max_duration: float | None = None, + mono: bool = True, + audio_format: str = "mp3", + bitrate: str = "64k", +) -> dict[ + Literal[ + "type", + "audio", + "format", + "mimetype", + "audio_samples", + "audio_seconds", + "audio_bytes", + "file_name", + ], + str | int | float | bytes | None, +]: + """Decode audio (if necessary) and re-encode to specified format.""" + samples = _decode_audio(audio, sample_rate=sample_rate, max_duration=max_duration) + + bitrate_val = ( + int(bitrate.rstrip("k")) * 1000 if bitrate.endswith("k") else int(bitrate) + ) + format_val = audio_format.lower() + + encoded_audio = _encode_audio( + samples=samples, + resample_rate=encode_sample_rate, + bitrate=bitrate_val, + audio_format=format_val, + mono=mono, + ) + + return { + "type": "audio_base64" if b64encode else "audio_file", + "audio": ( + base64.b64encode(encoded_audio).decode("utf-8") + if b64encode + else encoded_audio + ), + "file_name": get_file_name(audio) + if isinstance(audio, str | Path) + else file_name, + "format": audio_format, + "mimetype": f"audio/{format_val}", + "audio_samples": samples.sample_rate, + "audio_seconds": samples.duration_seconds, + "audio_bytes": len(encoded_audio), + } + + +def _decode_audio( # noqa: C901, PLR0912 + audio: AudioDecoder + | bytes + | str + | Path + | np.ndarray + | torch.Tensor + | dict[str, Any], + sample_rate: int | None = None, + max_duration: float | None = None, +) -> AudioSamples: + """Decode audio from various input types into AudioSamples.""" + # If input is a dict, unwrap it into a function call + if isinstance(audio, dict): + sample_rate = audio.get("sample_rate", audio.get("sampling_rate", sample_rate)) + if "data" not in audio and "url" not in audio: + raise ValueError( + f"Audio dict must contain either 'data' or 'url' keys, got {audio}" + ) + return _decode_audio( + audio=audio.get("data") or audio.get("url"), + sample_rate=sample_rate, + max_duration=max_duration, + ) + + # Convert numpy array to torch tensor and re-call + if isinstance(audio, np.ndarray): + return _decode_audio( + audio=torch.from_numpy(audio), + sample_rate=sample_rate, + max_duration=max_duration, + ) + + samples: AudioSamples + + data: torch.Tensor | bytes + # HF datasets return AudioDecoder for audio column + if isinstance(audio, AudioDecoder): + samples = audio.get_samples_played_in_range(stop_seconds=max_duration) + elif isinstance(audio, torch.Tensor): + # If float stream assume decoded audio + if torch.is_floating_point(audio): + if sample_rate is None: + raise ValueError("Sample rate must be set for decoded audio") + + full_duration = audio.shape[1] / sample_rate + # If max_duration is set, trim the audio to that duration + if max_duration is not None: + num_samples = int(max_duration * sample_rate) + duration = min(max_duration, full_duration) + data = audio[:, :num_samples] + else: + duration = full_duration + data = audio + + samples = AudioSamples( + data=data, + pts_seconds=0.0, + duration_seconds=duration, + sample_rate=sample_rate, + ) + # If bytes tensor assume encoded audio + elif audio.dtype == torch.uint8: + decoder = AudioDecoder( + source=audio, + sample_rate=sample_rate, + ) + samples = decoder.get_samples_played_in_range(stop_seconds=max_duration) + + else: + raise ValueError(f"Unsupported audio type: {type(audio)}") + + # If bytes, assume encoded audio + elif isinstance(audio, bytes): + decoder = AudioDecoder( + source=audio, + sample_rate=sample_rate, + ) + samples = decoder.get_samples_played_in_range(stop_seconds=max_duration) + + # If str or Path, assume file path or URL to encoded audio + elif isinstance(audio, str | Path): + if isinstance(audio, str) and is_url(audio): + response = httpx.get(audio) + response.raise_for_status() + data = response.content + else: + if not Path(audio).exists(): + raise ValueError(f"Audio file does not exist: {audio}") + data = Path(audio).read_bytes() + decoder = AudioDecoder( + source=data, + ) + samples = decoder.get_samples_played_in_range(stop_seconds=max_duration) + else: + raise ValueError(f"Unsupported audio type: {type(audio)}") + + return samples + + +def _encode_audio( + samples: AudioSamples, + resample_rate: int | None = None, + bitrate: int = 64000, + audio_format: str = "mp3", + mono: bool = True, +) -> bytes: + encoder = AudioEncoder( + samples=samples.data, + sample_rate=samples.sample_rate, + ) + + audio_tensor = encoder.to_tensor( + format=audio_format, + bit_rate=bitrate if audio_format == "mp3" else None, + num_channels=1 if mono else None, + sample_rate=resample_rate, + ) + + return audio_tensor.numpy().tobytes() + + +def get_file_name(path: Path | str) -> str: + """Get file name from path.""" + return Path(path).name diff --git a/src/guidellm/extras/multimodal.py b/src/guidellm/extras/vision.py similarity index 55% rename from src/guidellm/extras/multimodal.py rename to src/guidellm/extras/vision.py index 37d64f96..035c699f 100644 --- a/src/guidellm/extras/multimodal.py +++ b/src/guidellm/extras/vision.py @@ -7,20 +7,15 @@ import httpx import numpy as np -import torch try: from PIL import Image as PILImage - from torchcodec import AudioSamples - from torchcodec.decoders import AudioDecoder - from torchcodec.encoders import AudioEncoder except ImportError as e: raise ImportError( - "Please install guidellm[multimodal] to use audio/image/video features" + "Please install guidellm[vision] to use image/video features" ) from e __all__ = [ - "encode_audio", "encode_image", "encode_video", "get_file_format", @@ -241,196 +236,6 @@ def encode_video( } -def encode_audio( - audio: AudioDecoder - | bytes - | str - | Path - | np.ndarray - | torch.Tensor - | dict[str, Any], - b64encode: bool = False, - sample_rate: int | None = None, - file_name: str = "audio.wav", - encode_sample_rate: int = 16000, - max_duration: float | None = None, - mono: bool = True, - audio_format: str = "mp3", - bitrate: str = "64k", -) -> dict[ - Literal[ - "type", - "audio", - "format", - "mimetype", - "audio_samples", - "audio_seconds", - "audio_bytes", - "file_name", - ], - str | int | float | bytes | None, -]: - """Decode audio (if necessary) and re-encode to specified format.""" - samples = _decode_audio(audio, sample_rate=sample_rate, max_duration=max_duration) - - bitrate_val = ( - int(bitrate.rstrip("k")) * 1000 if bitrate.endswith("k") else int(bitrate) - ) - format_val = audio_format.lower() - - encoded_audio = _encode_audio( - samples=samples, - resample_rate=encode_sample_rate, - bitrate=bitrate_val, - audio_format=format_val, - mono=mono, - ) - - return { - "type": "audio_base64" if b64encode else "audio_file", - "audio": ( - base64.b64encode(encoded_audio).decode("utf-8") - if b64encode - else encoded_audio - ), - "file_name": get_file_name(audio) - if isinstance(audio, str | Path) - else file_name, - "format": audio_format, - "mimetype": f"audio/{format_val}", - "audio_samples": samples.sample_rate, - "audio_seconds": samples.duration_seconds, - "audio_bytes": len(encoded_audio), - } - - -def _decode_audio( # noqa: C901, PLR0912 - audio: AudioDecoder - | bytes - | str - | Path - | np.ndarray - | torch.Tensor - | dict[str, Any], - sample_rate: int | None = None, - max_duration: float | None = None, -) -> AudioSamples: - """Decode audio from various input types into AudioSamples.""" - # If input is a dict, unwrap it into a function call - if isinstance(audio, dict): - sample_rate = audio.get("sample_rate", audio.get("sampling_rate", sample_rate)) - if "data" not in audio and "url" not in audio: - raise ValueError( - f"Audio dict must contain either 'data' or 'url' keys, got {audio}" - ) - return _decode_audio( - audio=audio.get("data") or audio.get("url"), - sample_rate=sample_rate, - max_duration=max_duration, - ) - - # Convert numpy array to torch tensor and re-call - if isinstance(audio, np.ndarray): - return _decode_audio( - audio=torch.from_numpy(audio), - sample_rate=sample_rate, - max_duration=max_duration, - ) - - samples: AudioSamples - - data: torch.Tensor | bytes - # HF datasets return AudioDecoder for audio column - if isinstance(audio, AudioDecoder): - samples = audio.get_samples_played_in_range(stop_seconds=max_duration) - elif isinstance(audio, torch.Tensor): - # If float stream assume decoded audio - if torch.is_floating_point(audio): - if sample_rate is None: - raise ValueError("Sample rate must be set for decoded audio") - - full_duration = audio.shape[1] / sample_rate - # If max_duration is set, trim the audio to that duration - if max_duration is not None: - num_samples = int(max_duration * sample_rate) - duration = min(max_duration, full_duration) - data = audio[:, :num_samples] - else: - duration = full_duration - data = audio - - samples = AudioSamples( - data=data, - pts_seconds=0.0, - duration_seconds=duration, - sample_rate=sample_rate, - ) - # If bytes tensor assume encoded audio - elif audio.dtype == torch.uint8: - decoder = AudioDecoder( - source=audio, - sample_rate=sample_rate, - ) - samples = decoder.get_samples_played_in_range(stop_seconds=max_duration) - - else: - raise ValueError(f"Unsupported audio type: {type(audio)}") - - # If bytes, assume encoded audio - elif isinstance(audio, bytes): - decoder = AudioDecoder( - source=audio, - sample_rate=sample_rate, - ) - samples = decoder.get_samples_played_in_range(stop_seconds=max_duration) - - # If str or Path, assume file path or URL to encoded audio - elif isinstance(audio, str | Path): - if isinstance(audio, str) and is_url(audio): - response = httpx.get(audio) - response.raise_for_status() - data = response.content - else: - if not Path(audio).exists(): - raise ValueError(f"Audio file does not exist: {audio}") - data = Path(audio).read_bytes() - decoder = AudioDecoder( - source=data, - ) - samples = decoder.get_samples_played_in_range(stop_seconds=max_duration) - else: - raise ValueError(f"Unsupported audio type: {type(audio)}") - - return samples - - -def _encode_audio( - samples: AudioSamples, - resample_rate: int | None = None, - bitrate: int = 64000, - audio_format: str = "mp3", - mono: bool = True, -) -> bytes: - encoder = AudioEncoder( - samples=samples.data, - sample_rate=samples.sample_rate, - ) - - audio_tensor = encoder.to_tensor( - format=audio_format, - bit_rate=bitrate if audio_format == "mp3" else None, - num_channels=1 if mono else None, - sample_rate=resample_rate, - ) - - return audio_tensor.numpy().tobytes() - - -def get_file_name(path: Path | str) -> str: - """Get file name from path.""" - return Path(path).name - - def get_file_format(path: Path | str) -> str: """Get file format from path extension.""" suffix = Path(path).suffix.lower() From e0fc2e5f717352cdc6aabd9cceeea2d8edbcfa30 Mon Sep 17 00:00:00 2001 From: Samuel Monson Date: Fri, 17 Oct 2025 17:45:08 -0400 Subject: [PATCH 53/57] Ensure all optional dependicies are in container Signed-off-by: Samuel Monson --- Containerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Containerfile b/Containerfile index 1f935623..04740765 100644 --- a/Containerfile +++ b/Containerfile @@ -31,7 +31,7 @@ COPY / /src # Install guidellm and locked dependencies RUN pdm use -p /src -f /opt/app-root \ - && pdm install -p /src --check --prod --no-editable + && pdm install -p /src -G all --check --prod --no-editable # Prod image FROM $BASE_IMAGE From 8bba4dfa4cadbd436fb89897624db886c4722588 Mon Sep 17 00:00:00 2001 From: Samuel Monson Date: Fri, 17 Oct 2025 17:58:36 -0400 Subject: [PATCH 54/57] Add some nice utlities to the image Signed-off-by: Samuel Monson --- Containerfile | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/Containerfile b/Containerfile index 04740765..e3f53065 100644 --- a/Containerfile +++ b/Containerfile @@ -36,14 +36,23 @@ RUN pdm use -p /src -f /opt/app-root \ # Prod image FROM $BASE_IMAGE +# Switch to root for installing packages +USER root + +# Install some helpful utilities +RUN dnf install -y --setopt=install_weak_deps=False \ + vi tar rsync \ + && dnf clean all + +# Switch back to unpriv user +# Root group for k8s +USER 1001:0 + # Add guidellm bin to PATH # Argument defaults can be set with GUIDELLM_ ENV HOME="/home/guidellm" \ GUIDELLM_OUTPUT_PATH="/results/benchmarks.json" -# Make sure root is the primary group -USER 1001:0 - # Create the user home dir WORKDIR $HOME From 58665b686b62e54d9b386f0dcb6d76345a337a36 Mon Sep 17 00:00:00 2001 From: Samuel Monson Date: Fri, 17 Oct 2025 18:05:05 -0400 Subject: [PATCH 55/57] Add ffmpeg for audio Signed-off-by: Samuel Monson --- Containerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Containerfile b/Containerfile index e3f53065..d88b33f2 100644 --- a/Containerfile +++ b/Containerfile @@ -39,9 +39,9 @@ FROM $BASE_IMAGE # Switch to root for installing packages USER root -# Install some helpful utilities +# Install some helpful utilities and deps RUN dnf install -y --setopt=install_weak_deps=False \ - vi tar rsync \ + vi tar rsync ffmpeg-free \ && dnf clean all # Switch back to unpriv user From 9e2b7defd3c4f0c5e5d5a408fef75f2dccecd831 Mon Sep 17 00:00:00 2001 From: Samuel Monson Date: Mon, 20 Oct 2025 15:23:41 -0400 Subject: [PATCH 56/57] Generate synthetic data as multi-turn --- src/guidellm/data/deserializers/synthetic.py | 63 ++++++++++++++++---- 1 file changed, 51 insertions(+), 12 deletions(-) diff --git a/src/guidellm/data/deserializers/synthetic.py b/src/guidellm/data/deserializers/synthetic.py index d9e415c6..fcd9b08a 100644 --- a/src/guidellm/data/deserializers/synthetic.py +++ b/src/guidellm/data/deserializers/synthetic.py @@ -7,7 +7,7 @@ from typing import Any import yaml -from datasets import Features, IterableDataset, Value +from datasets import Features, IterableDataset, List, Value from faker import Faker from pydantic import ConfigDict, Field, model_validator from transformers import PreTrainedTokenizerBase @@ -92,6 +92,26 @@ class SyntheticTextDatasetConfig(StandardBaseModel): gt=0, default=None, ) + turns: int = Field( + description="The number of turns in the conversation.", + gt=0, + default=1, + ) + turns_stdev: int | None = Field( + description="The standard deviation of the number of turns.", + gt=0, + default=None, + ) + turns_min: int | None = Field( + description="The minimum number of turns in the conversation.", + gt=0, + default=None, + ) + turns_max: int | None = Field( + description="The maximum number of turns in the conversation.", + gt=0, + default=None, + ) source: str = Field( description="The source of the text data to be used for generation.", default="data:prideandprejudice.txt.gz", @@ -152,24 +172,43 @@ def __iter__(self) -> Iterator[dict[str, Any]]: random_seed=self.random_seed + 1, # ensure diff dist from prompts ) ) + turns_sampler = iter( + IntegerRangeSampler( + average=self.config.turns, + variance=self.config.turns_stdev, + min_value=self.config.turns_min, + max_value=self.config.turns_max, + random_seed=self.random_seed + 5, # ensure diff dist + ) + ) # Create a shared prefix if specified rand = Random(self.random_seed + 3) prefix_iter = self._create_prefix_iter(faker, rand) while True: - prompt_tokens_count = next(prompt_tokens_sampler) - output_tokens_count = next(output_tokens_sampler) + prompt_tokens_counts = [] + output_tokens_counts = [] + prompts = [] + + # Iterate over each turn + turns = next(turns_sampler) + for _ in range(turns): + prompt_tokens_counts.append(next(prompt_tokens_sampler)) + output_tokens_counts.append(next(output_tokens_sampler)) + prompts.append( + self._create_prompt( + prompt_tokens_counts[-1], faker, f"{samples_generated} " + ) + ) + samples_generated += 1 yield { "prefix": next(prefix_iter), - "prompt": self._create_prompt( - prompt_tokens_count, faker, f"{samples_generated} " - ), - "prompt_tokens_count": prompt_tokens_count, - "output_tokens_count": output_tokens_count, + "prompt": prompts, + "prompt_tokens_count": prompt_tokens_counts, + "output_tokens_count": output_tokens_counts, } - samples_generated += 1 def _create_prompt( self, prompt_tokens_count: int, faker: Faker, unique: str = "" @@ -257,9 +296,9 @@ def __call__( features=Features( { "prefix": Value("string"), - "prompt": Value("string"), - "prompt_tokens_count": Value("int32"), - "output_tokens_count": Value("int32"), + "prompt": List(Value("string")), + "prompt_tokens_count": List(Value("int32")), + "output_tokens_count": List(Value("int32")), } ), ) From fad9e9ce87cfb153ce245f9c66686104a45ba042 Mon Sep 17 00:00:00 2001 From: Samuel Monson Date: Mon, 20 Oct 2025 16:17:26 -0400 Subject: [PATCH 57/57] Hack multiturn into dataset formatters --- src/guidellm/data/preprocessors/formatters.py | 48 ++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/src/guidellm/data/preprocessors/formatters.py b/src/guidellm/data/preprocessors/formatters.py index a5d3d0bc..a243ad8c 100644 --- a/src/guidellm/data/preprocessors/formatters.py +++ b/src/guidellm/data/preprocessors/formatters.py @@ -60,6 +60,29 @@ def __init__( def __call__( self, columns: dict[GenerativeDatasetColumnType, list[Any]] + ) -> GenerationRequest | list[GenerationRequest]: + text_col = columns.get("text_column", []) + if any(isinstance(col, list) for col in text_col): + if len(text_col) > 1: + raise ValueError( + "Multi-turn currently not supported with dataset concatenation." + ) + turns = [{} for _ in range(len(text_col[0]))] + for col_type, col_values in columns.items(): + if isinstance(col_values[0], list): + for turn_idx, turn_value in zip( + range(len(turns)), col_values[0], strict=False + ): + turns[turn_idx][col_type] = [turn_value] + else: + for turn_idx in range(len(turns)): + turns[turn_idx][col_type] = [col_values[0]] + return [self._apply(turn_columns) for turn_columns in turns] + else: + return self._apply(columns) + + def _apply( + self, columns: dict[GenerativeDatasetColumnType, list[Any]] ) -> GenerationRequest: arguments: GenerationRequestArguments = GenerationRequestArguments(body={}) input_metrics = UsageMetrics() @@ -141,7 +164,30 @@ def __init__( encode_kwargs.get("audio", {}) if encode_kwargs else {} ) - def __call__( # noqa: C901, PLR0912, PLR0915 + def __call__( + self, columns: dict[GenerativeDatasetColumnType, list[Any]] + ) -> GenerationRequest | list[GenerationRequest]: + text_col = columns.get("text_column", []) + if any(isinstance(col, list) for col in text_col): + if len(text_col) > 1: + raise ValueError( + "Multi-turn currently not supported with dataset concatenation." + ) + turns = [{} for _ in range(len(text_col[0]))] + for col_type, col_values in columns.items(): + if isinstance(col_values[0], list): + for turn_idx, turn_value in zip( + range(len(turns)), col_values[0], strict=False + ): + turns[turn_idx][col_type] = [turn_value] + else: + for turn_idx in range(len(turns)): + turns[turn_idx][col_type] = [col_values[0]] + return [self._apply(turn_columns) for turn_columns in turns] + else: + return self._apply(columns) + + def _apply( # noqa: C901, PLR0912, PLR0915 self, columns: dict[GenerativeDatasetColumnType, list[Any]] ) -> GenerationRequest: arguments = GenerationRequestArguments(body={})

    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 z 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 Nsi&)-%Q~loCIIrvl;QvY literal 0 HcmV?d00001 diff --git a/docs/assets/sample-output3.png b/docs/assets/sample-output3.png new file mode 100644 index 0000000000000000000000000000000000000000..c5d763a494a2ba24786b3abe1b9c64d13a60e1db GIT binary patch literal 190330 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%0A&Bz2)=-rqh8#$)d^kid;tZS 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 literal 0 HcmV?d00001 diff --git a/docs/examples/practice_on_vllm_simulator.md b/docs/examples/practice_on_vllm_simulator.md new file mode 100644 index 00000000..80e85774 --- /dev/null +++ b/docs/examples/practice_on_vllm_simulator.md @@ -0,0 +1,117 @@ +# GuideLLM Benchmark Testing Best Practice + +Do first easy-go guidellm benchmark testing from scratch using vLLM Simulator. + +## Getting Started + +### 📦 1. Benchmark Testing Environment Setup + +#### 1.1 Create a Conda Environment (recommended) + +```bash +conda create -n guidellm-bench python=3.11 -y +conda activate guidellm-bench +``` + +#### 1.2 Install Dependencies + +```bash +git clone https://github.com/vllm-project/guidellm.git +cd guidellm +pip install guidellm +``` + +For more detailed instructions, refer to [GuideLLM README](https://github.com/vllm-project/guidellm/blob/main/README.md). + +#### 1.3 Verify Installation + +```bash +guidellm --help +``` + +#### 1.4 Startup OpenAI-compatible API in vLLM simulator docker container + +```bash +docker pull ghcr.io/llm-d/llm-d-inference-sim:v0.4.0 + +docker run --rm --publish 8000:8000 \ +ghcr.io/llm-d/llm-d-inference-sim:v0.4.0 \ +--port 8000 \ +--model "Qwen/Qwen2.5-1.5B-Instruct" \ +--lora-modules '{"name":"tweet-summary-0"}' '{"name":"tweet-summary-1"}' +``` + +For more detailed instructions, refer to: [vLLM Simulator](https://llm-d.ai/docs/architecture/Components/inference-sim) + +Docker image versions: [Docker Images](https://github.com/llm-d/llm-d-inference-sim/pkgs/container/llm-d-inference-sim) + +Check open-ai api working via curl: + +- check /v1/models + +```bash +curl --request GET 'http://localhost:8000/v1/models' +``` + +- check /v1/chat/completions + +```bash +curl --request POST 'http://localhost:8000/v1/chat/completions' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "model": "tweet-summary-0", + "stream": false, + "messages": [{"role": "user", "content": "Say this is a test!"}] +}' +``` + +- check /v1/completions + +```bash +curl --request POST 'http://localhost:8000/v1/completions' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "model": "tweet-summary-0", + "stream": false, + "prompt": "Say this is a test!", + "max_tokens": 128 +}' +``` + +#### 1.5 Download Tokenizer + +Download Qwen/Qwen2.5-1.5B-Instruct tokenizer files from [Qwen/Qwen2.5-1.5B-Instruct](https://modelscope.cn/models/Qwen/Qwen2.5-1.5B-Instruct/files) save to local path such as ${local_path}/Qwen2.5-1.5B-Instruct + +```bash +ls ./Qwen2.5-1.5B-Instruct +merges.txt tokenizer.json tokenizer_config.json vocab.json +``` + +______________________________________________________________________ + +## 🚀 2. Running Benchmarks + +```bash +guidellm benchmark \ +--target "http://localhost:8000/" \ +--model "tweet-summary-0" \ +--processor "${local_path}/Qwen2.5-1.5B-Instruct" \ +--rate-type sweep \ +--max-seconds 10 \ +--max-requests 10 \ +--data "prompt_tokens=128,output_tokens=56" +``` + +______________________________________________________________________ + +## 📊 3. Results Interpretation + +![alt text](../assets/sample-output1.png) ![alt text](../assets/sample-output2.png) ![alt text](../assets/sample-output3.png) + +After the benchmark completes, key results are clear and straightforward, such as: + +- **`TTFT`**: Time to First Token +- **`TPOT`**: Time Per Output Token +- **`ITL`**: Inter-Token Latency + +The first benchmark test complete. From 0701389359336bd05b09fc555678809ab5cc535f Mon Sep 17 00:00:00 2001 From: psydok <47638600+psydok@users.noreply.github.com> Date: Thu, 25 Sep 2025 23:14:36 +0500 Subject: [PATCH 02/57] Add formatting to json file with metrics Signed-off-by: psydok <47638600+psydok@users.noreply.github.com> --- src/guidellm/benchmark/output.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/guidellm/benchmark/output.py b/src/guidellm/benchmark/output.py index da868106..9302af84 100644 --- a/src/guidellm/benchmark/output.py +++ b/src/guidellm/benchmark/output.py @@ -144,10 +144,9 @@ def save_json(self, path: Union[str, Path]) -> Path: ) model_dict = self.model_dump() - model_json = json.dumps(model_dict) - with path.open("w") as file: - file.write(model_json) + with path.open("w", encoding="utf-8") as file: + json.dump(model_dict, file, ensure_ascii=False, indent=4) return path From f8f6f9d26e3f2ff9615278265624cce9e1a4d2b6 Mon Sep 17 00:00:00 2001 From: Samuel Monson Date: Tue, 30 Sep 2025 10:21:54 -0400 Subject: [PATCH 03/57] Container CI bugfix and disable dry-run on image cleaner (#379) ## Summary Final pieces needed for image CI work. Fully enables auto `latest`, `stable` tags and old image pruning. ## Details - Add `pipefail` to list-tags command to catch failures - Add missing `ghcr.io/` to skopeo commands - Disable dry-run option for development image cleanup job ## Test Plan Ran with `workflow_dispatch` [see here](https://github.com/vllm-project/guidellm/actions/runs/18108553536) 2025-09-29T15-45-39 2025-09-29T15-46-02 --- - [x] "I certify that all code in this PR is my own, except as noted below." ## Use of AI - [ ] Includes AI-assisted code completion - [ ] Includes code generated by an AI application - [ ] Includes AI-generated tests (NOTE: AI written tests should have a docstring that includes `## WRITTEN BY AI ##`) --------- Signed-off-by: Samuel Monson --- .github/workflows/container-maintenance.yml | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/workflows/container-maintenance.yml b/.github/workflows/container-maintenance.yml index ff75a3f4..86d54263 100644 --- a/.github/workflows/container-maintenance.yml +++ b/.github/workflows/container-maintenance.yml @@ -9,6 +9,9 @@ on: concurrency: group: ${{ github.workflow }} +permissions: + packages: write + jobs: cleanup-container-tags: runs-on: ubuntu-latest @@ -16,12 +19,12 @@ jobs: - name: Delete PR and untagged images older than 2 weeks uses: snok/container-retention-policy@v3.0.0 with: - account: ${{ github.actor }} + account: ${{ github.repository_owner }} token: ${{ github.token }} image-names: ${{ github.event.repository.name }} image-tags: "pr-*" cut-off: 2w - dry-run: true + dry-run: false push-container-tags: runs-on: ubuntu-latest @@ -31,19 +34,20 @@ jobs: - name: Log into ghcr.io uses: redhat-actions/podman-login@v1 with: - username: ${{ github.actor }} + username: ${{ github.repository_owner }} password: ${{ github.token }} registry: ghcr.io/${{ github.repository_owner }} - name: Get list of tags run: | - skopeo list-tags docker://${{ github.repository }} | jq --raw-output '.Tags[]' > tags + set -euo pipefail # Fail pipe if any command fails + skopeo list-tags docker://ghcr.io/${{ github.repository }} | jq --raw-output '.Tags[]' > tags - name: Get latest release and rc tags run: | STABLE_TAG="$(grep -P '^v\d+\.\d+\.\d+$' tags | sort -rV | head -n1)" - echo "STABLE_TAG=${STABLE_TAG:-v0.0.0}" >> $GITHUB_ENV + echo "stable_tag=${STABLE_TAG:-v0.0.0}" >> $GITHUB_ENV LATEST_TAG="$(grep -P '^v\d+\.\d+\.\d+' tags | sort -rV | head -n1)" - echo "LATEST_TAG=${LATEST_TAG:-v0.0.0}" >> $GITHUB_ENV + echo "latest_tag=${LATEST_TAG:-v0.0.0}" >> $GITHUB_ENV - name: Update latest and stable tags run: | - skopeo copy docker://${{ github.repository }}:${{ env.stable_tag }} docker://${{ github.repository }}:stable - skopeo copy docker://${{ github.repository }}:${{ env.latest_tag }} docker://${{ github.repository }}:latest + skopeo copy docker://ghcr.io/${{ github.repository }}:${{ env.stable_tag }} docker://ghcr.io/${{ github.repository }}:stable + skopeo copy docker://ghcr.io/${{ github.repository }}:${{ env.latest_tag }} docker://ghcr.io/${{ github.repository }}:latest From 730eeb17fc7d0128227de01d31c3681b63118529 Mon Sep 17 00:00:00 2001 From: Mark Kurtz Date: Wed, 1 Oct 2025 08:05:07 -0400 Subject: [PATCH 04/57] Initial state for datasets rework to enable multimodal and more complicated combinations --- src/guidellm/__init__.py | 3 + src/guidellm/__main__.py | 97 ++- src/guidellm/backends/__init__.py | 2 + src/guidellm/backends/backend.py | 1 + src/guidellm/backends/objects.py | 152 ++-- src/guidellm/backends/openai.py | 652 ++++++------------ src/guidellm/benchmark/aggregator.py | 227 +++--- src/guidellm/benchmark/entrypoints.py | 268 +++---- src/guidellm/benchmark/objects.py | 8 +- src/guidellm/benchmark/profile.py | 5 +- src/guidellm/data/__init__.py | 52 +- src/guidellm/data/datasets.py | 88 +++ src/guidellm/data/deserializers/__init__.py | 51 ++ .../data/deserializers/deserializer.py | 81 +++ src/guidellm/data/deserializers/file.py | 221 ++++++ .../data/deserializers/huggingface.py | 75 ++ src/guidellm/data/deserializers/memory.py | 191 +++++ src/guidellm/data/deserializers/synthetic.py | 255 +++++++ src/guidellm/data/formatters/__init__.py | 47 ++ src/guidellm/data/formatters/environment.py | 63 ++ src/guidellm/data/formatters/filters.py | 324 +++++++++ src/guidellm/data/formatters/globals.py | 9 + src/guidellm/data/formatters/objects.py | 92 +++ src/guidellm/data/formatters/templates.py | 182 +++++ src/guidellm/data/loaders.py | 93 +++ src/guidellm/data/objects.py | 230 ++++++ src/guidellm/data/preprocessors/__init__.py | 7 + src/guidellm/data/preprocessors/mappers.py | 115 +++ src/guidellm/data/preprocessors/objects.py | 20 + src/guidellm/data/prideandprejudice.txt.gz | Bin 241795 -> 0 bytes src/guidellm/data/utils.py | 161 +++++ src/guidellm/dataset/__init__.py | 22 - src/guidellm/dataset/creator.py | 213 ------ src/guidellm/dataset/entrypoints.py | 42 -- src/guidellm/dataset/file.py | 92 --- src/guidellm/dataset/hf_datasets.py | 62 -- src/guidellm/dataset/in_memory.py | 132 ---- src/guidellm/dataset/synthetic.py | 287 -------- src/guidellm/logger.py | 2 +- src/guidellm/preprocess/dataset.py | 7 +- src/guidellm/request/__init__.py | 18 - src/guidellm/request/loader.py | 284 -------- src/guidellm/request/request.py | 79 --- src/guidellm/request/types.py | 10 - src/guidellm/scheduler/scheduler.py | 2 +- src/guidellm/scheduler/worker_group.py | 9 +- 46 files changed, 2949 insertions(+), 2084 deletions(-) create mode 100644 src/guidellm/data/datasets.py create mode 100644 src/guidellm/data/deserializers/__init__.py create mode 100644 src/guidellm/data/deserializers/deserializer.py create mode 100644 src/guidellm/data/deserializers/file.py create mode 100644 src/guidellm/data/deserializers/huggingface.py create mode 100644 src/guidellm/data/deserializers/memory.py create mode 100644 src/guidellm/data/deserializers/synthetic.py create mode 100644 src/guidellm/data/formatters/__init__.py create mode 100644 src/guidellm/data/formatters/environment.py create mode 100644 src/guidellm/data/formatters/filters.py create mode 100644 src/guidellm/data/formatters/globals.py create mode 100644 src/guidellm/data/formatters/objects.py create mode 100644 src/guidellm/data/formatters/templates.py create mode 100644 src/guidellm/data/loaders.py create mode 100644 src/guidellm/data/objects.py create mode 100644 src/guidellm/data/preprocessors/__init__.py create mode 100644 src/guidellm/data/preprocessors/mappers.py create mode 100644 src/guidellm/data/preprocessors/objects.py delete mode 100644 src/guidellm/data/prideandprejudice.txt.gz create mode 100644 src/guidellm/data/utils.py delete mode 100644 src/guidellm/dataset/__init__.py delete mode 100644 src/guidellm/dataset/creator.py delete mode 100644 src/guidellm/dataset/entrypoints.py delete mode 100644 src/guidellm/dataset/file.py delete mode 100644 src/guidellm/dataset/hf_datasets.py delete mode 100644 src/guidellm/dataset/in_memory.py delete mode 100644 src/guidellm/dataset/synthetic.py delete mode 100644 src/guidellm/request/__init__.py delete mode 100644 src/guidellm/request/loader.py delete mode 100644 src/guidellm/request/request.py delete mode 100644 src/guidellm/request/types.py diff --git a/src/guidellm/__init__.py b/src/guidellm/__init__.py index f2206e94..dde6e937 100644 --- a/src/guidellm/__init__.py +++ b/src/guidellm/__init__.py @@ -7,6 +7,8 @@ import logging import os +from datasets.utils.logging import disable_progress_bar + with ( open(os.devnull, "w") as devnull, # noqa: PTH123 contextlib.redirect_stderr(devnull), @@ -19,6 +21,7 @@ os.environ["TOKENIZERS_PARALLELISM"] = "false" # Silence warnings for tokenizers hf_logging.set_verbosity_error() logging.getLogger("transformers").setLevel(logging.ERROR) + disable_progress_bar() from .logger import configure_logger, logger from .settings import ( diff --git a/src/guidellm/__main__.py b/src/guidellm/__main__.py index 13a748d5..82632bc8 100644 --- a/src/guidellm/__main__.py +++ b/src/guidellm/__main__.py @@ -56,6 +56,11 @@ from guidellm.benchmark.scenario import ( GenerativeTextScenario, ) +from guidellm.data import ( + GenerativeDatasetArgs, + GenerativeRequestFormatter, + GenerativeRequestType, +) from guidellm.mock_server import MockServer, MockServerConfig from guidellm.preprocess.dataset import ShortPromptStrategy, process_dataset from guidellm.scheduler import StrategyType @@ -143,6 +148,7 @@ def benchmark(): @click.option( "--data", type=str, + multiple=True, help=( "The HuggingFace dataset ID, a path to a HuggingFace dataset, " "a path to a data file csv, json, jsonl, or txt, " @@ -197,9 +203,7 @@ def benchmark(): default=None, help=( "A JSON string containing any arguments to pass to the backend as a " - "dict with **kwargs. Headers can be removed by setting their value to " - "null. For example: " - """'{"headers": {"Authorization": null, "Custom-Header": "Custom-Value"}}'""" + "dict with **kwargs." ), ) @click.option( @@ -234,19 +238,72 @@ def benchmark(): @click.option( "--data-args", default=None, - callback=cli_tools.parse_json, + callback=( + lambda _ctx, _param, value: [ + GenerativeDatasetArgs.model_validate_json(val) + if val + else GenerativeDatasetArgs() + for val in value + ] + if value + else None + ), help=( "A JSON string containing any arguments to pass to the dataset creation " "as a dict with **kwargs." ), ) +@click.option( + "--data-samples", + default=-1, + type=int, + help=( + "The number of samples to use from the dataset. If -1 (default), will use all " + "samples in the dataset." + ), +) @click.option( "--data-sampler", default=None, - type=click.Choice(["random"]), + type=click.Choice(["shuffle"]), + help="The data sampler type to use.", +) +@click.option( + "--data-request-type", + default="text_completions", + type=str, + help=( + "The type of request to create for each data sample. " + f"For example, {list(get_literal_vals(GenerativeRequestType))}." + ), +) +@click.option( + "--data-request-template", + default=None, + help=( + "A Jinja2 template string or path to a Jinja2 template file to use for " + "creating requests from the data samples. If not provided, will use a " + "default template based on the request type." + ), +) +@click.option( + "--data-request-extras", + default=None, + callback=cli_tools.parse_json, + help=("A JSON string of extra data to include with each data request."), +) +@click.option( + "--data-request-nonstreaming", + is_flag=True, + help="Set this flag to disable streaming for the data requests.", +) +@click.option( + "--dataloader_kwargs", + default=None, + callback=cli_tools.parse_json, help=( - "The data sampler type to use. 'random' will add a random shuffle on the data. " - "Defaults to None" + "A JSON string containing any arguments to pass to the dataloader constructor " + "as a dict with **kwargs." ), ) # Output configuration @@ -387,7 +444,13 @@ def run( processor, processor_args, data_args, + data_samples, data_sampler, + data_request_type, + data_request_template, + data_request_extras, + data_request_nonstreaming, + dataloader_kwargs, # Output configuration output_path, output_formats, @@ -420,7 +483,8 @@ def run( asyncio.run( benchmark_generative_text( target=target, - data=data, + data=list(data), + # Benchmark configuration profile=profile, rate=rate, random_seed=random_seed, @@ -432,7 +496,22 @@ def run( processor=processor, processor_args=processor_args, data_args=data_args, - data_sampler=data_sampler, + data_samples=data_samples, + data_column_mapper=None, # use default + data_request_formatter=GenerativeRequestFormatter( + request_type=data_request_type, + request_template=data_request_template, + request_extras=data_request_extras, + request_defaults=( + {} # disable defaults if non-streaming + if data_request_nonstreaming + else None + ), + ), + data_preprocessors=None, # no preprocessors through CLI for now + dataloader_sampler=data_sampler, + dataloader_collate_fn=None, # use default + dataloader_kwargs=dataloader_kwargs, # Output configuration output_path=output_path, output_formats=[ diff --git a/src/guidellm/backends/__init__.py b/src/guidellm/backends/__init__.py index 064722ac..4bcf5683 100644 --- a/src/guidellm/backends/__init__.py +++ b/src/guidellm/backends/__init__.py @@ -13,6 +13,7 @@ GenerationRequest, GenerationRequestTimings, GenerationResponse, + GenerationTokenStats, ) from .openai import OpenAIHTTPBackend @@ -22,5 +23,6 @@ "GenerationRequest", "GenerationRequestTimings", "GenerationResponse", + "GenerationTokenStats", "OpenAIHTTPBackend", ] diff --git a/src/guidellm/backends/backend.py b/src/guidellm/backends/backend.py index 8f91d5e7..a7d82979 100644 --- a/src/guidellm/backends/backend.py +++ b/src/guidellm/backends/backend.py @@ -115,5 +115,6 @@ def requests_limit(self) -> int | None: async def default_model(self) -> str | None: """ :return: The default model name or identifier for generation requests. + None if no default model is available. """ ... diff --git a/src/guidellm/backends/objects.py b/src/guidellm/backends/objects.py index 05280940..88d25949 100644 --- a/src/guidellm/backends/objects.py +++ b/src/guidellm/backends/objects.py @@ -6,62 +6,51 @@ implementations. """ -import uuid -from typing import Any, Literal, Optional +from __future__ import annotations + +from typing import Literal from pydantic import Field +from guidellm.data import ( + GenerationRequest, + GenerationRequestArguments, + GenerationRequestTimings, +) from guidellm.scheduler import ( - MeasuredRequestTimings, SchedulerMessagingPydanticRegistry, ) from guidellm.utils import StandardBaseModel __all__ = [ "GenerationRequest", + "GenerationRequestArguments", "GenerationRequestTimings", "GenerationResponse", + "GenerationTokenStats", ] @SchedulerMessagingPydanticRegistry.register() -class GenerationRequest(StandardBaseModel): - """Request model for backend generation operations.""" +class GenerationTokenStats(StandardBaseModel): + """Token statistics for generation requests and responses.""" - request_id: str = Field( - default_factory=lambda: str(uuid.uuid4()), - description="Unique identifier for the request.", - ) - request_type: Literal["text_completions", "chat_completions"] = Field( - default="text_completions", - description=( - "Type of request. 'text_completions' uses backend.text_completions(), " - "'chat_completions' uses backend.chat_completions()." - ), - ) - content: Any = Field( - description=( - "Request content. For text_completions: string or list of strings. " - "For chat_completions: string, list of messages, or raw content " - "(set raw_content=True in params)." - ) - ) - params: dict[str, Any] = Field( - default_factory=dict, - description=( - "Additional parameters passed to backend methods. " - "Common: max_tokens, temperature, stream." - ), + request: int | None = Field( + default=None, description="Number of tokens in the original request." ) - stats: dict[Literal["prompt_tokens"], int] = Field( - default_factory=dict, - description="Request statistics including prompt token count.", - ) - constraints: dict[Literal["output_tokens"], int] = Field( - default_factory=dict, - description="Request constraints such as maximum output tokens.", + response: int | None = Field( + default=None, description="Number of tokens in the generated response." ) + def value( + self, preference: Literal["request", "response"] | None = None + ) -> int | None: + if preference == "request": + return self.request + if preference == "response": + return self.response + return self.response if self.response is not None else self.request + @SchedulerMessagingPydanticRegistry.register() class GenerationResponse(StandardBaseModel): @@ -70,87 +59,32 @@ class GenerationResponse(StandardBaseModel): request_id: str = Field( description="Unique identifier matching the original GenerationRequest." ) - request_args: dict[str, Any] = Field( + request_args: GenerationRequestArguments = Field( description="Arguments passed to the backend for this request." ) - value: Optional[str] = Field( + text: str | None = Field( default=None, - description="Complete generated text content. None for streaming responses.", - ) - delta: Optional[str] = Field( - default=None, description="Incremental text content for streaming responses." + description="The generated response text.", ) iterations: int = Field( default=0, description="Number of generation iterations completed." ) - request_prompt_tokens: Optional[int] = Field( - default=None, description="Token count from the original request prompt." - ) - request_output_tokens: Optional[int] = Field( - default=None, - description="Expected output token count from the original request.", - ) - response_prompt_tokens: Optional[int] = Field( - default=None, description="Actual prompt token count reported by the backend." + + prompt_stats: GenerationTokenStats = Field( + default_factory=GenerationTokenStats, + description="Token statistics from the prompt.", ) - response_output_tokens: Optional[int] = Field( - default=None, description="Actual output token count reported by the backend." + output_stats: GenerationTokenStats = Field( + default_factory=GenerationTokenStats, + description="Token statistics from the generated output.", ) - @property - def prompt_tokens(self) -> Optional[int]: - """ - :return: The number of prompt tokens used in the request - (response_prompt_tokens if available, otherwise request_prompt_tokens). - """ - return self.response_prompt_tokens or self.request_prompt_tokens - - @property - def output_tokens(self) -> Optional[int]: - """ - :return: The number of output tokens generated in the response - (response_output_tokens if available, otherwise request_output_tokens). - """ - return self.response_output_tokens or self.request_output_tokens - - @property - def total_tokens(self) -> Optional[int]: - """ - :return: The total number of tokens used in the request and response. - Sum of prompt_tokens and output_tokens. - """ - if self.prompt_tokens is None or self.output_tokens is None: - return None - return self.prompt_tokens + self.output_tokens - - def preferred_prompt_tokens( - self, preferred_source: Literal["request", "response"] - ) -> Optional[int]: - if preferred_source == "request": - return self.request_prompt_tokens or self.response_prompt_tokens - else: - return self.response_prompt_tokens or self.request_prompt_tokens - - def preferred_output_tokens( - self, preferred_source: Literal["request", "response"] - ) -> Optional[int]: - if preferred_source == "request": - return self.request_output_tokens or self.response_output_tokens - else: - return self.response_output_tokens or self.request_output_tokens - - -@SchedulerMessagingPydanticRegistry.register() -@MeasuredRequestTimings.register("generation_request_timings") -class GenerationRequestTimings(MeasuredRequestTimings): - """Timing model for tracking generation request lifecycle events.""" + def total_tokens( + self, preference: Literal["request", "response"] | None = None + ) -> int | None: + prompt_tokens = self.prompt_stats.value(preference=preference) + output_tokens = self.output_stats.value(preference=preference) - timings_type: Literal["generation_request_timings"] = "generation_request_timings" - first_iteration: Optional[float] = Field( - default=None, - description="Unix timestamp when the first generation iteration began.", - ) - last_iteration: Optional[float] = Field( - default=None, - description="Unix timestamp when the last generation iteration completed.", - ) + if prompt_tokens is None and output_tokens is None: + return None + return (prompt_tokens or 0) + (output_tokens or 0) diff --git a/src/guidellm/backends/openai.py b/src/guidellm/backends/openai.py index ce83076f..22394afe 100644 --- a/src/guidellm/backends/openai.py +++ b/src/guidellm/backends/openai.py @@ -10,17 +10,15 @@ OpenAIHTTPBackend: HTTP backend for OpenAI-compatible API servers. """ -import base64 -import contextlib -import copy +from __future__ import annotations + +import asyncio import json import time from collections.abc import AsyncIterator -from pathlib import Path -from typing import Any, ClassVar, Optional, Union +from typing import Any, cast import httpx -from PIL import Image from pydantic import dataclasses from guidellm.backends.backend import Backend @@ -28,9 +26,18 @@ GenerationRequest, GenerationRequestTimings, GenerationResponse, + GenerationTokenStats, ) from guidellm.scheduler import ScheduledRequestInfo +try: + import orjson + + HAS_ORJSON = True +except ImportError: + orjson = None + HAS_ORJSON = False + __all__ = ["OpenAIHTTPBackend", "UsageStats"] @@ -38,8 +45,18 @@ class UsageStats: """Token usage statistics for generation requests.""" - prompt_tokens: Optional[int] = None - output_tokens: Optional[int] = None + prompt_tokens: int | None = None + output_tokens: int | None = None + + +open_ai_paths: dict[str, str] = { + "health": "health", + "models": "v1/models", + "text_completions": "v1/completions", + "chat_completions": "v1/chat/completions", + "audio_transcriptions": "v1/audio/transcriptions", + "audio_translations": "v1/audio/translations", +} @Backend.register("openai_http") @@ -66,78 +83,34 @@ class OpenAIHTTPBackend(Backend): await backend.process_shutdown() """ - HEALTH_PATH: ClassVar[str] = "/health" - MODELS_PATH: ClassVar[str] = "/v1/models" - TEXT_COMPLETIONS_PATH: ClassVar[str] = "/v1/completions" - CHAT_COMPLETIONS_PATH: ClassVar[str] = "/v1/chat/completions" - - MODELS_KEY: ClassVar[str] = "models" - TEXT_COMPLETIONS_KEY: ClassVar[str] = "text_completions" - CHAT_COMPLETIONS_KEY: ClassVar[str] = "chat_completions" - def __init__( self, target: str, - model: Optional[str] = None, - api_key: Optional[str] = None, - organization: Optional[str] = None, - project: Optional[str] = None, + model: str | None = None, timeout: float = 60.0, http2: bool = True, follow_redirects: bool = True, - max_output_tokens: Optional[int] = None, - stream_response: bool = True, - extra_query: Optional[dict] = None, - extra_body: Optional[dict] = None, - remove_from_body: Optional[list[str]] = None, - headers: Optional[dict] = None, verify: bool = False, + validate_backend: bool | str | dict[str, Any] = True, ): - """ - Initialize OpenAI HTTP backend. - - :param target: Target URL for the OpenAI server (e.g., "http://localhost:8000"). - :param model: Model to use for requests. If None, uses first available model. - :param api_key: API key for authentication. Adds Authorization header - if provided. - :param organization: Organization ID. Adds OpenAI-Organization header - if provided. - :param project: Project ID. Adds OpenAI-Project header if provided. - :param timeout: Request timeout in seconds. Defaults to 60 seconds. - :param http2: Whether to use HTTP/2. Defaults to True. - :param follow_redirects: Whether to follow redirects. Default True. - :param max_output_tokens: Maximum tokens for completions. If None, none is set. - :param stream_response: Whether to stream responses by default. Can be - overridden per request. Defaults to True. - :param extra_query: Additional query parameters. Both general and - endpoint-specific with type keys supported. - :param extra_body: Additional body parameters. Both general and - endpoint-specific with type keys supported. - :param remove_from_body: Parameter names to remove from request bodies. - :param headers: Additional HTTP headers. - :param verify: Whether to verify SSL certificates. Default False. - """ super().__init__(type_="openai_http") # Request Values self.target = target.rstrip("/").removesuffix("/v1") self.model = model - self.headers = self._build_headers(api_key, organization, project, headers) # Store configuration self.timeout = timeout self.http2 = http2 self.follow_redirects = follow_redirects self.verify = verify - self.max_output_tokens = max_output_tokens - self.stream_response = stream_response - self.extra_query = extra_query or {} - self.extra_body = extra_body or {} - self.remove_from_body = remove_from_body or [] + self.validate_backend: dict[str, Any] | None = self._resolve_validate_kwargs( + validate_backend + ) # Runtime state self._in_process = False - self._async_client: Optional[httpx.AsyncClient] = None + self._async_client: httpx.AsyncClient | None = None @property def info(self) -> dict[str, Any]: @@ -147,20 +120,12 @@ def info(self) -> dict[str, Any]: return { "target": self.target, "model": self.model, - "headers": self.headers, "timeout": self.timeout, "http2": self.http2, "follow_redirects": self.follow_redirects, "verify": self.verify, - "max_output_tokens": self.max_output_tokens, - "stream_response": self.stream_response, - "extra_query": self.extra_query, - "extra_body": self.extra_body, - "remove_from_body": self.remove_from_body, - "health_path": self.HEALTH_PATH, - "models_path": self.MODELS_PATH, - "text_completions_path": self.TEXT_COMPLETIONS_PATH, - "chat_completions_path": self.CHAT_COMPLETIONS_PATH, + "openai_paths": open_ai_paths, + "validate_backend": self.validate_backend, } async def process_startup(self): @@ -206,45 +171,17 @@ async def validate(self): """ self._check_in_process() - if self.model: - with contextlib.suppress(httpx.TimeoutException, httpx.HTTPStatusError): - # Model is set, use /health endpoint as first check - target = f"{self.target}{self.HEALTH_PATH}" - headers = self._get_headers() - response = await self._async_client.get(target, headers=headers) # type: ignore [union-attr] - response.raise_for_status() - - return - - with contextlib.suppress(httpx.TimeoutException, httpx.HTTPStatusError): - # Check if models endpoint is available next - models = await self.available_models() - if models and not self.model: - self.model = models[0] - elif not self.model: - raise RuntimeError( - "No model available and could not set a default model " - "from the server's available models." - ) - + if not self.validate_backend: return - with contextlib.suppress(httpx.TimeoutException, httpx.HTTPStatusError): - # Last check, fall back on dummy request to text completions - async for _, __ in self.text_completions( - prompt="Validate backend", - request_id="validate", - output_token_count=1, - stream_response=False, - ): - pass - - return - - raise RuntimeError( - "Backend validation failed. Could not connect to the server or " - "validate the backend configuration." - ) + try: + response = await self._async_client.request(**self.validate_backend) + response.raise_for_status() + except Exception as exc: + raise RuntimeError( + "Backend validation request failed. Could not connect to the server " + "or validate the backend configuration." + ) from exc async def available_models(self) -> list[str]: """ @@ -256,15 +193,13 @@ async def available_models(self) -> list[str]: """ self._check_in_process() - target = f"{self.target}{self.MODELS_PATH}" - headers = self._get_headers() - params = self._get_params(self.MODELS_KEY) - response = await self._async_client.get(target, headers=headers, params=params) # type: ignore [union-attr] + target = f"{self.target}/{open_ai_paths['models']}" + response = await self._async_client.get(target) response.raise_for_status() return [item["id"] for item in response.json()["data"]] - async def default_model(self) -> Optional[str]: + async def default_model(self) -> str | None: """ Get the default model for this backend. @@ -276,11 +211,11 @@ async def default_model(self) -> Optional[str]: models = await self.available_models() return models[0] if models else None - async def resolve( + async def resolve( # noqa: C901 self, request: GenerationRequest, request_info: ScheduledRequestInfo, - history: Optional[list[tuple[GenerationRequest, GenerationResponse]]] = None, + history: list[tuple[GenerationRequest, GenerationResponse]] | None = None, ) -> AsyncIterator[tuple[GenerationResponse, ScheduledRequestInfo]]: """ Process a generation request and yield progressive responses. @@ -300,350 +235,207 @@ async def resolve( "Multi-turn requests with conversation history are not yet supported" ) - response = GenerationResponse( - request_id=request.request_id, - request_args={ - "request_type": request.request_type, - "output_token_count": request.constraints.get("output_tokens"), - **request.params, - }, - value="", - request_prompt_tokens=request.stats.get("prompt_tokens"), - request_output_tokens=request.constraints.get("output_tokens"), - ) request_info.request_timings = GenerationRequestTimings() - request_info.request_timings.request_start = time.time() - - completion_method = ( - self.text_completions - if request.request_type == "text_completions" - else self.chat_completions - ) - completion_kwargs = ( - { - "prompt": request.content, - "request_id": request.request_id, - "output_token_count": request.constraints.get("output_tokens"), - "stream_response": request.params.get("stream", self.stream_response), - **request.params, - } - if request.request_type == "text_completions" - else { - "content": request.content, - "request_id": request.request_id, - "output_token_count": request.constraints.get("output_tokens"), - "stream_response": request.params.get("stream", self.stream_response), - **request.params, - } + request.arguments.url = ( + request.arguments.url or f"{self.target}/{request.arguments.path}" + if request.arguments.path is not None + else f"{self.target}/{open_ai_paths[request.request_type]}" ) + request_info.request_timings.request_start = time.time() - async for delta, usage_stats in completion_method(**completion_kwargs): - if request_info.request_timings.request_start is None: - request_info.request_timings.request_start = time.time() - - if delta is not None: - if request_info.request_timings.first_iteration is None: - request_info.request_timings.first_iteration = time.time() - response.value += delta # type: ignore [operator] - response.delta = delta - request_info.request_timings.last_iteration = time.time() - response.iterations += 1 - - if usage_stats is not None: - request_info.request_timings.request_end = time.time() - response.response_output_tokens = usage_stats.output_tokens - response.response_prompt_tokens = usage_stats.prompt_tokens - - yield response, request_info - - if request_info.request_timings.request_end is None: - request_info.request_timings.request_end = time.time() - response.delta = None - yield response, request_info - - async def text_completions( - self, - prompt: Union[str, list[str]], - request_id: Optional[str], # noqa: ARG002 - output_token_count: Optional[int] = None, - stream_response: bool = True, - **kwargs, - ) -> AsyncIterator[tuple[Optional[str], Optional[UsageStats]]]: - """ - Generate text completions using the /v1/completions endpoint. - - :param prompt: Text prompt(s) for completion. Single string or list. - :param request_id: Request identifier for tracking. - :param output_token_count: Maximum tokens to generate. Overrides default - if specified. - :param stream_response: Whether to stream response progressively. - :param kwargs: Additional request parameters (temperature, top_p, etc.). - :yields: Tuples of (generated_text, usage_stats). First yield is (None, None). - :raises RuntimeError: If backend is not initialized. - :raises HTTPError: If API request fails. - """ - self._check_in_process() - target = f"{self.target}{self.TEXT_COMPLETIONS_PATH}" - headers = self._get_headers() - params = self._get_params(self.TEXT_COMPLETIONS_KEY) - body = self._get_body( - endpoint_type=self.TEXT_COMPLETIONS_KEY, - request_kwargs=kwargs, - max_output_tokens=output_token_count, - prompt=prompt, - ) - yield None, None # Initial yield for async iterator to signal start - - if not stream_response: - response = await self._async_client.post( # type: ignore [union-attr] - target, - headers=headers, - params=params, - json=body, + if not request.arguments.stream: + response = await self._async_client.request( + request.arguments.method or "POST", + request.arguments.url, + content=request.arguments.content_body, + files=request.arguments.files, + json=request.arguments.json_body, + params=request.arguments.params, + headers=request.arguments.headers, ) response.raise_for_status() data = response.json() + prompt_stats, output_stats = self._extract_response_stats(data, request) + request_info.request_timings.request_end = time.time() + yield ( - self._get_completions_text_content(data), - self._get_completions_usage_stats(data), + GenerationResponse( + request_id=request.request_id, + request_args=request.arguments, + text=self._extract_response_text(data), + iterations=0, + prompt_stats=prompt_stats, + output_stats=output_stats, + ), + request_info, ) return - body.update({"stream": True, "stream_options": {"include_usage": True}}) - async with self._async_client.stream( # type: ignore [union-attr] - "POST", - target, - headers=headers, - params=params, - json=body, - ) as stream: - stream.raise_for_status() - async for line in stream.aiter_lines(): - if not line or not line.strip().startswith("data:"): - continue - if line.strip() == "data: [DONE]": - break - data = json.loads(line.strip()[len("data: ") :]) - yield ( - self._get_completions_text_content(data), - self._get_completions_usage_stats(data), - ) - - async def chat_completions( - self, - content: Union[ - str, - list[Union[str, dict[str, Union[str, dict[str, str]]], Path, Image.Image]], - Any, - ], - request_id: Optional[str] = None, # noqa: ARG002 - output_token_count: Optional[int] = None, - raw_content: bool = False, - stream_response: bool = True, - **kwargs, - ) -> AsyncIterator[tuple[Optional[str], Optional[UsageStats]]]: - """ - Generate chat completions using the /v1/chat/completions endpoint. - - Supports multimodal inputs including text and images with message formatting. - - :param content: Chat content - string, list of mixed content, or raw content - when raw_content=True. - :param request_id: Request identifier (currently unused). - :param output_token_count: Maximum tokens to generate. Overrides default - if specified. - :param raw_content: If True, passes content directly without formatting. - :param stream_response: Whether to stream response progressively. - :param kwargs: Additional request parameters (temperature, top_p, tools, etc.). - :yields: Tuples of (generated_text, usage_stats). First yield is (None, None). - :raises RuntimeError: If backend is not initialized. - :raises HTTPError: If API request fails. - """ - self._check_in_process() - target = f"{self.target}{self.CHAT_COMPLETIONS_PATH}" - headers = self._get_headers() - params = self._get_params(self.CHAT_COMPLETIONS_KEY) - body = self._get_body( - endpoint_type=self.CHAT_COMPLETIONS_KEY, - request_kwargs=kwargs, - max_output_tokens=output_token_count, - messages=self._get_chat_messages(content) if not raw_content else content, - **kwargs, - ) - yield None, None # Initial yield for async iterator to signal start + deltas = [] + prompt_stats = None + output_stats = None + end_reached = False + + try: + async with self._async_client.stream( + request.arguments.method or "POST", + request.arguments.url, + content=request.arguments.content_body, + files=request.arguments.files, + json=request.arguments.json_body, + params=request.arguments.params, + headers=request.arguments.headers, + ) as stream: + stream.raise_for_status() + buffer = bytearray() + + async for chunk in stream.aiter_bytes(): + if not chunk or end_reached: + continue + buffer.extend(chunk) + + while (start := buffer.find(b"data:")) != -1 and ( + end := buffer.find(b"\n", start) + ) != -1: + line = buffer[start + len(b"data:") : end].strip() + buffer = buffer[end + 1 :] + + if not line: + continue + + if line == b"[DONE]": + if request_info.request_timings.request_end is None: + request_info.request_timings.request_end = time.time() + end_reached = True + break + + data = ( + json.loads(line) if not HAS_ORJSON else orjson.loads(line) + ) + + if "usage" in data and data["usage"] is not None: + request_info.request_timings.request_end = time.time() + prompt_stats, output_stats = self._extract_response_stats( + data, request + ) + else: + if request_info.request_timings.first_iteration is None: + request_info.request_timings.first_iteration = ( + time.time() + ) + request_info.request_timings.last_iteration = time.time() + deltas.append(self._extract_response_text(data)) - if not stream_response: - response = await self._async_client.post( # type: ignore [union-attr] - target, headers=headers, params=params, json=body - ) - response.raise_for_status() - data = response.json() yield ( - self._get_completions_text_content(data), - self._get_completions_usage_stats(data), + GenerationResponse( + request_id=request.request_id, + request_args=request.arguments, + text="".join(deltas) if deltas else None, + iterations=len(deltas), + prompt_stats=prompt_stats or GenerationTokenStats(), + output_stats=output_stats or GenerationTokenStats(), + ), + request_info, ) - return - - body.update({"stream": True, "stream_options": {"include_usage": True}}) - async with self._async_client.stream( # type: ignore [union-attr] - "POST", target, headers=headers, params=params, json=body - ) as stream: - stream.raise_for_status() - async for line in stream.aiter_lines(): - if not line or not line.strip().startswith("data:"): - continue - if line.strip() == "data: [DONE]": - break - data = json.loads(line.strip()[len("data: ") :]) - yield ( - self._get_completions_text_content(data), - self._get_completions_usage_stats(data), - ) - - def _build_headers( - self, - api_key: Optional[str], - organization: Optional[str], - project: Optional[str], - user_headers: Optional[dict], - ) -> dict[str, str]: - headers = {} - - if api_key: - headers["Authorization"] = ( - f"Bearer {api_key}" if not api_key.startswith("Bearer") else api_key + except asyncio.CancelledError as err: + yield ( # Ensure we yield what we have so far before stopping + GenerationResponse( + request_id=request.request_id, + request_args=request.arguments, + text="".join(deltas) if deltas else None, + iterations=len(deltas), + prompt_stats=prompt_stats or GenerationTokenStats(), + output_stats=output_stats or GenerationTokenStats(), + ), + request_info, ) - if organization: - headers["OpenAI-Organization"] = organization - if project: - headers["OpenAI-Project"] = project - if user_headers: - headers.update(user_headers) + raise err - return {key: val for key, val in headers.items() if val is not None} + def _extract_response_text(self, data: dict) -> str: + if not data: + return None - def _check_in_process(self): - if not self._in_process or self._async_client is None: - raise RuntimeError( - "Backend not started up for process, cannot process requests." - ) + object_type = data.get("object") or data.get("type") - def _get_headers(self) -> dict[str, str]: - return { - "Content-Type": "application/json", - **self.headers, - } + if object_type == "text_completion": + return data.get("choices", [{}])[0].get("text", "") - def _get_params(self, endpoint_type: str) -> dict[str, str]: - if endpoint_type in self.extra_query: - return copy.deepcopy(self.extra_query[endpoint_type]) - return copy.deepcopy(self.extra_query) + if object_type == "chat.completion": + return data.get("choices", [{}])[0].get("message", {}).get("content", "") - def _get_chat_messages( - self, - content: Union[ - str, - list[Union[str, dict[str, Union[str, dict[str, str]]], Path, Image.Image]], - Any, - ], - ) -> list[dict[str, Any]]: - if isinstance(content, str): - return [{"role": "user", "content": content}] - - if not isinstance(content, list): - raise ValueError(f"Unsupported content type: {type(content)}") - - resolved_content = [] - for item in content: - if isinstance(item, dict): - resolved_content.append(item) - elif isinstance(item, str): - resolved_content.append({"type": "text", "text": item}) - elif isinstance(item, (Image.Image, Path)): - resolved_content.append(self._get_chat_message_media_item(item)) - else: - raise ValueError(f"Unsupported content item type: {type(item)}") - - return [{"role": "user", "content": resolved_content}] - - def _get_chat_message_media_item( - self, item: Union[Path, Image.Image] - ) -> dict[str, Any]: - if isinstance(item, Image.Image): - encoded = base64.b64encode(item.tobytes()).decode("utf-8") - return { - "type": "image", - "image": {"url": f"data:image/jpeg;base64,{encoded}"}, - } + if object_type == "chat.completion.chunk": + return data.get("choices", [{}])[0].get("delta", {}).get("content", "") - # Handle file paths - suffix = item.suffix.lower() - if suffix in [".jpg", ".jpeg"]: - image = Image.open(item) - encoded = base64.b64encode(image.tobytes()).decode("utf-8") - return { - "type": "image", - "image": {"url": f"data:image/jpeg;base64,{encoded}"}, - } - elif suffix == ".wav": - encoded = base64.b64encode(item.read_bytes()).decode("utf-8") - return { - "type": "input_audio", - "input_audio": {"data": encoded, "format": "wav"}, - } - else: - raise ValueError(f"Unsupported file type: {suffix}") + if "text" in data: + return data.get("text", "") - def _get_body( - self, - endpoint_type: str, - request_kwargs: Optional[dict[str, Any]], - max_output_tokens: Optional[int] = None, - **kwargs, - ) -> dict[str, Any]: - # Start with endpoint-specific extra body parameters - extra_body: dict = self.extra_body.get(endpoint_type, self.extra_body) - - body = copy.deepcopy(extra_body) - body.update(request_kwargs or {}) - body.update(kwargs) - body["model"] = self.model - - # Handle token limits - max_tokens = max_output_tokens or self.max_output_tokens - if max_tokens is not None: - body.update( - { - "max_tokens": max_tokens, - "max_completion_tokens": max_tokens, - } - ) - # Set stop conditions only for request-level limits - if max_output_tokens: - body.update({"stop": None, "ignore_eos": True}) + if "delta" in data: + return data.get("delta", "") - if self.remove_from_body: - for key in self.remove_from_body: - body.pop(key, None) + raise ValueError(f"Unsupported response format: {data}") - return {key: val for key, val in body.items() if val is not None} + def _extract_response_stats( + self, data: dict, request: GenerationRequest + ) -> tuple[GenerationTokenStats, GenerationTokenStats]: + prompt_stats = GenerationTokenStats() + output_stats = GenerationTokenStats() - def _get_completions_text_content(self, data: dict) -> Optional[str]: - if not data.get("choices"): - return None + if not data or not (usage := cast("dict", data.get("usage"))): + return prompt_stats, output_stats - choice: dict = data["choices"][0] - return ( - choice.get("text") - or choice.get("delta", {}).get("content") - or choice.get("message", {}).get("content") + prompt_stats.request = request.stats.get("prompt_tokens") + prompt_stats.response = usage.get("prompt_tokens", usage.get("input_tokens")) + prompt_token_details = usage.get( + "prompt_tokens_details", usage.get("input_tokens_details") + ) + if prompt_token_details: + for key, val in prompt_token_details.items(): + setattr(prompt_stats, key, val) + + output_stats.request = request.stats.get("output_tokens") + output_stats.response = usage.get( + "completion_tokens", usage.get("output_tokens") + ) + output_token_details = usage.get( + "completion_tokens_details", usage.get("output_tokens_details") ) + if output_token_details: + for key, val in output_token_details.items(): + setattr(output_stats, key, val) - def _get_completions_usage_stats(self, data: dict) -> Optional[UsageStats]: - if not data.get("usage"): + return prompt_stats, output_stats + + def _resolve_validate_kwargs( + self, validate_backend: bool | str | dict[str, Any] + ) -> dict[str, Any] | None: + if not (validate_kwargs := validate_backend): return None - return UsageStats( - prompt_tokens=data["usage"].get("prompt_tokens"), - output_tokens=data["usage"].get("completion_tokens"), - ) + if validate_kwargs is True: + validate_kwargs = "health" + + if isinstance(validate_kwargs, str) and validate_kwargs in open_ai_paths: + validate_kwargs = f"{self.target}/{open_ai_paths[validate_kwargs]}" + + if isinstance(validate_kwargs, str): + validate_kwargs = { + "method": "GET", + "url": validate_kwargs, + } + + if not isinstance(validate_kwargs, dict) or "url" not in validate_kwargs: + raise ValueError( + "validate_backend must be a boolean, string, or dictionary and contain " + f"a target URL. Got: {validate_kwargs}" + ) + + if "method" not in validate_kwargs: + validate_kwargs["method"] = "GET" + + return validate_kwargs + + def _check_in_process(self): + if not self._in_process or self._async_client is None: + raise RuntimeError( + "Backend not started up for process, cannot process requests." + ) diff --git a/src/guidellm/benchmark/aggregator.py b/src/guidellm/benchmark/aggregator.py index e965c482..3040ad36 100644 --- a/src/guidellm/benchmark/aggregator.py +++ b/src/guidellm/benchmark/aggregator.py @@ -532,116 +532,117 @@ def __call__( :return: Updated aggregation state for progress reporting. """ _ = (request,) # unused - if request_info.status not in {"completed", "errored", "cancelled"}: - # Only compile progress stats for processed requests - return None - state["updated_generative_stats"] = True - start_time = scheduler_state.start_time - end_time = ( - safe_getattr(request_info.request_timings, "request_end") - or request_info.scheduler_timings.resolve_end + # Request Concurrency + state.set_metric( + key="requests", + value=scheduler_state.processing_requests, + type_="avg", ) - duration = end_time - start_time if end_time else None - - for prefix in (request_info.status, None): - requests_count = ( - scheduler_state.processed_requests - if prefix is None - else scheduler_state.successful_requests - if request_info.status == "completed" - else scheduler_state.cancelled_requests - if request_info.status == "cancelled" - else scheduler_state.errored_requests + + if request_info.status in {"completed", "errored", "cancelled"}: + # Only compile progress stats for processed requests + state["updated_generative_stats"] = True + start_time = scheduler_state.start_time + end_time = ( + safe_getattr(request_info.request_timings, "request_end") + or request_info.scheduler_timings.resolve_end ) + duration = end_time - start_time if end_time else None + + for prefix in (request_info.status, None): + requests_count = ( + scheduler_state.processed_requests + if prefix is None + else scheduler_state.successful_requests + if request_info.status == "completed" + else scheduler_state.cancelled_requests + if request_info.status == "cancelled" + else scheduler_state.errored_requests + ) - # Requests per Second - if duration is not None: - state.set_metric( - key="requests", - value=safe_divide(requests_count, duration), - type_="rate", + # Requests per Second + if duration is not None: + state.set_metric( + key="requests", + value=safe_divide(requests_count, duration), + type_="rate", + prefix=prefix, + ) + + # Request Latency + state.add_metric( + key="request_latency", + value=safe_getattr(request_info.request_timings, "request_end"), + start_val=safe_getattr( + request_info.request_timings, "request_start" + ), prefix=prefix, ) - # Request Concurrency - state.set_metric( - key="requests", - value=scheduler_state.processing_requests, - type_="avg", - prefix=prefix, - ) - - # Request Latency - state.add_metric( - key="request_latency", - value=safe_getattr(request_info.request_timings, "request_end"), - start_val=safe_getattr(request_info.request_timings, "request_start"), - prefix=prefix, - ) - - # Time to First Token - state.add_metric( - key="time_to_first_token", - value=safe_getattr(request_info.request_timings, "first_iteration"), - start_val=safe_getattr(request_info.request_timings, "request_start"), - prefix=prefix, - ) + # Time to First Token + state.add_metric( + key="time_to_first_token", + value=safe_getattr(request_info.request_timings, "first_iteration"), + start_val=safe_getattr( + request_info.request_timings, "request_start" + ), + prefix=prefix, + ) - output_tokens = safe_getattr(response, "output_tokens") - prompt_tokens = safe_getattr(response, "prompt_tokens") + output_tokens = response.output_stats.value() if response else None + prompt_tokens = response.prompt_stats.value() if response else None + total_tokens = response.total_tokens() if response else None - # Inter Token Latency - state.add_metric( - key="inter_token_latency", - value=safe_getattr(request_info.request_timings, "last_iteration"), - start_val=safe_getattr(request_info.request_timings, "first_iteration"), - count=( - output_tokens - 1 if output_tokens and output_tokens > 1 else None - ), - prefix=prefix, - ) + # Inter Token Latency + state.add_metric( + key="inter_token_latency", + value=safe_getattr(request_info.request_timings, "last_iteration"), + start_val=safe_getattr( + request_info.request_timings, "first_iteration" + ), + count=( + output_tokens - 1 + if output_tokens and output_tokens > 1 + else None + ), + prefix=prefix, + ) - # Time per Output Token - state.add_metric( - key="time_per_output_token", - value=safe_getattr(request_info.request_timings, "request_start"), - start_val=safe_getattr(request_info.request_timings, "last_iteration"), - count=output_tokens, - prefix=prefix, - ) + # Time per Output Token + state.add_metric( + key="time_per_output_token", + value=safe_getattr(request_info.request_timings, "request_start"), + start_val=safe_getattr( + request_info.request_timings, "last_iteration" + ), + count=output_tokens, + prefix=prefix, + ) - # Prompt Tokens - state.add_metric( - key="prompt_tokens", - value=prompt_tokens, - duration=duration, - prefix=prefix, - ) + # Prompt Tokens + state.add_metric( + key="prompt_tokens", + value=prompt_tokens, + duration=duration, + prefix=prefix, + ) - # Output Tokens - state.add_metric( - key="output_tokens", - value=output_tokens, - duration=duration, - prefix=prefix, - ) + # Output Tokens + state.add_metric( + key="output_tokens", + value=output_tokens, + duration=duration, + prefix=prefix, + ) - # Total Tokens - state.add_metric( - key="total_tokens", - value=( - prompt_tokens + output_tokens - if all_defined(prompt_tokens, output_tokens) - else prompt_tokens - if all_defined(prompt_tokens) - else output_tokens - if all_defined(output_tokens) - else None - ), - duration=duration, - prefix=prefix, - ) + # Total Tokens + state.add_metric( + key="total_tokens", + value=total_tokens, + duration=duration, + prefix=prefix, + ) return state @@ -929,29 +930,29 @@ def _is_in_cooldown( @classmethod def _create_generative_request_stats( cls, - response: GenerationResponse, + response: GenerationResponse | None, request: GenerationRequest, request_info: ScheduledRequestInfo, ) -> GenerativeRequestStats: - prompt_tokens = response.preferred_prompt_tokens( - settings.preferred_prompt_tokens_source - ) - output_tokens = response.preferred_output_tokens( - settings.preferred_output_tokens_source - ) - return GenerativeRequestStats( request_id=request.request_id, request_type=request.request_type, - prompt=str(request.content), - request_args=response.request_args, - output=response.value, - iterations=response.iterations, - prompt_tokens=prompt_tokens, - output_tokens=output_tokens, + request_args=request.arguments, + output=response.text if response else None, + iterations=response.iterations if response else 0, + prompt_tokens=( + response.prompt_stats.value(settings.preferred_prompt_tokens_source) + if response + else None + ), + output_tokens=( + response.output_stats.value(settings.preferred_output_tokens_source) + if response + else None + ), total_tokens=( - prompt_tokens + output_tokens - if prompt_tokens is not None and output_tokens is not None + response.total_tokens(settings.preferred_output_tokens_source) + if response else None ), scheduler_info=request_info, diff --git a/src/guidellm/benchmark/entrypoints.py b/src/guidellm/benchmark/entrypoints.py index 828402d8..23bc985a 100644 --- a/src/guidellm/benchmark/entrypoints.py +++ b/src/guidellm/benchmark/entrypoints.py @@ -1,11 +1,11 @@ from __future__ import annotations -from collections.abc import Iterable from pathlib import Path from typing import Any, Literal -from datasets import Dataset, DatasetDict, IterableDataset, IterableDatasetDict +from torch.utils.data import Sampler from transformers import ( # type: ignore[import] + AutoTokenizer, PreTrainedTokenizerBase, ) @@ -26,6 +26,7 @@ from guidellm.benchmark.benchmarker import Benchmarker from guidellm.benchmark.objects import GenerativeBenchmark, GenerativeBenchmarksReport from guidellm.benchmark.output import ( + GenerativeBenchmarkerConsole, GenerativeBenchmarkerOutput, ) from guidellm.benchmark.profile import Profile, ProfileType @@ -33,8 +34,14 @@ BenchmarkerProgress, BenchmarkerProgressGroup, ) -from guidellm.benchmark.scenario import GenerativeTextScenario, Scenario -from guidellm.request import GenerativeRequestLoader +from guidellm.data import ( + DatasetPreprocessor, + GenerativeColumnMapper, + GenerativeDataLoader, + GenerativeRequestCollator, + GenerativeRequestFormatter, +) +from guidellm.data.objects import GenerativeDatasetArgs from guidellm.scheduler import ( ConstraintInitializer, NonDistributedEnvironment, @@ -44,7 +51,6 @@ __all__ = [ "benchmark_generative_text", - "benchmark_with_scenario", "reimport_benchmarks_report", ] @@ -52,113 +58,13 @@ _CURRENT_WORKING_DIR = Path.cwd() -# Data types - -DataType = ( - Iterable[str] - | Iterable[dict[str, Any]] - | Dataset - | DatasetDict - | IterableDataset - | IterableDatasetDict - | str - | Path -) - -OutputFormatType = ( - tuple[str, ...] - | list[str] - | dict[str, str | dict[str, Any] | GenerativeBenchmarkerOutput] - | None -) - - -# Helper functions - -async def initialize_backend( - backend: BackendType | Backend, - target: str, - model: str | None, - backend_kwargs: dict[str, Any] | None, -) -> Backend: - backend = ( - Backend.create( - backend, target=target, model=model, **(backend_kwargs or {}) - ) - if not isinstance(backend, Backend) - else backend - ) - await backend.process_startup() - await backend.validate() - return backend - - -async def resolve_profile( - constraint_inputs: dict[str, int | float], - profile: Profile | str | None, - rate: list[float] | None, - random_seed: int, - constraints: dict[str, ConstraintInitializer | Any], -): - for key, val in constraint_inputs.items(): - if val is not None: - constraints[key] = val - if not isinstance(profile, Profile): - if isinstance(profile, str): - profile = Profile.create( - rate_type=profile, - rate=rate, - random_seed=random_seed, - constraints={**constraints}, - ) - else: - raise ValueError(f"Expected string for profile; got {type(profile)}") - - elif constraints: - raise ValueError( - "Constraints must be empty when providing a Profile instance. " - f"Provided constraints: {constraints} ; provided profile: {profile}" - ) - return profile - -async def resolve_output_formats( - output_formats: OutputFormatType, - output_path: str | Path | None, -) -> dict[str, GenerativeBenchmarkerOutput]: - output_formats = GenerativeBenchmarkerOutput.resolve( - output_formats=(output_formats or {}), output_path=output_path - ) - return output_formats - -async def finalize_outputs( - report: GenerativeBenchmarksReport, - resolved_output_formats: dict[str, GenerativeBenchmarkerOutput] -): - output_format_results = {} - for key, output in resolved_output_formats.items(): - output_result = await output.finalize(report) - output_format_results[key] = output_result - return output_format_results - - -# Complete entrypoints - -async def benchmark_with_scenario(scenario: Scenario, **kwargs): - """ - Run a benchmark using a scenario and specify any extra arguments - """ - - if isinstance(scenario, GenerativeTextScenario): - return await benchmark_generative_text(**vars(scenario), **kwargs) - else: - raise ValueError(f"Unsupported Scenario type {type(scenario)}") - - # @validate_call(config={"arbitrary_types_allowed": True}) -async def benchmark_generative_text( # noqa: C901 +async def benchmark_generative_text( # noqa: C901, PLR0915 + # Required target: str, - data: DataType, - profile: StrategyType | ProfileType | Profile, + data: list[Any], + # Benchmark configuration + profile: StrategyType | ProfileType | Profile = "sweep", rate: float | list[float] | None = None, random_seed: int = 42, # Backend configuration @@ -168,11 +74,22 @@ async def benchmark_generative_text( # noqa: C901 # Data configuration processor: str | Path | PreTrainedTokenizerBase | None = None, processor_args: dict[str, Any] | None = None, - data_args: dict[str, Any] | None = None, - data_sampler: Literal["random"] | None = None, + data_args: list[GenerativeDatasetArgs] | None = None, + data_samples: int = -1, + data_column_mapper: GenerativeColumnMapper | None = None, + data_preprocessors: list[DatasetPreprocessor] | None = None, + data_request_formatter: GenerativeRequestFormatter | None = None, + dataloader_sampler: Sampler[int] | Literal["shuffle"] | None = None, + dataloader_collate_fn: GenerativeRequestCollator | None = None, + dataloader_kwargs: dict[str, Any] | None = None, # Output configuration output_path: str | Path | None = _CURRENT_WORKING_DIR, - output_formats: OutputFormatType = ("console", "json", "html", "csv"), + output_formats: ( + tuple[str, ...] + | list[str] + | dict[str, str | dict[str, Any] | GenerativeBenchmarkerOutput] + | None + ) = ("console", "json", "html", "csv"), # Updates configuration progress: tuple[str, ...] | list[str] | list[BenchmarkerProgress] | None = None, print_updates: bool = False, @@ -196,7 +113,16 @@ async def benchmark_generative_text( # noqa: C901 with console.print_update_step( title=f"Initializing backend {backend}" ) as console_step: - backend = await initialize_backend(backend, target, model, backend_kwargs) + backend = ( + Backend.create( + backend, target=target, model=model, **(backend_kwargs or {}) + ) + if not isinstance(backend, Backend) + else backend + ) + console_step.update(f"{backend.__class__.__name__} backend initialized") + await backend.process_startup() + await backend.validate() console_step.finish( title=f"{backend.__class__.__name__} backend initialized", details=backend.info, @@ -236,19 +162,36 @@ async def benchmark_generative_text( # noqa: C901 with console.print_update_step( title=f"Initializing request loader from {data}" ) as console_step: - request_loader = GenerativeRequestLoader( + + def processor_factory() -> PreTrainedTokenizerBase: + nonlocal processor + if isinstance(processor, PreTrainedTokenizerBase): + return processor + else: + processor = AutoTokenizer.from_pretrained( + processor, + **(processor_args or {}), + ) + return processor + + request_loader = GenerativeDataLoader( data=data, data_args=data_args, - processor=processor, - processor_args=processor_args, - shuffle=data_sampler == "random", + data_samples=data_samples, + processor_factory=processor_factory, + column_mapper=data_column_mapper or GenerativeColumnMapper(), + preprocessors=data_preprocessors or [], + request_formatter=data_request_formatter or GenerativeRequestFormatter(), + sampler=dataloader_sampler, + collate_fn=dataloader_collate_fn, random_seed=random_seed, + **(dataloader_kwargs or {}), ) - unique_requests = request_loader.num_unique_items(raise_err=False) console_step.finish( title=( - f"Request loader initialized with {unique_requests} unique requests " - f"from {data}" + f"Request loader initialized with " + f"{data_samples if data_samples > 0 else 'inf'} " + f"unique requests from {data}" ), details=InfoMixin.extract_from_obj(request_loader), status_level="success", @@ -257,19 +200,27 @@ async def benchmark_generative_text( # noqa: C901 with console.print_update_step( title=f"Resolving profile {profile}" ) as console_step: - profile = await resolve_profile( - { - "max_seconds": max_seconds, - "max_requests": max_requests, - "max_errors": max_errors, - "max_error_rate": max_error_rate, - "max_global_error_rate": max_global_error_rate, - }, - profile, - rate, - random_seed, - constraints, - ) + for key, val in { + "max_seconds": max_seconds, + "max_requests": max_requests, + "max_errors": max_errors, + "max_error_rate": max_error_rate, + "max_global_error_rate": max_global_error_rate, + }.items(): + if val is not None: + constraints[key] = val + if not isinstance(profile, Profile): + profile = Profile.create( + rate_type=profile, + rate=rate, + random_seed=random_seed, + constraints={**constraints}, + ) + elif constraints: + raise ValueError( + "Constraints must be empty when providing a Profile instance. " + f"Provided constraints: {constraints} ; provided profile: {profile}" + ) console_step.finish( title=f"{profile.__class__.__name__} profile resolved", details=InfoMixin.extract_from_obj(profile), @@ -296,10 +247,12 @@ async def benchmark_generative_text( # noqa: C901 ) with console.print_update_step(title="Resolving output formats") as console_step: - resolved_output_formats = await resolve_output_formats(output_formats, output_path) + output_formats = GenerativeBenchmarkerOutput.resolve( + output_formats=(output_formats or {}), output_path=output_path + ) console_step.finish( title="Output formats resolved", - details={key: str(val) for key, val in resolved_output_formats.items()}, + details={key: str(val) for key, val in output_formats.items()}, status_level="success", ) @@ -335,11 +288,14 @@ async def benchmark_generative_text( # noqa: C901 if benchmark: report.benchmarks.append(benchmark) - output_format_results = await finalize_outputs(report, resolved_output_formats) + output_format_results = {} + for key, output in output_formats.items(): + output_result = await output.finalize(report) + output_format_results[key] = output_result console.print("\n\n") console.print_update( - title=f"Benchmarking complete; generated {len(report.benchmarks)} benchmark(s)", + title=f"Benchmarking complete, generated {len(report.benchmarks)} benchmark(s)", status="success", ) for key, value in output_format_results.items(): @@ -348,34 +304,20 @@ async def benchmark_generative_text( # noqa: C901 return report, output_format_results -async def reimport_benchmarks_report( - file: Path, - output_path: Path | None, - output_formats: OutputFormatType = ("console", "json", "html", "csv"), -) -> tuple[GenerativeBenchmarksReport, dict[str, Any]]: +def reimport_benchmarks_report(file: Path, output_path: Path | None) -> None: """ The command-line entry point for re-importing and displaying an - existing benchmarks report. Can also specify an output format. + existing benchmarks report. Can also specify Assumes the file provided exists. """ + report = GenerativeBenchmarksReport.load_file(file) + console_output = GenerativeBenchmarkerConsole() + console_output.finalize(report) console = Console() - with console.print_update_step( - title=f"Loading benchmarks from {file}" - ) as console_step: - report = GenerativeBenchmarksReport.load_file(file) - console_step.finish(f"Import of old benchmarks complete; loaded {len(report.benchmarks)} benchmark(s)") - - with console.print_update_step(title="Resolving output formats") as console_step: - resolved_output_formats = await resolve_output_formats(output_formats, output_path) - console_step.finish( - title="Output formats resolved", - details={key: str(val) for key, val in resolved_output_formats.items()}, - status_level="success", - ) - - output_format_results = await finalize_outputs(report, resolved_output_formats) - for key, value in output_format_results.items(): - console.print_update(title=f" {key:<8}: {value}", status="debug") - - return report, output_format_results + if output_path: + with console.print_update_step( + title=f"Saving benchmarks report to {output_path}..." + ) as console_step: + saved_path = report.save_file(output_path) + console_step.finish(title=f"Benchmarks report saved to {saved_path}") diff --git a/src/guidellm/benchmark/objects.py b/src/guidellm/benchmark/objects.py index 8afabba9..a9b5ff79 100644 --- a/src/guidellm/benchmark/objects.py +++ b/src/guidellm/benchmark/objects.py @@ -34,6 +34,9 @@ from guidellm.benchmark.profile import ( Profile, ) +from guidellm.data import ( + GenerationRequestArguments, +) from guidellm.scheduler import ( ScheduledRequestInfo, SchedulerState, @@ -214,9 +217,8 @@ class GenerativeRequestStats(BenchmarkRequestStats): request_type: Literal["text_completions", "chat_completions"] = Field( description="Type of generative request: text or chat completion" ) - prompt: str = Field(description="Input text prompt for generation") - request_args: dict[str, Any] = Field( - description="Generation parameters and configuration options" + request_args: GenerationRequestArguments | None = Field( + default=None, description="Arguments passed to the backend for this request" ) output: str | None = Field( description="Generated text output, if request completed successfully" diff --git a/src/guidellm/benchmark/profile.py b/src/guidellm/benchmark/profile.py index d2f9d70c..fd2a3850 100644 --- a/src/guidellm/benchmark/profile.py +++ b/src/guidellm/benchmark/profile.py @@ -681,7 +681,10 @@ def next_strategy( prev_benchmark.metrics.requests_per_second.successful.mean ) if self.synchronous_rate <= 0 and self.throughput_rate <= 0: - raise RuntimeError("Invalid rates in sweep; aborting. Were there any successful requests?") + raise RuntimeError( + "Invalid rates in sweep; aborting. " + "Were there any successful requests?" + ) self.measured_rates = list( np.linspace( self.synchronous_rate, diff --git a/src/guidellm/data/__init__.py b/src/guidellm/data/__init__.py index 8a48204e..282c5b59 100644 --- a/src/guidellm/data/__init__.py +++ b/src/guidellm/data/__init__.py @@ -1,4 +1,48 @@ -""" -Required for python < 3.12 -https://docs.python.org/3/library/importlib.resources.html#importlib.resources.files -""" +from .datasets import GenerativeRequestsDataset +from .deserializers import ( + DataNotSupportedError, + DatasetDeserializer, + DatasetDeserializerFactory, +) +from .formatters import ( + GenerativeRequestFormatter, + JinjaEnvironmentMixin, + JinjaFiltersRegistry, + JinjaGlobalsRegistry, + JinjaTemplatesRegistry, +) +from .loaders import GenerativeDataLoader, GenerativeRequestCollator +from .objects import ( + GenerationRequest, + GenerationRequestArguments, + GenerationRequestTimings, + GenerativeDatasetArgs, + GenerativeDatasetColumnType, + GenerativeRequestType, +) +from .preprocessors import ( + DatasetPreprocessor, + GenerativeColumnMapper, +) + +__all__ = [ + "DataNotSupportedError", + "DatasetDeserializer", + "DatasetDeserializerFactory", + "DatasetPreprocessor", + "GenerationRequest", + "GenerationRequestArguments", + "GenerationRequestTimings", + "GenerativeColumnMapper", + "GenerativeDataLoader", + "GenerativeDatasetArgs", + "GenerativeDatasetColumnType", + "GenerativeRequestCollator", + "GenerativeRequestFormatter", + "GenerativeRequestType", + "GenerativeRequestsDataset", + "JinjaEnvironmentMixin", + "JinjaFiltersRegistry", + "JinjaGlobalsRegistry", + "JinjaTemplatesRegistry", +] diff --git a/src/guidellm/data/datasets.py b/src/guidellm/data/datasets.py new file mode 100644 index 00000000..8c24683c --- /dev/null +++ b/src/guidellm/data/datasets.py @@ -0,0 +1,88 @@ +from __future__ import annotations + +from collections.abc import Callable +from typing import Any + +from datasets import Dataset, IterableDataset +from transformers import PreTrainedTokenizerBase + +from guidellm.data.deserializers import DatasetDeserializerFactory +from guidellm.data.formatters import GenerativeRequestFormatter +from guidellm.data.objects import GenerativeDatasetArgs +from guidellm.data.preprocessors import ( + DatasetPreprocessor, + GenerativeColumnMapper, +) +from guidellm.data.utils import datasets_item_iterator, resolve_dataset_split + +__all__ = ["GenerativeRequestsDataset"] + + +class GenerativeRequestsDataset: + @classmethod + def build( + cls, + data: list[Any], + data_args: list[GenerativeDatasetArgs] | None, + data_samples: int, + processor_factory: Callable[[], PreTrainedTokenizerBase], + column_mapper: GenerativeColumnMapper, + preprocessors: list[DatasetPreprocessor], + request_formatter: GenerativeRequestFormatter, + random_seed: int = 42, + ) -> Dataset | IterableDataset: + if not data or not isinstance(data, list): + raise ValueError(f"Data must be a non-empty list, got {data}.") + + if data_args is None: + data_args = [GenerativeDatasetArgs() for _ in data] + + if len(data) != len(data_args): + raise ValueError( + f"Length of data ({len(data)}) must match length of data_args " + f"({len(data_args)})." + ) + + datasets = [] + for datum, args in zip(data, data_args): + datasets.append( + resolve_dataset_split( + dataset=DatasetDeserializerFactory.deserialize( + data=datum, + data_kwargs=args.to_kwargs(), + processor_factory=processor_factory, + random_seed=random_seed, + type_=args.type_, + ), + split=args.split, + ) + ) + + column_mapper.init_data(datasets=datasets, data_args=data_args) + request_formatter.init_data(datasets=datasets, data_args=data_args) + for preprocessor in preprocessors: + preprocessor.init_data(datasets=datasets, data_args=data_args) + + if data_samples > 0: + dataset = Dataset.from_list( + list( + datasets_item_iterator( + datasets=datasets, + data_samples=data_samples, + ) + ) + ) + else: + dataset = IterableDataset.from_generator( + datasets_item_iterator, + gen_kwargs={ + "datasets": datasets, + "data_samples": data_samples, + }, + ) + + dataset = dataset.map(column_mapper) + for preprocessor in preprocessors: + dataset = dataset.map(preprocessor) + + return dataset.map(request_formatter) diff --git a/src/guidellm/data/deserializers/__init__.py b/src/guidellm/data/deserializers/__init__.py new file mode 100644 index 00000000..fdee12ce --- /dev/null +++ b/src/guidellm/data/deserializers/__init__.py @@ -0,0 +1,51 @@ +from .deserializer import ( + DataNotSupportedError, + DatasetDeserializer, + DatasetDeserializerFactory, +) +from .file import ( + ArrowFileDatasetDeserializer, + CSVFileDatasetDeserializer, + DBFileDatasetDeserializer, + HDF5FileDatasetDeserializer, + JSONFileDatasetDeserializer, + ParquetFileDatasetDeserializer, + TarFileDatasetDeserializer, + TextFileDatasetDeserializer, +) +from .huggingface import HuggingFaceDatasetDeserializer +from .memory import ( + InMemoryCsvDatasetDeserializer, + InMemoryDictDatasetDeserializer, + InMemoryDictListDatasetDeserializer, + InMemoryItemListDatasetDeserializer, + InMemoryJsonStrDatasetDeserializer, +) +from .synthetic import ( + SyntheticTextDatasetConfig, + SyntheticTextDatasetDeserializer, + SyntheticTextGenerator, +) + +__all__ = [ + "ArrowFileDatasetDeserializer", + "CSVFileDatasetDeserializer", + "DBFileDatasetDeserializer", + "DataNotSupportedError", + "DatasetDeserializer", + "DatasetDeserializerFactory", + "HDF5FileDatasetDeserializer", + "HuggingFaceDatasetDeserializer", + "InMemoryCsvDatasetDeserializer", + "InMemoryDictDatasetDeserializer", + "InMemoryDictListDatasetDeserializer", + "InMemoryItemListDatasetDeserializer", + "InMemoryJsonStrDatasetDeserializer", + "JSONFileDatasetDeserializer", + "ParquetFileDatasetDeserializer", + "SyntheticTextDatasetConfig", + "SyntheticTextDatasetDeserializer", + "SyntheticTextGenerator", + "TarFileDatasetDeserializer", + "TextFileDatasetDeserializer", +] diff --git a/src/guidellm/data/deserializers/deserializer.py b/src/guidellm/data/deserializers/deserializer.py new file mode 100644 index 00000000..ed9050a1 --- /dev/null +++ b/src/guidellm/data/deserializers/deserializer.py @@ -0,0 +1,81 @@ +from __future__ import annotations + +import contextlib +from collections.abc import Callable +from typing import Any, Protocol, Union, runtime_checkable + +from datasets import Dataset, DatasetDict, IterableDataset, IterableDatasetDict +from transformers import PreTrainedTokenizerBase + +from guidellm.utils import RegistryMixin + +__all__ = [ + "DataNotSupportedError", + "DatasetDeserializer", + "DatasetDeserializerFactory", +] + + +class DataNotSupportedError(Exception): + """Exception raised when data format is not supported by deserializer.""" + + +@runtime_checkable +class DatasetDeserializer(Protocol): + def __call__( + self, + data: Any, + data_kwargs: dict[str, Any], + processor_factory: Callable[[], PreTrainedTokenizerBase], + random_seed: int, + ) -> dict[str, list]: ... + + +class DatasetDeserializerFactory( + RegistryMixin[Union["type[DatasetDeserializer]", DatasetDeserializer]], +): + @classmethod + def deserialize( + cls, + data: Any, + data_kwargs: dict[str, Any], + processor_factory: Callable[[], PreTrainedTokenizerBase], + random_seed: int = 42, + type_: str | None = None, + ) -> Dataset | IterableDataset | DatasetDict | IterableDatasetDict: + if type_ is not None: + deserializer = cls.get_registered_object(type_) + + if deserializer is None: + raise DataNotSupportedError( + f"Deserializer type '{type_}' is not registered. " + f"Available types: {cls.registry}" + ) + elif isinstance(deserializer, type): + deserializer_fn = deserializer() + else: + deserializer_fn = deserializer + + return deserializer_fn( + data=data, + data_kwargs=data_kwargs, + processor_factory=processor_factory, + random_seed=random_seed, + ) + + for deserializer in cls.registered_objects(): + deserializer_fn: DatasetDeserializer = ( + deserializer() if isinstance(deserializer, type) else deserializer + ) + + with contextlib.suppress(DataNotSupportedError): + return deserializer_fn( + data=data, + data_kwargs=data_kwargs, + processor_factory=processor_factory, + random_seed=random_seed, + ) + + raise DataNotSupportedError( + f"No suitable deserializer found for data {data} with kwargs {data_kwargs}." + ) diff --git a/src/guidellm/data/deserializers/file.py b/src/guidellm/data/deserializers/file.py new file mode 100644 index 00000000..53688cf0 --- /dev/null +++ b/src/guidellm/data/deserializers/file.py @@ -0,0 +1,221 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Any, Callable + +import pandas as pd +from datasets import Dataset, load_dataset +from transformers import PreTrainedTokenizerBase + +from guidellm.data.deserializers.deserializer import ( + DataNotSupportedError, + DatasetDeserializer, + DatasetDeserializerFactory, +) + +__all__ = [ + "ArrowFileDatasetDeserializer", + "CSVFileDatasetDeserializer", + "DBFileDatasetDeserializer", + "HDF5FileDatasetDeserializer", + "JSONFileDatasetDeserializer", + "ParquetFileDatasetDeserializer", + "TarFileDatasetDeserializer", + "TextFileDatasetDeserializer", +] + + +@DatasetDeserializerFactory.register("text_file") +class TextFileDatasetDeserializer(DatasetDeserializer): + def __call__( + self, + data: Any, + data_kwargs: dict[str, Any], + processor_factory: Callable[[], PreTrainedTokenizerBase], + random_seed: int, + ) -> dict[str, list]: + _ = (processor_factory, random_seed) # Ignore unused args format errors + + if ( + not isinstance(data, (str, Path)) + or not (path := Path(data)).exists() + or not path.is_file() + or path.suffix.lower() not in {".txt", ".text"} + ): + raise DataNotSupportedError( + "Unsupported data for TextFileDatasetDeserializer, " + f"expected str or Path to a local .txt or .text file, got {data}" + ) + + with path.open() as file: + lines = file.readlines() + + return Dataset.from_dict({"text": lines}, **data_kwargs) + + +@DatasetDeserializerFactory.register("csv_file") +class CSVFileDatasetDeserializer(DatasetDeserializer): + def __call__( + self, + data: Any, + data_kwargs: dict[str, Any], + processor_factory: Callable[[], PreTrainedTokenizerBase], + random_seed: int, + ) -> dict[str, list]: + _ = (processor_factory, random_seed) + if ( + not isinstance(data, (str, Path)) + or not (path := Path(data)).exists() + or not path.is_file() + or path.suffix.lower() != ".csv" + ): + raise DataNotSupportedError( + "Unsupported data for CSVFileDatasetDeserializer, " + f"expected str or Path to a local .csv file, got {data}" + ) + + return load_dataset("csv", data_files=str(path), **data_kwargs) + + +@DatasetDeserializerFactory.register("json_file") +class JSONFileDatasetDeserializer(DatasetDeserializer): + def __call__( + self, + data: Any, + data_kwargs: dict[str, Any], + processor_factory: Callable[[], PreTrainedTokenizerBase], + random_seed: int, + ) -> dict[str, list]: + _ = (processor_factory, random_seed) + if ( + not isinstance(data, (str, Path)) + or not (path := Path(data)).exists() + or not path.is_file() + or path.suffix.lower() not in {".json", ".jsonl"} + ): + raise DataNotSupportedError( + f"Unsupported data for JSONFileDatasetDeserializer, " + f"expected str or Path to a local .json or .jsonl file, got {data}" + ) + + return load_dataset("json", data_files=str(path), **data_kwargs) + + +@DatasetDeserializerFactory.register("parquet_file") +class ParquetFileDatasetDeserializer(DatasetDeserializer): + def __call__( + self, + data: Any, + data_kwargs: dict[str, Any], + processor_factory: Callable[[], PreTrainedTokenizerBase], + random_seed: int, + ) -> dict[str, list]: + _ = (processor_factory, random_seed) + if ( + not isinstance(data, (str, Path)) + or not (path := Path(data)).exists() + or not path.is_file() + or path.suffix.lower() != ".parquet" + ): + raise DataNotSupportedError( + f"Unsupported data for ParquetFileDatasetDeserializer, " + f"expected str or Path to a local .parquet file, got {data}" + ) + + return load_dataset("parquet", data_files=str(path), **data_kwargs) + + +@DatasetDeserializerFactory.register("arrow_file") +class ArrowFileDatasetDeserializer(DatasetDeserializer): + def __call__( + self, + data: Any, + data_kwargs: dict[str, Any], + processor_factory: Callable[[], PreTrainedTokenizerBase], + random_seed: int, + ) -> dict[str, list]: + _ = (processor_factory, random_seed) + if ( + not isinstance(data, (str, Path)) + or not (path := Path(data)).exists() + or not path.is_file() + or path.suffix.lower() != ".arrow" + ): + raise DataNotSupportedError( + f"Unsupported data for ArrowFileDatasetDeserializer, " + f"expected str or Path to a local .arrow file, got {data}" + ) + + return load_dataset("arrow", data_files=str(path), **data_kwargs) + + +@DatasetDeserializerFactory.register("hdf5_file") +class HDF5FileDatasetDeserializer(DatasetDeserializer): + def __call__( + self, + data: Any, + data_kwargs: dict[str, Any], + processor_factory: Callable[[], PreTrainedTokenizerBase], + random_seed: int, + ) -> dict[str, list]: + _ = (processor_factory, random_seed) + if ( + not isinstance(data, (str, Path)) + or not (path := Path(data)).exists() + or not path.is_file() + or path.suffix.lower() not in {".hdf5", ".h5"} + ): + raise DataNotSupportedError( + f"Unsupported data for HDF5FileDatasetDeserializer, " + f"expected str or Path to a local .hdf5 or .h5 file, got {data}" + ) + + return Dataset.from_pandas(pd.read_hdf(str(path)), **data_kwargs) + + +@DatasetDeserializerFactory.register("db_file") +class DBFileDatasetDeserializer(DatasetDeserializer): + def __call__( + self, + data: Any, + data_kwargs: dict[str, Any], + processor_factory: Callable[[], PreTrainedTokenizerBase], + random_seed: int, + ) -> dict[str, list]: + _ = (processor_factory, random_seed) + if ( + not isinstance(data, (str, Path)) + or not (path := Path(data)).exists() + or not path.is_file() + or path.suffix.lower() != ".db" + ): + raise DataNotSupportedError( + f"Unsupported data for DBFileDatasetDeserializer, " + f"expected str or Path to a local .db file, got {data}" + ) + + return Dataset.from_sql(con=str(path), **data_kwargs) + + +@DatasetDeserializerFactory.register("tar_file") +class TarFileDatasetDeserializer(DatasetDeserializer): + def __call__( + self, + data: Any, + data_kwargs: dict[str, Any], + processor_factory: Callable[[], PreTrainedTokenizerBase], + random_seed: int, + ) -> dict[str, list]: + _ = (processor_factory, random_seed) + if ( + not isinstance(data, (str, Path)) + or not (path := Path(data)).exists() + or not path.is_file() + or path.suffix.lower() != ".tar" + ): + raise DataNotSupportedError( + f"Unsupported data for TarFileDatasetDeserializer, " + f"expected str or Path to a local .tar file, got {data}" + ) + + return load_dataset("webdataset", data_files=str(path), **data_kwargs) diff --git a/src/guidellm/data/deserializers/huggingface.py b/src/guidellm/data/deserializers/huggingface.py new file mode 100644 index 00000000..275f7180 --- /dev/null +++ b/src/guidellm/data/deserializers/huggingface.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Any, Callable + +from datasets import ( + Dataset, + DatasetDict, + IterableDataset, + IterableDatasetDict, + load_dataset, + load_from_disk, +) +from transformers import PreTrainedTokenizerBase + +from guidellm.data.deserializers.deserializer import ( + DataNotSupportedError, + DatasetDeserializer, + DatasetDeserializerFactory, +) + +__all__ = ["HuggingFaceDatasetDeserializer"] + + +@DatasetDeserializerFactory.register("huggingface") +class HuggingFaceDatasetDeserializer(DatasetDeserializer): + def __call__( + self, + data: Any, + data_kwargs: dict[str, Any], + processor_factory: Callable[[], PreTrainedTokenizerBase], + random_seed: int, + ) -> dict[str, list]: + _ = (processor_factory, random_seed) + + if isinstance( + data, (Dataset, IterableDataset, DatasetDict, IterableDatasetDict) + ): + return data + + load_error = None + + if ( + isinstance(data, (str, Path)) + and (path := Path(data)).exists() + and ((path.is_file() and path.suffix == ".py") or path.is_dir()) + ): + # Handle python script or nested python script in a directory + try: + return load_dataset(str(data), **data_kwargs) + except Exception as err: # noqa: BLE001 + load_error = err + + if ( + isinstance(data, (str, Path)) + and (path := Path(data)).exists() + and path.is_dir() + ): + # Handle local dataset directory + try: + return load_from_disk(str(data), **data_kwargs) + except Exception as err: # noqa: BLE001 + load_error = err + + not_supported = DataNotSupportedError( + "Unsupported data for HuggingFaceDatasetDeserializer, " + "expected Dataset, IterableDataset, DatasetDict, IterableDatasetDict, " + "str or Path to a local dataset directory or a local .py dataset script, " + f"got {data} and HF load error: {load_error}" + ) + + if load_error is not None: + raise not_supported from load_error + else: + raise not_supported diff --git a/src/guidellm/data/deserializers/memory.py b/src/guidellm/data/deserializers/memory.py new file mode 100644 index 00000000..b04ea6bc --- /dev/null +++ b/src/guidellm/data/deserializers/memory.py @@ -0,0 +1,191 @@ +from __future__ import annotations + +import contextlib +import csv +import json +from io import StringIO +from typing import Any, Callable, cast + +from datasets import Dataset +from transformers import PreTrainedTokenizerBase + +from guidellm.data.deserializers.deserializer import ( + DataNotSupportedError, + DatasetDeserializer, + DatasetDeserializerFactory, +) + +__all__ = [ + "InMemoryCsvDatasetDeserializer", + "InMemoryDictDatasetDeserializer", + "InMemoryDictListDatasetDeserializer", + "InMemoryItemListDatasetDeserializer", + "InMemoryJsonStrDatasetDeserializer", +] + + +@DatasetDeserializerFactory.register("in_memory_dict") +class InMemoryDictDatasetDeserializer(DatasetDeserializer): + def __call__( + self, + data: Any, + data_kwargs: dict[str, Any], + processor_factory: Callable[[], PreTrainedTokenizerBase], + random_seed: int, + ) -> dict[str, list]: + _ = (processor_factory, random_seed) # Ignore unused args format errors + + if ( + not data + or not isinstance(data, dict) + or not all( + isinstance(key, str) and isinstance(val, list) + for key, val in data.items() + ) + ): + raise DataNotSupportedError( + f"Unsupported data for InMemoryDictDatasetDeserializer, " + f"expected dict[str, list], got {data}" + ) + + rows = len(list(data.values())[0]) + if not all(len(val) == rows for val in data.values()): + raise DataNotSupportedError( + "All lists in the data dictionary must have the same length, " + f"expected {rows} for all keys {list(data.keys())}" + ) + + return Dataset.from_dict(data, **data_kwargs) + + +@DatasetDeserializerFactory.register("in_memory_dict_list") +class InMemoryDictListDatasetDeserializer(DatasetDeserializer): + def __call__( + self, + data: Any, + data_kwargs: dict[str, Any], + processor_factory: Callable[[], PreTrainedTokenizerBase], + random_seed: int, + ) -> dict[str, list]: + _ = (processor_factory, random_seed) # Ignore unused args format errors + + if ( + not data + or not isinstance(data, list) + or not all(isinstance(item, dict) for item in data) + or not all(isinstance(key, str) for item in data for key in item) + ): + raise DataNotSupportedError( + f"Unsupported data for InMemoryDictListDatasetDeserializer, " + f"expected list of dicts, got {data}" + ) + + data: list[dict[str, Any]] = cast("list[dict[str, Any]]", data) + first_keys = set(data[0].keys()) + for index, item in enumerate(data): + if set(item.keys()) != first_keys: + raise DataNotSupportedError( + f"All dictionaries must have the same keys. " + f"Expected keys: {first_keys}, " + f"got keys at index {index}: {set(item.keys())}" + ) + + # Convert list of dicts to dict of lists + result_dict = {key: [] for key in first_keys} + for item in data: + for key, value in item.items(): + result_dict[key].append(value) + + return Dataset.from_dict(result_dict, **data_kwargs) + + +@DatasetDeserializerFactory.register("in_memory_item_list") +class InMemoryItemListDatasetDeserializer(DatasetDeserializer): + def __call__( + self, + data: Any, + data_kwargs: dict[str, Any], + processor_factory: Callable[[], PreTrainedTokenizerBase], + random_seed: int, + ) -> dict[str, list]: + _ = (processor_factory, random_seed) # Ignore unused args format errors + + primitive_types = (str, int, float, bool, type(None)) + if ( + not data + or not isinstance(data, list) + or not all(isinstance(item, primitive_types) for item in data) + ): + raise DataNotSupportedError( + f"Unsupported data for InMemoryItemListDatasetDeserializer, " + f"expected list of primitive items, got {data}" + ) + + column_name = data_kwargs.pop("column_name", "data") + + return Dataset.from_dict({column_name: data}, **data_kwargs) + + +@DatasetDeserializerFactory.register("in_memory_json_str") +class InMemoryJsonStrDatasetDeserializer(DatasetDeserializer): + def __call__( + self, + data: Any, + data_kwargs: dict[str, Any], + processor_factory: Callable[[], PreTrainedTokenizerBase], + random_seed: int, + ) -> dict[str, list]: + if ( + isinstance(data, str) + and (json_str := data.strip()) + and ( + (json_str.startswith("{") and json_str.endswith("}")) + or (json_str.startswith("[") and json_str.endswith("]")) + ) + ): + with contextlib.suppress(Exception): + parsed = json.loads(data) + + for deserializer in [ + InMemoryDictDatasetDeserializer, + InMemoryDictListDatasetDeserializer, + InMemoryItemListDatasetDeserializer, + ]: + with contextlib.suppress(DataNotSupportedError): + return deserializer()( + parsed, data_kwargs, processor_factory, random_seed + ) + + raise DataNotSupportedError( + f"Unsupported data for InMemoryJsonStrDatasetDeserializer, " + f"expected JSON string with a list or dict of items, got {data}" + ) + + +@DatasetDeserializerFactory.register("in_memory_csv_str") +class InMemoryCsvDatasetDeserializer(DatasetDeserializer): + def __call__( + self, + data: Any, + data_kwargs: dict[str, Any], + processor_factory: Callable[[], PreTrainedTokenizerBase], + random_seed: int, + ) -> dict[str, list]: + if ( + isinstance(data, str) + and (csv_str := data.strip()) + and len(csv_str.split("\n")) > 0 + ): + with contextlib.suppress(Exception): + csv_buffer = StringIO(data) + reader = csv.DictReader(csv_buffer) + rows = list(reader) + + return InMemoryDictListDatasetDeserializer()( + rows, data_kwargs, processor_factory, random_seed + ) + + raise DataNotSupportedError( + f"Unsupported data for InMemoryCsvDatasetDeserializer, " + f"expected CSV string, got {type(data)}" + ) diff --git a/src/guidellm/data/deserializers/synthetic.py b/src/guidellm/data/deserializers/synthetic.py new file mode 100644 index 00000000..2335596d --- /dev/null +++ b/src/guidellm/data/deserializers/synthetic.py @@ -0,0 +1,255 @@ +from __future__ import annotations + +from collections.abc import Iterator +from pathlib import Path +from typing import Any, Callable + +import yaml +from datasets import Features, IterableDataset, Value +from faker import Faker +from pydantic import Field +from transformers import PreTrainedTokenizerBase + +from guidellm.data.deserializers.deserializer import ( + DataNotSupportedError, + DatasetDeserializer, + DatasetDeserializerFactory, +) +from guidellm.utils import IntegerRangeSampler, StandardBaseModel + +__all__ = [ + "SyntheticTextDatasetConfig", + "SyntheticTextDatasetDeserializer", + "SyntheticTextGenerator", +] + + +class SyntheticTextDatasetConfig(StandardBaseModel): + prompt_tokens: int = Field( + description="The average number of text tokens generated for prompts.", + gt=0, + ) + prompt_tokens_stdev: int | None = Field( + description="The standard deviation of the tokens generated for prompts.", + gt=0, + default=None, + ) + prompt_tokens_min: int | None = Field( + description="The minimum number of text tokens generated for prompts.", + gt=0, + default=None, + ) + prompt_tokens_max: int | None = Field( + description="The maximum number of text tokens generated for prompts.", + gt=0, + default=None, + ) + output_tokens: int = Field( + description="The average number of text tokens generated for outputs.", + gt=0, + ) + output_tokens_stdev: int | None = Field( + description="The standard deviation of the tokens generated for outputs.", + gt=0, + default=None, + ) + output_tokens_min: int | None = Field( + description="The minimum number of text tokens generated for outputs.", + gt=0, + default=None, + ) + output_tokens_max: int | None = Field( + description="The maximum number of text tokens generated for outputs.", + gt=0, + default=None, + ) + source: str = Field( + description="The source of the text data to be used for generation.", + default="data:prideandprejudice.txt.gz", + ) + + +class SyntheticTextGenerator: + def __init__( + self, + config: SyntheticTextDatasetConfig, + processor: PreTrainedTokenizerBase, + random_seed: int = 42, + ): + self.config = config + self.processor = processor + self.random_seed = random_seed + + def __iter__(self) -> Iterator[dict[str, Any]]: + samples_generated = 0 + + faker = Faker() + faker.seed_instance(self.random_seed) + prompt_tokens_sampler = iter( + IntegerRangeSampler( + average=self.config.prompt_tokens, + variance=self.config.prompt_tokens_stdev, + min_value=self.config.prompt_tokens_min, + max_value=self.config.prompt_tokens_max, + random_seed=self.random_seed, + ) + ) + output_tokens_sampler = iter( + IntegerRangeSampler( + average=self.config.output_tokens, + variance=self.config.output_tokens_stdev, + min_value=self.config.output_tokens_min, + max_value=self.config.output_tokens_max, + random_seed=self.random_seed + 1, # ensure diff dist from prompts + ) + ) + + while True: + prompt_tokens_count = next(prompt_tokens_sampler) + output_tokens_count = next(output_tokens_sampler) + + yield { + "prompt": self._create_prompt( + prompt_tokens_count, samples_generated, faker + ), + "prompt_tokens_count": prompt_tokens_count, + "output_tokens_count": output_tokens_count, + } + samples_generated += 1 + + def _create_prompt(self, prompt_tokens_count: int, index: int, faker: Faker) -> str: + prompt_token_ids = [] + avg_chars_per_token = 5 + margin_of_safety = 1.5 + attempts = 0 + + while len(prompt_token_ids) < prompt_tokens_count: + attempts += 1 + num_chars = ( + prompt_tokens_count * avg_chars_per_token * margin_of_safety * attempts + ) + text = f"{index} " + faker.text(max_nb_chars=num_chars) + prompt_token_ids = self.processor.encode(text) + + return self.processor.decode( + prompt_token_ids[:prompt_tokens_count], skip_special_tokens=True + ) + + +@DatasetDeserializerFactory.register("synthetic_text") +class SyntheticTextDatasetDeserializer(DatasetDeserializer): + def __call__( + self, + data: Any, + data_kwargs: dict[str, Any], + processor_factory: Callable[[], PreTrainedTokenizerBase], + random_seed: int, + ) -> IterableDataset: + # Config file pathways, deserialize and call self again + if (config := self._load_config_file(data)) is not None: + return self(config, data_kwargs, processor_factory, random_seed) + + # Config str pathways, deserialize and call self again + if (config := self._load_config_str(data)) is not None: + return self(config, data_kwargs, processor_factory, random_seed) + + if not isinstance(data, SyntheticTextDatasetConfig): + raise DataNotSupportedError( + "Unsupported data for SyntheticTextDatasetDeserializer, " + "expected SyntheticTextDatasetConfig, str or Path to a config file, " + f"got {data}" + ) + + return IterableDataset.from_generator( + lambda: SyntheticTextGenerator( + config=data, processor=processor_factory(), random_seed=random_seed + ), + features=Features( + { + "prompt": Value("string"), + "prompt_tokens_count": Value("int32"), + "output_tokens_count": Value("int32"), + } + ), + ) + + def _load_config_file(self, data: Any) -> SyntheticTextDatasetConfig | None: + if (not isinstance(data, str) and not isinstance(data, Path)) or ( + not Path(data).is_file() + ): + return None + + data_path = Path(data) if isinstance(data, str) else data + error = None + + if Path(data).is_file() and data_path.suffix.lower() == ".json": + try: + return SyntheticTextDatasetConfig.model_validate_json( + data_path.read_text() + ) + except Exception as err: # noqa: BLE001 + error = err + + if Path(data).is_file() and data_path.suffix.lower() in { + ".yaml", + ".yml", + ".config", + }: + try: + return SyntheticTextDatasetConfig.model_validate( + yaml.safe_load(data_path.read_text()) + ) + except Exception as err: # noqa: BLE001 + error = err + + err_message = ( + f"Unsupported file {data_path} for " + f"SyntheticTextDatasetDeserializer, expected .json, " + f".yaml, .yml, or .config" + ) + + if error is not None: + err_message += f" with error: {error}" + raise DataNotSupportedError(err_message) from error + raise DataNotSupportedError(err_message) + + def _load_config_str(self, data: str) -> SyntheticTextDatasetConfig | None: + if not isinstance(data, str): + return None + + data_str = data.strip() + error = None + + if (data_str.startswith("{") and data_str.endswith("}")) or ( + data_str.startswith("[") and data_str.endswith("]") + ): + try: + return SyntheticTextDatasetConfig.model_validate_json(data_str) + except Exception as err: # noqa: BLE001 + error = err + + if data_str.count("=") > 1: + # key=value pairs separated by commas + try: + config_dict = {} + items = data_str.split(",") + for item in items: + key, value = item.split("=") + config_dict[key.strip()] = ( + int(value.strip()) + if value.strip().isnumeric() + else value.strip() + ) + + return SyntheticTextDatasetConfig.model_validate(config_dict) + except Exception as err: # noqa: BLE001 + error = err + + err_message = ( + "Unsupported string data for SyntheticTextDatasetDeserializer, " + f"expected JSON or key-value pairs, got {data}" + ) + if error is not None: + err_message += f" with error: {error}" + raise DataNotSupportedError(err_message) from error + raise DataNotSupportedError(err_message) diff --git a/src/guidellm/data/formatters/__init__.py b/src/guidellm/data/formatters/__init__.py new file mode 100644 index 00000000..0a5ccbc9 --- /dev/null +++ b/src/guidellm/data/formatters/__init__.py @@ -0,0 +1,47 @@ +from .environment import JinjaEnvironmentMixin +from .filters import ( + JinjaFiltersRegistry, + download_audio, + download_image, + download_video, + encode_audio, + encode_image, + encode_image_base64, + encode_video, + encode_video_base64, + get_file_format, + is_url, + resize_image, +) +from .globals import JinjaGlobalsRegistry +from .objects import GenerativeRequestFormatter +from .templates import ( + DEFAULT_AUDIO_TRANSCRIPTIONS_TEMPLATE, + DEFAULT_AUDIO_TRANSLATIONS_TEMPLATE, + DEFAULT_CHAT_COMPLETIONS_TEMPLATE, + DEFAULT_TEXT_COMPLETIONS_TEMPLATE, + JinjaTemplatesRegistry, +) + +__all__ = [ + "DEFAULT_AUDIO_TRANSCRIPTIONS_TEMPLATE", + "DEFAULT_AUDIO_TRANSLATIONS_TEMPLATE", + "DEFAULT_CHAT_COMPLETIONS_TEMPLATE", + "DEFAULT_TEXT_COMPLETIONS_TEMPLATE", + "GenerativeRequestFormatter", + "JinjaEnvironmentMixin", + "JinjaFiltersRegistry", + "JinjaGlobalsRegistry", + "JinjaTemplatesRegistry", + "download_audio", + "download_image", + "download_video", + "encode_audio", + "encode_image", + "encode_image_base64", + "encode_video", + "encode_video_base64", + "get_file_format", + "is_url", + "resize_image", +] diff --git a/src/guidellm/data/formatters/environment.py b/src/guidellm/data/formatters/environment.py new file mode 100644 index 00000000..bd37e26b --- /dev/null +++ b/src/guidellm/data/formatters/environment.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +from typing import Any, ClassVar + +from jinja2 import Template +from jinja2.nativetypes import NativeEnvironment, NativeTemplate + +from guidellm.data.formatters.filters import JinjaFiltersRegistry +from guidellm.data.formatters.globals import JinjaGlobalsRegistry +from guidellm.data.formatters.templates import JinjaTemplatesRegistry + +__all__ = ["JinjaEnvironmentMixin"] + + +class JinjaEnvironmentMixin: + jinja_environment: ClassVar[NativeEnvironment | None] = None + + @classmethod + def create_environment(cls, **env_kwargs: Any) -> NativeEnvironment: + if "autoescape" not in env_kwargs: + env_kwargs["autoescape"] = False + + extensions = env_kwargs.pop("extensions", []) + extensions = set(extensions) | {"jinja2.ext.do"} + + env = NativeEnvironment(extensions=list(extensions), **env_kwargs) # noqa: S701 + + # Attach registered filters + filters_registry = JinjaFiltersRegistry.registry # type: ignore[misc] + if filters_registry: + for name, func in filters_registry.items(): + env.filters[name] = func + + # Attach registered globals + globals_registry = JinjaGlobalsRegistry.registry # type: ignore[misc] + if globals_registry: + for name, value in globals_registry.items(): + env.globals[name] = value + + cls.jinja_environment = env + return env + + @classmethod + def get_environment(cls) -> NativeEnvironment: + if cls.jinja_environment is None: + raise ValueError( + "Jinja environment is not initialized. Call create_environment first." + ) + return cls.jinja_environment + + @classmethod + def template_from_source(cls, source: str | Template) -> NativeTemplate: + if isinstance(source, Template): + return source + env = cls.get_environment() + return env.from_string(source) + + @classmethod + def template_from_registry(cls, name: str) -> NativeTemplate: + template = JinjaTemplatesRegistry.get_registered_object(name) + if template is None: + raise ValueError(f"Template '{name}' not found in registry.") + return cls.template_from_source(template) diff --git a/src/guidellm/data/formatters/filters.py b/src/guidellm/data/formatters/filters.py new file mode 100644 index 00000000..8dd4e445 --- /dev/null +++ b/src/guidellm/data/formatters/filters.py @@ -0,0 +1,324 @@ +from __future__ import annotations + +import base64 +import io +from pathlib import Path +from typing import Any, Callable, Literal + +import datasets +import httpx +import librosa +import numpy as np +import soundfile +from PIL import Image as PILImage + +from guidellm.utils import RegistryMixin + +__all__ = [ + "JinjaFiltersRegistry", + "download_audio", + "download_image", + "download_video", + "encode_audio", + "encode_image", + "encode_image_base64", + "encode_video", + "encode_video_base64", + "get_file_format", + "is_url", + "resize_image", +] + + +class JinjaFiltersRegistry(RegistryMixin[Callable[..., Any]]): + pass + + +@JinjaFiltersRegistry.register("is_url") +def is_url(text: Any) -> bool: + return isinstance(text, str) and text.startswith(("http://", "https://")) + + +@JinjaFiltersRegistry.register("encode_image") +def encode_image( + image: bytes | str | Path | np.ndarray | PILImage.Image | datasets.Image, + max_size: int | None = None, + max_width: int | None = None, + max_height: int | None = None, + encode_type: Literal["base64", "url"] | None = None, +) -> str: + """ + Input image types: + - bytes: raw image bytes, decoded with Pillow + - str: file path on disk, url, or already base64 encoded image string + - pathlib.Path: file path on disk + - np.ndarray: image array, decoded with Pillow + - PIL.Image.Image: Pillow image + - datasets.Image: HuggingFace datasets Image object + + max_size: maximum size of the longest edge of the image + max_width: maximum width of the image + max_height: maximum height of the image + + encode_type: None to return the supported format + (url for url, base64 string for others) + "base64" to return base64 encoded string (or download URL and encode) + "url" to return url (only if input is url, otherwise fails) + + Returns a str of either: + - image url + - "data:image/{type};base64, {data}" string + """ + url = is_url(image) + + if ( + url + and (encode_type is None or encode_type == "url") + and (max_size is not None or max_width is not None or max_height is not None) + ): + raise ValueError("Cannot resize image when encode_type is 'url'") + elif url and (encode_type is None or encode_type == "url"): + return image + elif url and encode_type == "base64": + raise ValueError(f"Cannot convert non-url image to URL {image}") + + return encode_image_base64( + image=image, + max_size=max_size, + max_width=max_width, + max_height=max_height, + ) + + +@JinjaFiltersRegistry.register("encode_image_base64") +def encode_image_base64( + image: bytes | str | Path | np.ndarray | PILImage.Image, + width: int | None = None, + height: int | None = None, + max_width: int | None = None, + max_height: int | None = None, + max_size: int | None = None, +) -> str: + if ( + isinstance(image, str) + and image.startswith("data:image/") + and ";base64," in image + ): + return image + + if is_url(image): + image = download_image(image) + + if isinstance(image, bytes): + image = PILImage.open(io.BytesIO(image)) + elif isinstance(image, (str, Path)): + image = PILImage.open(image) + elif isinstance(image, np.ndarray): + image = PILImage.fromarray(image) + elif not isinstance(image, PILImage.Image): + raise ValueError(f"Unsupported image type: {type(image)}") + + image = resize_image( + image, + width=width, + height=height, + max_width=max_width, + max_height=max_height, + max_size=max_size, + ) + if image.mode != "RGB": + image = image.convert("RGB") + + buffer = io.BytesIO() + image.save(buffer, format="JPEG") + image_bytes = buffer.getvalue() + image_base64 = base64.b64encode(image_bytes).decode("utf-8") + + return f"data:image/jpeg;base64,{image_base64}" + + +@JinjaFiltersRegistry.register("resize_image") +def resize_image( + image: PILImage.Image, + width: int | None = None, + height: int | None = None, + max_width: int | None = None, + max_height: int | None = None, + max_size: int | None = None, +) -> PILImage.Image: + if not isinstance(image, PILImage.Image): + raise ValueError(f"Unsupported image type: {type(image)}") + + if width is not None and height is not None: + return image.resize((width, height), PILImage.Resampling.BILINEAR) + + orig_w, orig_h = image.size + aspect = orig_w / orig_h + + if width is not None: + target_w = width + target_h = round(width / aspect) + elif height is not None: + target_h = height + target_w = round(height * aspect) + else: + target_w, target_h = orig_w, orig_h + + # Normalize max_size → max_width/max_height + if max_size is not None: + max_width = max_width or max_size + max_height = max_height or max_size + + # Apply max constraints (preserve aspect ratio) + if max_width or max_height: + scale_w = max_width / target_w if max_width else 1.0 + scale_h = max_height / target_h if max_height else 1.0 + scale = min(scale_w, scale_h, 1.0) # never upscale + target_w = round(target_w * scale) + target_h = round(target_h * scale) + + if (target_w, target_h) != (orig_w, orig_h): + image = image.resize((target_w, target_h), PILImage.Resampling.BILINEAR) + + return image + + +@JinjaFiltersRegistry.register("download_image") +def download_image(url: str) -> bytes: + response = httpx.get(url) + response.raise_for_status() + return response.content + + +@JinjaFiltersRegistry.register("encode_video") +def encode_video( + video: bytes | str | Path | datasets.Video, + encode_type: Literal["base64", "url"] | None = None, +) -> str: + """ + Input video types: + - bytes: raw video bytes + - str: file path on disk, url, or already base64 encoded video string + - pathlib.Path: file path on disk + - datasets.Video: HuggingFace datasets Video object + + encode_type: None to return the supported format + (url for url, base64 string for others) + "base64" to return base64 encoded string (or download URL and encode) + "url" to return url (only if input is url, otherwise fails) + + Returns a str of either: + - video url + - "data:video/{type};base64, {data}" string + """ + url = is_url(video) + + if url and (encode_type is None or encode_type == "url"): + return video + elif url and encode_type == "base64": + raise ValueError(f"Cannot encode URL video {video}") + + return encode_video_base64(video=video) + + +@JinjaFiltersRegistry.register("encode_video_base64") +def encode_video_base64(video: bytes | str | Path) -> str: + if ( + isinstance(video, str) + and video.startswith("data:video/") + and ";base64," in video + ): + return video + + video_format = "unknown" + + if is_url(video): + video, video_format = download_video(video) + + if isinstance(video, (str, Path)): + path = Path(video) + video = path.read_bytes() + video_format = get_file_format(path) + elif not isinstance(video, bytes): + raise ValueError(f"Unsupported video type: {type(video)}") + + video_base64 = base64.b64encode(video).decode("utf-8") + return f"data:video/{video_format};base64,{video_base64}" + + +@JinjaFiltersRegistry.register("download_video") +def download_video(url: str) -> tuple[bytes, str]: + response = httpx.get(url) + response.raise_for_status() + return response.content, get_file_format(url) + + +@JinjaFiltersRegistry.register("encode_audio") +def encode_audio( + audio: bytes | str | Path | dict | np.ndarray, + sample_rate: int | None = None, + max_duration: float | None = None, +) -> dict[str, str]: + """ + Input audio types: + - bytes: raw audio bytes + - str: file path on disk or URL + - pathlib.Path: file path on disk + - dict: {"data": base64_string, "format": "wav"} format + - numpy.ndarray: audio array, assumed to be at sample_rate if provided + + sample_rate: sample rate of the input audio if input is np.ndarray + target_sample_rate: resample to this rate if provided + duration: limit audio to this duration in seconds if provided + + Returns dict with format: + { + "data": base64_encoded_audio_bytes, + "format": "wav" + } + """ + if is_url(audio): + audio, _ = download_audio(audio) + + if isinstance(audio, dict): + if "data" not in audio: + raise ValueError("Audio dict must contain 'data' key") + audio = base64.b64decode(audio["data"]) + + if isinstance(audio, bytes): + audio_data, sample_rate = librosa.load(io.BytesIO(audio), sr=sample_rate) + elif isinstance(audio, (str, Path)): + audio_data, sample_rate = librosa.load(str(audio), sr=sample_rate) + elif isinstance(audio, np.ndarray): + if sample_rate is None: + raise ValueError("sample_rate must be provided for numpy arrays") + audio_data = audio + else: + raise ValueError(f"Unsupported audio type: {type(audio)}") + + if max_duration is not None: + max_samples = int(max_duration * sample_rate) + if len(audio_data) > max_samples: + audio_data = audio_data[:max_samples] + + buffer = io.BytesIO() + soundfile.write(buffer, audio_data, sample_rate, format="WAV", subtype="PCM_16") + + return {"data": buffer.getvalue(), "format": "wav"} + + +@JinjaFiltersRegistry.register("download_audio") +def download_audio(url: str) -> tuple[bytes, str]: + """Download audio from URL and return bytes with format.""" + response = httpx.get(url) + response.raise_for_status() + content = response.content + audio_format = get_file_format(url) + return content, audio_format + + +@JinjaFiltersRegistry.register("get_file_format") +def get_file_format(path: Path | str) -> str: + """Get file format from path extension.""" + suffix = Path(path).suffix.lower() + return suffix[1:] if suffix.startswith(".") else "unknown" diff --git a/src/guidellm/data/formatters/globals.py b/src/guidellm/data/formatters/globals.py new file mode 100644 index 00000000..6c066191 --- /dev/null +++ b/src/guidellm/data/formatters/globals.py @@ -0,0 +1,9 @@ +from typing import Any + +from guidellm.utils import RegistryMixin + +__all__ = ["JinjaGlobalsRegistry"] + + +class JinjaGlobalsRegistry(RegistryMixin[Any]): + pass diff --git a/src/guidellm/data/formatters/objects.py b/src/guidellm/data/formatters/objects.py new file mode 100644 index 00000000..3e032089 --- /dev/null +++ b/src/guidellm/data/formatters/objects.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +from typing import Any, Literal + +from datasets import Dataset, IterableDataset +from jinja2 import Template + +from guidellm.data.formatters import JinjaEnvironmentMixin +from guidellm.data.objects import ( + GenerationRequest, + GenerationRequestArguments, + GenerativeDatasetArgs, + GenerativeRequestType, +) +from guidellm.data.preprocessors.objects import DatasetPreprocessor + +__all__ = ["GenerativeRequestFormatter"] + + +class GenerativeRequestFormatter(DatasetPreprocessor, JinjaEnvironmentMixin): + def __init__( + self, + request_type: GenerativeRequestType | str = "text_completions", + request_template: str | Template | None = None, + request_extras: dict[str, Any] | GenerationRequestArguments | None = None, + request_defaults: dict[str, Any] | GenerationRequestArguments | None = None, + environment_extras: dict[str, Any] | None = None, + ): + self.datasets: list[Dataset | IterableDataset] | None = None + self.data_args: list[GenerativeDatasetArgs] | None = None + + self.request_type = request_type + self.request_template = request_template + self.request_extras = request_extras or {} + self.request_defaults = request_defaults or { + "stream": True, + "json_body": { + "stream": True, + "stream_options": { + "include_usage": True, + }, + }, + } + self.environment_extras = environment_extras or {} + self.jinja_template: Template | None = None + + def init_data( + self, + datasets: list[Dataset | IterableDataset], + data_args: list[GenerativeDatasetArgs], + ): + self.datasets = datasets + self.data_args = data_args + + self.create_environment(**self.environment_extras) + self.jinja_template = ( + self.template_from_source(self.request_template) + if self.request_template + else self.template_from_registry(self.request_type) + ) + + def __call__( + self, item: dict[str, Any] + ) -> dict[Literal["request"], GenerationRequest]: + if self.jinja_template is None: + raise ValueError("GenerativeRequestCreator not initialized with data.") + + stats = {} + if "prompt_tokens_count" in item: + count = item["prompt_tokens_count"][0] + stats["prompt_tokens"] = count + item["prompt_tokens_count"] = count + if "output_tokens_count" in item: + count = item["output_tokens_count"][0] + stats["output_tokens"] = count + item["output_tokens_count"] = count + + return { + "request": { + "request_type": self.request_type, + "arguments": GenerationRequestArguments.model_combine_dict( + self.request_defaults, + self.request_extras, + self.jinja_template.render( + **item, + request_defaults=self.request_defaults, + request_extras=self.request_extras, + ), + ), + "stats": stats, + } + } diff --git a/src/guidellm/data/formatters/templates.py b/src/guidellm/data/formatters/templates.py new file mode 100644 index 00000000..2cf6e2f3 --- /dev/null +++ b/src/guidellm/data/formatters/templates.py @@ -0,0 +1,182 @@ +import textwrap +from typing import Union + +from jinja2 import Template + +from guidellm.utils import RegistryMixin + +__all__ = [ + "DEFAULT_AUDIO_TRANSCRIPTIONS_TEMPLATE", + "DEFAULT_AUDIO_TRANSLATIONS_TEMPLATE", + "DEFAULT_CHAT_COMPLETIONS_TEMPLATE", + "DEFAULT_TEXT_COMPLETIONS_TEMPLATE", + "JinjaTemplatesRegistry", +] + + +class JinjaTemplatesRegistry(RegistryMixin[Union[Template, str]]): + pass + + +DEFAULT_TEXT_COMPLETIONS_TEMPLATE = JinjaTemplatesRegistry.register("text_completions")( + textwrap.dedent(""" + {% set obj = { + "json_body": { + "prompt": ( + text_column[0] + if text_column and text_column|length == 1 + else text_column + ) + } + } %} + + {% if output_tokens_count is defined and output_tokens_count is not none %} + {% do obj["json_body"].update({ + "max_tokens": output_tokens_count, + "max_completion_tokens": output_tokens_count, + "stop": None, + "ignore_eos": True + }) %} + {% elif max_tokens is defined and max_tokens is not none %} + {% do obj["json_body"].update({"max_tokens": max_tokens}) %} + {% elif max_completion_tokens is defined and max_completion_tokens is not none %} + {% do obj["json_body"].update({"max_completion_tokens": max_completion_tokens}) %} + {% endif %} + + {{ obj }} + """).strip() # noqa: E501 +) + +DEFAULT_CHAT_COMPLETIONS_TEMPLATE = JinjaTemplatesRegistry.register("chat_completions")( + textwrap.dedent(""" + {% set obj = { + "json_body": { + "messages": [ + { + "role": "user", + "content": [] + } + ] + } + } %} + + {%- for item in text_column or [] %} + {% do obj["json_body"].messages[0].content.append({"type": "text", "text": item}) %} + {%- endfor %} + + {%- for item in image_column or [] %} + {% do obj["json_body"].messages[0].content.append({ + "type": "image_url", + "image_url": encode_image( + item, + max_size=max_size|default(None), + max_width=max_width|default(None), + max_height=max_height|default(None), + encode_type=image_encode_type|default(encode_type|default(None)) + ) + }) %} + {%- endfor %} + + {%- for item in video_column or [] %} + {% do obj["json_body"].messages[0].content.append({ + "type": "video_url", + "video_url": encode_video( + item, + encode_type=video_encode_type|default(encode_type|default(None)) + ) + }) %} + {%- endfor %} + + {%- for item in audio_column or [] %} + {%- set audio_type, audio_val = encode_audio( + item, + sample_rate=sample_rate|default(None), + max_duration=max_duration|default(None), + encode_type=audio_encode_type|default(encode_type|default(None)) + ) -%} + {% do content_list.append({"type": audio_type, audio_type: audio_val}) %} + {%- endfor %} + + {% if output_tokens_count is defined and output_tokens_count is not none %} + {% do obj["json_body"].update({ + "max_completion_tokens": output_tokens_count, + "stop": None, + "ignore_eos": True + }) %} + {% elif max_tokens is defined and max_tokens is not none %} + {% do obj["json_body"].update({"max_completion_tokens": max_tokens}) %} + {% elif max_completion_tokens is defined and max_completion_tokens is not none %} + {% do obj["json_body"].update({"max_completion_tokens": max_completion_tokens}) %} + {% endif %} + + {{ obj }} + """).strip() # noqa: E501 +) + +DEFAULT_AUDIO_TRANSCRIPTIONS_TEMPLATE = JinjaTemplatesRegistry.register( + "audio_transcriptions" +)( + textwrap.dedent(""" + { + {%- if output_tokens_count_column is defined and output_tokens_count_column is not none -%} + "max_tokens": {{ output_tokens_count_column }}, + "max_completion_tokens": {{ output_tokens_count_column }}, + "stop": None, + "ignore_eos": True, + {%- else -%} + {%- if max_tokens is defined and max_tokens is not none -%} + "max_tokens": {{ max_tokens }}, + {%- endif -%} + {%- if max_completion_tokens is defined and max_completion_tokens is not none -%} + "max_completion_tokens": {{ max_completion_tokens }}, + {%- endif -%} + {%- endif -%} + "files": { + "file": {{ encode_audio_file( + audio_column[0], + encode_type=audio_encode_type|default(encode_type|default(None)) + ) }} + } + {%- if text_column and text_column|length > 0 -%} + , + "json": { + "prompt": {{ text_column[0] }} + } + {%- endif -%} + } + """).strip() # noqa: E501 +) + +DEFAULT_AUDIO_TRANSLATIONS_TEMPLATE = JinjaTemplatesRegistry.register( + "audio_translations" +)( + textwrap.dedent(""" + { + {%- if output_tokens_count_column is defined and output_tokens_count_column is not none -%} + "max_tokens": {{ output_tokens_count_column }}, + "max_completion_tokens": {{ output_tokens_count_column }}, + "stop": None, + "ignore_eos": True, + {%- else -%} + {%- if max_tokens is defined and max_tokens is not none -%} + "max_tokens": {{ max_tokens }}, + {%- endif -%} + {%- if max_completion_tokens is defined and max_completion_tokens is not none -%} + "max_completion_tokens": {{ max_completion_tokens }}, + {%- endif -%} + {%- endif -%} + "files": { + "file": {{ encode_audio_file( + audio_column[0], + encode_type=audio_encode_type|default(encode_type|default(None)) + ) }} + } + {%- if text_column and text_column|length > 0 -%} + , + "json": { + "prompt": {{ text_column[0] }} + } + {%- endif -%} + } + """).strip() # noqa: E501 +) diff --git a/src/guidellm/data/loaders.py b/src/guidellm/data/loaders.py new file mode 100644 index 00000000..ebecdb6f --- /dev/null +++ b/src/guidellm/data/loaders.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +from collections.abc import Callable +from typing import Any, Literal + +from datasets import Dataset, IterableDataset +from torch.utils.data import DataLoader, Sampler +from transformers import PreTrainedTokenizerBase + +from guidellm.data.datasets import GenerativeRequestsDataset +from guidellm.data.formatters import GenerativeRequestFormatter +from guidellm.data.objects import GenerationRequest, GenerativeDatasetArgs +from guidellm.data.preprocessors import ( + DatasetPreprocessor, + GenerativeColumnMapper, +) + +__all__ = ["GenerativeDataLoader", "GenerativeRequestCollator"] + + +class GenerativeRequestCollator: + def __call__( + self, batch: list[dict[Literal["request"], dict[str, Any]]] + ) -> GenerationRequest: + if len(batch) != 1: + raise NotImplementedError( + f"Batch size greater than 1 is not currently supported. " + f"Got batch size: {len(batch)}" + ) + + return GenerationRequest.model_validate(batch[0]["request"]) + + +class GenerativeDataLoader(DataLoader[GenerationRequest]): + def __init__( + self, + data: list[Any], + data_args: list[GenerativeDatasetArgs] | None, + data_samples: int, + processor_factory: Callable[[], PreTrainedTokenizerBase], + column_mapper: GenerativeColumnMapper, + preprocessors: list[DatasetPreprocessor], + request_formatter: GenerativeRequestFormatter, + sampler: Sampler[int] | Literal["shuffle"] | None = None, + collate_fn: GenerativeRequestCollator | None = None, + num_workers: int | None = None, + random_seed: int = 42, + **kwargs: Any, + ): + dataset = GenerativeRequestsDataset.build( + data=data, + data_args=data_args, + data_samples=data_samples, + processor_factory=processor_factory, + column_mapper=column_mapper, + request_formatter=request_formatter, + preprocessors=preprocessors, + random_seed=random_seed, + ) + + if collate_fn is None: + collate_fn = GenerativeRequestCollator() + + # Handle sampler/shuffle logic based on dataset type + if sampler == "shuffle": + shuffle = True + sampler = None + elif isinstance(sampler, str) and sampler != "shuffle": + raise ValueError( + f"Invalid string sampler: {sampler}. " + f"Only 'shuffle' is supported as a string value." + ) + else: + shuffle = False + + if isinstance(dataset, IterableDataset) and sampler is not None: + raise ValueError( + "Samplers are not supported with IterableDataset. " + "Use shuffle=True or apply shuffling to the dataset directly." + ) + elif isinstance(dataset, Dataset) and shuffle: + dataset = dataset.shuffle(seed=random_seed) + shuffle = False + + super().__init__( + dataset=dataset, + batch_size=1, + shuffle=shuffle, + sampler=sampler, + collate_fn=collate_fn, + num_workers=num_workers or 0, + **kwargs, + ) diff --git a/src/guidellm/data/objects.py b/src/guidellm/data/objects.py new file mode 100644 index 00000000..04c5407d --- /dev/null +++ b/src/guidellm/data/objects.py @@ -0,0 +1,230 @@ +from __future__ import annotations + +import uuid +from typing import Any, Literal, get_args + +from pydantic import Field + +from guidellm.scheduler import ( + MeasuredRequestTimings, + SchedulerMessagingPydanticRegistry, +) +from guidellm.utils import StandardBaseDict, StandardBaseModel + +__all__ = [ + "GenerationRequest", + "GenerationRequestArguments", + "GenerationRequestTimings", + "GenerativeDatasetArgs", + "GenerativeDatasetColumnType", + "GenerativeRequestType", +] + + +GenerativeRequestType = Literal[ + "text_completions", + "chat_completions", + "audio_transcriptions", + "audio_translations", +] + +GenerativeDatasetColumnType = Literal[ + "prompt_tokens_count_column", + "output_tokens_count_column", + "text_column", + "image_column", + "video_column", + "audio_column", +] + + +class GenerationRequestArguments(StandardBaseDict): + @classmethod + def model_combine_dict( # noqa: C901, PLR0912 + cls, *arguments: GenerationRequestArguments | dict[str, Any] + ) -> dict[str, Any]: + combined = {} + + for args in arguments: + if ( + url := args.get("url") if isinstance(args, dict) else args.url + ) is not None: + combined["url"] = url + + if ( + path := args.get("path") if isinstance(args, dict) else args.path + ) is not None: + combined["path"] = path + + if ( + method := args.get("method") if isinstance(args, dict) else args.method + ) is not None: + combined["method"] = method + + if ( + stream := args.get("stream") if isinstance(args, dict) else args.stream + ) is not None: + combined["stream"] = stream + + if ( + content_body := ( + args.get("content_body") + if isinstance(args, dict) + else args.content_body + ) + ) is not None: + combined["content_body"] = content_body + + if ( + json_body := ( + args.get("json_body") if isinstance(args, dict) else args.json_body + ) + ) is not None: + if "json_body" not in combined: + combined["json_body"] = {} + combined["json_body"].update(json_body) + + if ( + files := args.get("files") if isinstance(args, dict) else args.files + ) is not None: + if "files" not in combined: + combined["files"] = {} + combined["files"].update(files) + + if ( + params := args.get("params") if isinstance(args, dict) else args.params + ) is not None: + if "params" not in combined: + combined["params"] = {} + combined["params"].update(params) + + if ( + headers := ( + args.get("headers") if isinstance(args, dict) else args.headers + ) + ) is not None: + if "headers" not in combined: + combined["headers"] = {} + combined["headers"].update(headers) + + return combined + + url: str | None = Field( + default=None, + description="The URL endpoint to which the request will be sent.", + ) + path: str | None = Field( + default=None, + description="The path to append to the base URL for the request.", + ) + method: str | None = Field( + default=None, + description="The HTTP method to use for the request (e.g., 'POST', 'GET').", + ) + stream: bool | None = Field( + default=None, + description="Whether to stream the response, if applicable.", + ) + content_body: Any | None = Field( + default=None, + description="Raw content to send in the request body, if applicable.", + ) + json_body: dict[str, Any] | None = Field( + default=None, + description="JSON content to include in the request body, if applicable.", + ) + files: dict[str, Any] | None = Field( + default=None, + description="Files to include in the request, if applicable.", + ) + params: dict[str, Any] | None = Field( + default=None, + description="Query parameters to include in the request URL, if applicable.", + ) + headers: dict[str, str] | None = Field( + default=None, + description="HTTP headers to include in the request, if applicable.", + ) + + +@SchedulerMessagingPydanticRegistry.register() +class GenerationRequest(StandardBaseModel): + """Request model for backend generation operations.""" + + request_id: str = Field( + default_factory=lambda: str(uuid.uuid4()), + description="Unique identifier for the request.", + ) + request_type: GenerativeRequestType | str = Field( + description=( + "Type of request. If url is not provided in arguments, " + "this will be used to determine the request url." + ), + ) + arguments: GenerationRequestArguments = Field( + description=( + "Payload for the request, structured as a dictionary of arguments to pass " + "to the respective backend method. For example, can contain " + "'json', 'headers', 'files', etc." + ) + ) + stats: dict[Literal["prompt_tokens", "output_tokens"], int] = Field( + default_factory=dict, + description="Request statistics including prompt and output token counts.", + ) + + +@SchedulerMessagingPydanticRegistry.register() +@MeasuredRequestTimings.register("generation_request_timings") +class GenerationRequestTimings(MeasuredRequestTimings): + """Timing model for tracking generation request lifecycle events.""" + + timings_type: Literal["generation_request_timings"] = "generation_request_timings" + first_iteration: float | None = Field( + default=None, + description="Unix timestamp when the first generation iteration began.", + ) + last_iteration: float | None = Field( + default=None, + description="Unix timestamp when the last generation iteration completed.", + ) + + +class GenerativeDatasetArgs(StandardBaseDict): + type_: str | None = None + split: str | None = None + prompt_tokens_count_column: str | None = None + output_tokens_count_column: str | None = None + text_column: str | list[str] | None = None + image_column: str | list[str] | None = None + video_column: str | list[str] | None = None + audio_column: str | list[str] | None = None + + def to_kwargs(self) -> dict[str, Any]: + return { + key: value + for key, value in self.model_extra.items() + if not key.endswith("_column") + } + + def get_mapped_columns( + self, + ) -> dict[GenerativeDatasetColumnType | str, str | list[str]]: + column_mapping: dict[GenerativeDatasetColumnType | str, list[str] | None] = {} + + # Add in any non None columns from the fields + for column in get_args(GenerativeDatasetColumnType): + value = getattr(self, column) + if value is not None: + column_mapping[column] = value + + # Enable flexibility for extra columns to be passed through and referenced later + for extra in self.model_extra: + if ( + extra.endswith("_column") + and extra not in column_mapping + and self.model_extra[extra] is not None + ): + column_mapping[extra] = self.model_extra[extra] + + return column_mapping diff --git a/src/guidellm/data/preprocessors/__init__.py b/src/guidellm/data/preprocessors/__init__.py new file mode 100644 index 00000000..039f74a5 --- /dev/null +++ b/src/guidellm/data/preprocessors/__init__.py @@ -0,0 +1,7 @@ +from .mappers import GenerativeColumnMapper +from .objects import DatasetPreprocessor + +__all__ = [ + "DatasetPreprocessor", + "GenerativeColumnMapper", +] diff --git a/src/guidellm/data/preprocessors/mappers.py b/src/guidellm/data/preprocessors/mappers.py new file mode 100644 index 00000000..1792cb7e --- /dev/null +++ b/src/guidellm/data/preprocessors/mappers.py @@ -0,0 +1,115 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Literal + +from datasets import Dataset, IterableDataset + +from guidellm.data.objects import ( + GenerativeDatasetArgs, + GenerativeDatasetColumnType, +) +from guidellm.data.preprocessors.objects import DatasetPreprocessor +from guidellm.data.utils import DEFAULT_COLUMN_NAMES + +__all__ = ["ColumnMapping", "GenerativeColumnMapper"] + + +@dataclass +class ColumnMapping: + indices: list[int] + names: list[str] + + +class GenerativeColumnMapper(DatasetPreprocessor): + def __init__(self): + self.datasets: list[Dataset | IterableDataset] | None = None + self.data_args: list[GenerativeDatasetArgs] | None = None + self.column_mappings: ( + dict[GenerativeDatasetColumnType, ColumnMapping | None] | None + ) = None + + def __call__( + self, row: dict[Literal["items"], tuple[dict[str, Any]]] + ) -> dict[str, Any]: + if ( + self.datasets is None + or self.data_args is None + or self.column_mapping is None + ): + raise ValueError("GenerativeColumnMapper not initialized with data.") + + mapped: dict[GenerativeDatasetColumnType, list[Any]] = {} + items = row.pop("items") + + for column_type, column_mapping in self.column_mapping.items(): + mapped[column_type] = [ + items[index].get(name) + for index, name in zip(column_mapping.indices, column_mapping.names) + ] + + return mapped + + def init_data( + self, + datasets: list[Dataset | IterableDataset], + data_args: list[GenerativeDatasetArgs], + ): + self.datasets = datasets + self.data_args = data_args + self.column_mapping = self.generate_column_mapping() + + def generate_column_mapping( + self, + ) -> dict[GenerativeDatasetColumnType, ColumnMapping]: + mappings: dict[GenerativeDatasetColumnType, ColumnMapping] = {} + # Map any columns specified in the GenerativeDatasetArgs first + self._fill_mappings_from_data_args(mappings) + # For standard column types not mapped, fill in first one found from defaults + self._fill_mappings_from_defaults(mappings) + + return mappings + + def _fill_mappings_from_data_args( + self, mappings: dict[GenerativeDatasetColumnType, ColumnMapping] + ): + for index, args in enumerate(self.data_args): + args_column_mappings = args.get_mapped_columns() + for column_type, column_name in args_column_mappings.items(): + if column_type not in mappings: + mappings[column_type] = ColumnMapping(indices=[], names=[]) + column_mapping = mappings[column_type] + + for name in ( + column_name if isinstance(column_name, list) else [column_name] + ): + if name not in self.datasets[index].column_names: + raise ValueError( + f"Column '{name}' not found in dataset columns: " + f"{self.datasets[index].column_names}" + ) + column_mapping.indices.append(index) + column_mapping.names.append(name) + + def _fill_mappings_from_defaults( + self, mappings: dict[GenerativeDatasetColumnType, ColumnMapping] + ): + for column_type, default_names in DEFAULT_COLUMN_NAMES.items(): + if column_type in mappings: + continue + + for index, dataset in enumerate(self.datasets): + for name in default_names: + if name in dataset.column_names: + mappings[column_type] = ColumnMapping( + indices=[index], names=[name] + ) + break + # Check for plural form of the name + if f"{name}s" in dataset.column_names: + mappings[column_type] = ColumnMapping( + indices=[index], names=[f"{name}s"] + ) + break + if column_type in mappings: + break diff --git a/src/guidellm/data/preprocessors/objects.py b/src/guidellm/data/preprocessors/objects.py new file mode 100644 index 00000000..831f944d --- /dev/null +++ b/src/guidellm/data/preprocessors/objects.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +from typing import Any, Protocol, runtime_checkable + +from datasets import Dataset, IterableDataset + +from guidellm.data.objects import GenerativeDatasetArgs + +__all__ = ["DatasetPreprocessor"] + + +@runtime_checkable +class DatasetPreprocessor(Protocol): + def init_data( + self, + datasets: list[Dataset | IterableDataset], + data_args: list[GenerativeDatasetArgs], + ): ... + + def __call__(self, item: dict[str, Any]) -> dict[str, Any]: ... diff --git a/src/guidellm/data/prideandprejudice.txt.gz b/src/guidellm/data/prideandprejudice.txt.gz deleted file mode 100644 index 8c7a10727c239964fff2e203f07318b29108eb2c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 241795 zcmV(rK<>XEiwFoug!X3w|8R0?WMyG)WN>n2YIS63V`VOMcys`az1xl>Ns=Y_u3y1; zr#rB~KJ11+1hTTaJ7c%44sEk5_&49kX}c<+ww zV@f}b@2&mNrtg-;9)}+q>?hjIv2A<%iFq?lo42lgpYgT%x@uTPFhG&k$=G4vU z*iH5u?Xa|yeaSvDwB5^TH(sZcjRb?Xchj5Mf6U|Nz3uzWvHR8@Ff9Dwxf>2MMvp(t z*J&EB!vPOFHs`MI+Sy;-{?#6_ABVX+w3B_mnfaB;esDQn`*fMcOFJz$4tP9cj4j?` zv`>tzyCj*x#?_A2)v(ZLUr> z8NX_|TU!~MpY5g@`02EaHXNG{+f$pymIjY~#89e{oSQ>yKW*Qc-`hz8`2C2%@Ey5* zY!a|dHt(mAzqd56_FElaYqOv5BA762ufkQ@hzW_G$<2-f=UxE%(HwZ}zRe z^!(#^xp9wd{*V9b|NcKWyW8fU+HrbmH$U`!J3K!B+rOpsGY4+()D3Ul!XL0rJ=#Xm z;D0&UqMx->www{3ElkI@T7|i}&05TQCv-kl$$q-Qm+g}{Nwy>V1_O!ndYr~Hhd)O2&p={(x$`!E@rX}qEx!m}Ocbg*-d zgJ6H#;dn0jN1B(Zjlaeo`QtcE_7dIs+#b5dmhSdc9m#~{wD+rQk?lfzr*`wl&W6?Y z=VtcZ$WwXbHksS$%{GHsKKr0OV%H`&2&eICZ(@^$KeYBW`^M0ITg-N}ui6S+l7@y8 z&l+r2zQjNLjt26UKlpXVIM2<*9crgW7BqkU&!?^bRkg$%5Uf%AY4byEbMF`JNz*+} zW(wah-!^!>sx7oV=yCIltoE@R@Q*ZWFiG~6mu~9kPcaI!$2L7*?B(BWk#Vdx&GBf5 z0kd$!vw#h_4u>uM(#1#y=0O zo6vJTv)Tss!9zQDFT9fG)@~Gh1y(2z{P+C#FPlG^{ci35wwro*OVhvF06%TA(c9SN zbhF#()plEKw)AY5&$HbNW;^VM`q3two3t97HX06}wqQQ1Z{6H2wBpU2=Fv83!;P`G z`IyTq@zlT)?e`70GL{R!|A;w@himb87ru-S5B3rMj{VVI?vOAL-@xa&?cZ>Ry`Stb zZ0v$MH5V+By#p3y(SIyhzlyA&Vqc95*tv2XCpW{Dqd{r7fzHr-$o{tScs%=^fW#C*aTwWsac=DL_A z++COS)r>Chhl^cJIHfu=Hb`xAJFrtTPIi+wwve{Va28sas`n8G2{#CiOnRBjoIPAG zP{EQuHn`^MiZ-LbL-jsh`$O8bvOv{&-u%_v zqF-zyc4h(aYJY&w^M~uck##=U0#D|qwOr(vahuL|yzB>YPxIIId%M*ZZD2c@FYVAy zO|PlbC22o&^D}6tYiQpS<_Saps#(pmY|qzomV8RgnXtR1+w~gz^L2j4Ncgy9N7~$t zBOjnGnWyYub&#}q=KXZNTzj+6TN-wAOl+bL_;Aob2G}@~7ATZikK2bH0UF|A+4Hf3J=PjkMcD4xiJd^TI>>sBd6v;;=NE zUAMS#(l&)Tp*uZ)_Gz#k!}px!p5tq$riTN*vR(hJ8O-FHUlaeDFy@ET^=unizWg6< z=3G4T25p=bNuXL&Sc+-(OA`!>Ja&)8q?MwYFulWtQzwYJ^Pt*rNX zeP_0xaa(eO(&}PeJH}u3al5p}2~I1`t3nAb#}lle&)11=)wz}B2KH00yy)C zGjm&3omwcj|1Ru@)y z*7o|BM$Vc2pKX!LXp3RT^sT!6@P)Gt5#HRnxI>TOp*OZ=Ursi2^97#CadboEkJ(@m z5*E(Bh@j(WE|=|r{C$N=Jn41>=;mXQc!!5+qw@{RX%n!#+sAJ7q-F+Dtwm9Jv_G7- z)uE5rgr3ZNHQ#AknNAymg@{n33jR(ri9+qv?;e&1fOqpz27;9C^$6|=zm zh>nn4Z2s0ZELV9PHvHp#Wa@#!c)+`!yKl=?9su|B1|9{hXfxRrv2^>5&ha+c&cIgC zgzlTm)FJZVaOli`GykWL9Pz0*$HJ+u?fLJpTkc0azj7HF})DLx(3_dp_u3X12G@U>Bba3ZDr#0hYS8|1A%< zv$RcaPP@T&{Fxh6Z+IQtWwLnod}$|lab|z&p_^@2FF;2Q^l8$u8TPU{llg^59vs_J zJUM1h=8opVy_+7diRa+bZQ%e!(0sO)$5J82^;o0?Fex=aI(p88D6I|m;I~WcfqP) zHF|r)3_Kmjz8~N1*~ylE!0P>ZhwbY_G(%vkcgSAW#+JArZ~pzsqG2Ik_6}PFC-e1i zXyJx62eZ}ltYxNg+de$_4NM#K!PVg1f)l*Huz%6X#kBC6w^_^}%rh+A^XgG}%3Oh0 z@auVD>0Ax~$$EiTvM=xD`LTv8lgS7HPzh{Hb3Qla;s4Nge{ak)IBj)z!rovV8^SuR z2UCiov@PY?f_0c^B-;g;H;CuU7KG=-|uiRIV1`@LX*XBkC zWotI7f#1Bq1sGtfk{cfj*_7VM}+GPf2%PEyM>FgaunlT?e}?-9yf%c z+97pw_(9)ngW?P@820m=F_53f2iqp_+5sL7@YsqoV^6jVw*bBlsFp(Z|FKd;@ysfdX>Rq;SczC`TyzKvWZE#09kZD6t=Nm@; zwD}G)W}c<}_Ls6ZPUC>Fa=`m;+F#FfdTr-DZhj~W$o(F_)xfI483`aIIf{i7fFKo+6X$2Nz#djJEoIJ;o zXnyT#xBoFmw*9v68@YIOTBGGz|NWToRvCnm6EU6XvBgF%-XZf@%j5wd0iRw2PL5RK z^lGGE0KaK#n2WtQ2Ctoq-+bmOJUgBYyu$u=oBr5L76$&K;6@@H=Y}z_kOw3M9}BWZm?NByIQqz z+Swv5x4E@J0`{5@m-*x45TKyLOSV_cIish{9OApXC4Dq+XUb!NS&Flb99vaBa*LcFsM+~`#Rc{m^ zq<8GB$DBkV7Uo~Ks1*1Ti0C{fF1YzYTN!%U6mCxCO>77_L)eb>o|!qo8{9csgp@Hi_}DXo zE?E}EKY`)`O69i4X3a4RiB(2Zo&oX8q-J<%GWzx?B=5q;c1G zFY-Q`{l2|u9}1#*IC&&tXUaa5;YGNO8I;|olGOr~H6gAu-Bze!7;NAU`a5uSa+`u+rBLGB3i8&3jrB7vHrG*9b?i#vF^06&CpMrt)ej0lj zQMjo~Bv#AG+c=_PmncTjI3q!M-i>WWx@Fo%FfAi8>^M(B=*A16XUm+gUtSOnPZ0NfN`e+k<;yN4JI87C_w{ zfWU2?VNP>0E?s{Ut3dHU#jSzONq!-fuz)_DS#X_(95}nfpd^kN*1yT2!quG|P2JKL zzy}@i(Nx+4;S0fN$qaoRJnMG=uvr_;%+va9^D^Ul5JvRfnf#{W{Aumm z45L^6no>nOx;@J~ct>$uaUIX_l|ap~5hJOY4UVU3;!@E$S7`Gx0c$@vfkLs#a6RwD zr`5wfX2yxwk7rl`KQRS@%rAtQa$09vm^kSMyJ;F?T^jN>tkQ1MN^q*??pql!*H{y| zP1s4K-g$0aQ-&RA+wqa1RgMfa!?dItW8@}o4+>K{r`XI)e;xJPe0tL_;|ur7=M}`j zHAaTPe}1d8=8jl+sr`XYt$edFf*J8ZA5*$tMRFthaeR$Dp08RnAjk7LW~kXjRFTNP zX$QEK`WgHC8^M2)2wR{!Fo$q`5N7&Zan;C8d^cNevnhwdPH8s@J=%A0)sBswOdy~5 zvMmhW;Gr8H`sQ7D>`!JKktHH8C}HTn3G&GOYQbs|{^i$x-%R_P_VrFy3AHQOsOEtm0P6FPOv?a8wYB_;l*<44Y~P0IBUL9mVFO(}k6iw1ue z{k80<11GYKgU4MsgR-F@y)C?Se&0^J8}Adpjb<`X!hHC$*=&z9nJl~Rpk%)dF}vrR z59dmwxDM~2Xy1I?9PL!1f;nlA-yLn7S?+bR?QJOJ0Nf`k!cD!HmE3oircal4y!36P zt#KSa=k=xk?PLUJU)XdqYCmM`0Hdc%(F`xst?)5ul07MI_cO=k#vk;_P6WhaCO%bmLT{C5 zl*bH9&~2gp;a97F=*$_(hr)9azJe*|o#ZuxT1&GG+}z!C!U@Z701n+?6WYHP$D?3f zd}Yp=bBGyc*snx_)m{#soZ_|gQ_Wt{A(7S1p`~_%ofBRn+_0-ns#+jgIop=KXV$p* zObH=eI8-h*-8TYXOdL8#=*2eSWBLKqkXf(8UwZ3CVsQNTS&4lNY6WvBs<1(cC3mE$ z4CA_MaoFH{(igW^wV5T20?A&uO|ZR)$3apcFfD7J{E*HObpJ+%I+GE?dyRNKTKR83F6tR+}?p1VJ0JDYlnV6-k|F zh52L3f%~7uUW;lbR%alsOgdOOtaQmWA_C@UzZr?fRfRca&Z(Fo{N1l(2a=c~?;itt-+g#ps5Oj?+#L`wg*z%s*|6;z1)ajaNBj4|(b|m9yZ@(RvJQxrU2K7z zfPr>_+aO5jUx~Yhn@LCmob5wZbKrPcSZDU$a2Ci~M%<&*m*L9_8#%7H`;$~KU2Nq4;Vi1z7Zg0PQyGTUYk zQZ^}X0Nj;sree34d0})&6XS(?C$N8*0>J5x{GaB)iAN~|^Zd7L^3CXstSltE`nB5K zYbr-wKk$oChtc`BjS}2kr7P1%8&+N_dn30|*5y2MTbkoovA*COZp|bu`%|QG+na(n zEE2tWzvu#>=npv6wklf;$LUv#-7Z92KQlg1g4CDQZ70hG&zEKMd~Tng3h&$&d>E6c zr&!gCE|6R@k|7b{05ufG8Ryjc)q>hQomhL~O{@X^Dlt+Gey zjh(k#&Kp=RF>6dd%RQWy>(#Rr>TiKs5G+@z@a=Jxp{BR( z)v$5eGnZ4jO~gU)2<049KCRbGZSK8y_p9aRXYxn~6(9w#%0^mzpkv!F{0ZRB$~&6DdKC8}!@Ze; z>`Z+5!L`dA_0_O|z!5FR>GeLfxm}UowKabvT#TrewM0k;mYK;Yado;uji)umyuz38 z12)&udd{x_SSI!2*0LgC(vjceywPB>aD|8}Q(J+^*GwkH{x;yG4L&k7P;tm{I-L-r3TyYS!nqv-OBnB{){3%}s513F# zY!kzp1lv6Wm@pz;&`+InMq*M{zKSk97H2M>yUzs#hTuY3{7I+C{VPN<=3miGE2#&e zoYx_TEMlF@u7Svp(w{|UEBHI*?mmCNLe{Bk753-RntkovnY;QK?)Q=|D|m=w1%;NN zP4-o}pDyi-fsL|C|Xy@mZ8G*}v)| z&16=Y^LzK9?iioJ`z40`>yOOcxA+M8&E;R8e`kA|XZ-oW{$Gw*jycoVh@4Q7!CBC0 zj@=#UIS}+pM}lr+0Wg9}=WiZWhqzEbkrMe&;~}%rw{)V1F&F0%qx5UWPQyxqW4=tD zXT?oC>`Bm>}S415E$1GNswT)kG@YNNYY4wg|W#YvKP+qr;Ll$0Vr@I zmZ1>7e~j=gF+5WN5i+^i-QwG~J<@7RuIPj&3JX@|RY5G$Bg~<(zO0~ZoYNQ7Y{wG+ z)OrD^P<0#KbiI(GM@*@{^Dfhf*ggJ@nB6Vi(NlnkBlG>&VqLtNze^NNyZG12L=8sj4 z;a-@6a|IQLGXH&WLM!)~szx(?9{{Y6>_+rpzN&=l+149+2gAOg=qy?JfC0kfO_Q*e zt<2rRN><|3@|#r@GpdGDZ@Xb?w_CElkYd<(6%m`g ztSX=qRy>bhMU9K$6UZswi|-1{?9*@Gr#NOQ%X5eV*}XYp=Fp(1aqaQ|GtB)Q=Z%mN z{n=(v^u<~|8Sy%xa^6utGSlP~Z^lK`z3~z4amfO4Q9;e-Lci&b0^{jVFn=yy66V|# z9b#VbbZe-{qMf3c1QsDAZn~P6>jduymMfN2^&J@!l`p}_UYA;LJy?rCI_RSB!o|^%mybR z>wt)E!J=u@S8Z6v2kZ*}rOeYB15M!&f}_R}yv`wm@M5;g?xg8f>8&_hx~(wbZcb|; zEDH1p_>i65HY($$SIv7hm%-z&4uUhABy1WdmwDu>sM>-D8tByxU+OD-wGd!|`Q2QW zmCl9#a>?0w8ssry9DdqbCVm^x;f034|9pfZ)h1_g_;Wm8%z?%mCz09-x6S#+uS)Bc zadTQ_gVJo)y*Pi*-GUEjGHtHc5$;$wxlmbZZ&pOv4;$qOy`vp3I^T-?vE)_v3@555 z6S7xjqvRsQh*q%SA?KMiX1z2xlO7CXcTdlXWnC}GC7DKG)n?0WH<&Y^xm~4C9HGkVkK^7;sAbjmGo^G8V6=!H?o~k zfBxipNZW^pP{)Eh#n-VP{3QpJGf>YHv^DFX#>@D{;icsjf0U_Y^I`%w^|wkfVI zF<(&m$5YY0MyWvKC7MoGNvtmrD*Oe(MK|Sqe-sCkDd{IQFYk}}P})bf2&w2CYk!28 zhC|x!eaK4{9S0dCPk*%;*0;mDLZFBbOPC=;Zq7c@lz?eRSDw7P3LB=4cPWbjS4XZ+ z#tX6T)&YfZp;Jb(ys}hpTHJyjGp4*H^g&uu0!V#oq6%|(LtXXZvwPJg@o9FrrdWm0 zU$_!I$tUs-L~y7Khu%uwD|nVgRs;kkR;+8+QN#j&Z}$b!@f47~pR7=#gtW zxAuXqontO?y{Ov>8oQY7QsD}aCS%gb73W02)EwG7-cC0F&&FbBiP7Lpp1}{2K%vsk zcHa&bO&`+TdOGU}yp~4xE5eCdN8@sx%pOt4(Wy+^gl!s*XK2(MMgoTGLHc7lX%)SP zOPE-xNf(X0BRc@yJ`+($a0;ye5nyG{-tn?;WdF%f==PF0V*oyVw+qEp^41_;=CyZr zyY7geE}lUSg^`38r#>Bv@1#OfDWAaT(`u1Ft@9V8UlvncT*k>WUk4p|iV#v}WL+WP zW-+SKSwA7m>LxA4q(c?=o*OA|bNI$kFUrD#I~HqHk$P05gqd%t1@-=5>1Njsnh!Xkvs zK$$z$7v-+NV@A?T@44GQVQ@@M;H<blQTAmvWD}5Q~m(5k{(a7;Hu3G)(=RvY;u>;?|x6%x-pon5FH#bEoP< z>YH8S9P!H@c8&;XBW7YE(+Phb_GNZJR17?_HE>Rl&|l5MeI)a^Q|<4@I}_ZiFxZ=7T9vLR){}O86I}$J+BL|>>Yt0 zS~TE`Ed3xH_|@;sxbiE0HfD_L=2bwshG$)+GG9X*6eda`Hr*|kGK|H0s$W+SIkHPB z_@%Z}kS`GnKrcbz@|ar-z4 z%;?PaWyX`rvW{kOD{f^tgoI>Y8Cc-|%V*YYZO8g4=``n$4sxU6y@yKH1 zoQ7Z`6xsYjW=kuIFierxh|DE@AEl=?jwA2(tyMMBOWP-2136ddQ1&)*wK}L1>KlSa z0idgQ840X{`;=TGaT2HCB&#S!HD-#(CL_Xu8amCoYKU+(1Xj^x-sy4X_T7FWn^<7}O7$-sm^ zD3r@?f#nh(mcpQ;=X#^M9zet`G6Cmz=x1Tmxou*B)p$;I6*9GKndQieV4m4pzxXPB zl4>u6*Gl=Vr^-2Y*V*^p)eOnh{ax-cAkS1^$hCg1a;vion9`y8Ma2{i#?4yWo*&Fy z3vnt4hA}GeyVN14KJtp+CM$fn@ zY_#GNF^6RLx4)^yV!4j)mAX79Lf~>+r2H1q0g=^1*1Utd!e{~gq7o}73F$o97`CCQ zvGvjOK#Z^qwozUvS1VRi$$B)&i!tIt%|&=}Hu!gP87WP~l{izRt(jg~Dr-J#VwEfs zB~ih4-t0!8^FBM~XjAkeU(dc;tT%PH5O8pUR%Gua3%L*9lfuOwpR3J^py(22NgY8i%*k^nDYZ=MowWtr1dnbS72nO3+;zESL= zx(TModhuFBDtPN$?kLb;CX`MHGudJTxy!n2{?;lgmy8Kv%p z=4)qL=?Wj--XAG@Tj6X+LzJZwuFPfoQQBCtiz@RnwsBVhzgeV$&y_hX*`9R0wL?O=;SJQ+Z$6FQiLgLJ<(IR34l+Q&KTm zQB$Dy<9&LC%BvEJ{_1iPKv>&-0|Rr%ajB?Glru=Q4zw)Kb}FCw^SF$ZHq2?&c6xXS z+RxX(_d-(=Ee7!Ahlj78P$?=`A)@o}pajSX?7UkMa(j5lpH2EKX6(O@yKbn{OMh|Q zr2N93JBg~0!-ra^`Z1rw;y?u4jfXrC!uRz@B}}L69%cJbJ`p)E98|Z@ALKbJ&?U^9 z46%E7ZIuO6X#m}*ByhiG;VU6Y5B?dC3$B{;7vWvc%a@2;lB(pHrz+9$N=P<@N02uD zU)VO^C-QI8uDy+f_=!S&_My8@)=nUV$8~d7u_qDH7V1A@t)e&rB1)e;9jWsFNF{rv z*4CM1E|o=Mhws1C9l2vFPFlB6XNk3Uqw>w3)+vd^QUmeF+jHI=Byw{B1pjJkF4qZ1 zMOY3KXY=z`IreM;QZ@=a*M(P}6^axniY}s5heJ-yTt$UjP?S#Xm(5=wvc%u`LC>h%F^h6&eD3<>{SbU&{Jvu`~t*mTNO6gG@!;XhQwWd*TF zfJ@H+F3KE$uzp8pg1>WqtYi|SaT50)eZ9;`g@y+UCxg&Y0ZZ_R3YVGnh=5YU3+%f~ z$)f$>qGHZ2q?F^iv59ETUp8N7KYcoK9#?r*>q|E7Fqw}Zs;8Rwy23h25;fYD>S!3) zIdd;vyO*!MG8-Ro)$FPTS71{Paj-EJTB*V;E(M2GQH%=Z2v%e&ykEv5j$fmqz82fn zTNg&vDJbN8Q>WFdI4pVfLRWbNcd~5EIK%&V_B{n;d3tSjwN%ei#yM*iT*a65OvQoRc=WCE9ss9`OD5uilgD@ z)z?`6d}85Ium@0cY+21lQESPrad$qixo#_g>!0YxW2t`Y<@0hdC~&Gz5VRWqc2b8M ze6}3zT$v_SJ^YnAtgJL?MZnM|n8%dUvo$r#y+_4=+Y%*nE6P!FpiEj?+vJY4TnN|M zVi3n*jHkqBZ48Hc=DWd6yZCAKy8-61gac1lH< zRZHCEAk0ZA$ENFDOVoE8-}C07a`+=g+uoh>2;#PWml z!Mx9<^eOUXF2LZ8tY&Tirx(xJAtN*-&ywsTRz$xcEPB&$p(W;W`IYT=O|p!nU8|O4 z+4`U=sMA@`S9?X9k7Mc1No4KRNh#m!LG>0ayWl6>`d=Cf?55-R z?ZlL}=Y=K=xdC7V?*xzmY(|!?TNYi^ zCFbmsm*d{o_PguGv}Q%LR=q3U3E`f?L|RXBzEx(KAvn#RCl>M&DQ`J1Ijta%wvfVv z_KQ7Ta6cljT@+X3s0F7d`)u1l5K!%LA@E9pt~-`imBVooU&~}tOuIclRBAEd@``8o z2t>u$I_C&rT}zUOMUpeMZs4myN@*H*&8|}}Dl1HM!?C}v)ii5TO;Hg7FwO1fj5WN} z8}s*)z!PK$9PkYMDg0F7G^H%-Nm@-V{U1D%kZJTM>f5=n?uJv~?&G>Mn9CEDvfP)e zfY#7abUMemyf7iH(kK;^lf37OH-nR}m1Qs^x~ZBHuhDA1eN#{bqo3vI)sLZtSv+Y2;N8n(mJZS#Ws&U&4yocJXAW+@vN(Ip99jOJKpC>U0mkDQEMxpD>5 zWXR`gQR-{L9uVgEdSUE!*LVA>QyM^CEtN}bXF8NXgDg!cK`IZ^^!lI~d_Cs&P|Tt> zy%;c3Rtv?jlCY_I9OC7AVA9*26xszm za?~8fwgen>d5kuUZ4Jy6$mHiGReqY$$h5U!1V+h1{i&yFUP&Y1j4$U$UGhC!+cqz}R ztUeu<**mAtckP(GAW1pn>p(cNJ$Q2-z$e%YGLVO{a)1Px6D>UQ;W_vt%A$&=RkNo2 zq3Q$MTE?9XKjhD2NIqiN@3Cj zN*5^SzuR;}H8Nnydt49Cg>vqiY%uC)w$3oSf2z zGn_oeVT974iZs}`bb5r*2+yN=&EVOexzUuQs=~TJouIA!yt*vI= z*323Cx==L^m#gp&38fVC>_rVCzC=-jFwm2;=*tznr}bo>Nmze4URBFHC5Up8_q>ga zgZC}!+m5#yCd?J7QylMVpUj;!8*T4KZH?=eyZ6& zC4}yTORZmtP)ZZslyzSj&1IQtXjfnx#Gfm)E(bHsVZ1B^Zhl9eDY|4vnMOW6q51Mu zcV%t%^f7lgFHNyfccz2}!j|YD@}^rRM8N%$KeTK{Q#D-!(Iq3EnsPnS*-H zLvgTTs8RyjHO@~R8FT+EYM+`EYe%AiHl*viQAEj>Xz>%oMhS)J(`hkn!_ z9!RvpO5`u#enM}FDo(Gny-ckUPWEpnV=~`^LBN{w#}hQJgsRoFLv^mR=HWUV#~zMe>AKkW1$J0Fo?e?Q zi+J55=U+rS&(bzt;^eD;8nboI!4?S^k)hGp#pRA}f`wAzu;^NtZ627=Sb9e=T zeXx`K^>B+EQze)s0VaRe}yXQ%;&Mte0Q6!{=*wMq!yT$bol*G;9*{agEup7m2zwHW)-*>;8Jh(4ro&t`)O z=djF=5XHi6i(*phU(Rw8)2b|!otV{z*{*mqL=~;04rY|QHN#YiB<6ofvjp9my31rO z|B1eBq16}wS?R_0XV9iPFLBD9QSW&m_tjw;yeJA~h<=Za$B~F3+x?OYibFRdu<{-F z{$5^DVke@)K?n_!0V-gF5at}8J3-EMrQlzRRlfA2H z(PuA(X)G=|JNb6~pb&gle^I2x=r#^x0?fx6GbTvM(}E0nPs#hS$ONxsapy95Oydbk z@_XS}G-6e1$v=;YTPiNF zn`~u!Xq9Tb@4%rP%Z-HepL6L9-V09Cz9=Jaa(7nyp({(C)qbDJlg; zh*o)`RC|47dy@Tq&dn@A%6;R?dh)IQeAEQT+3^=v=LfTF?dRMBMIXqmJkDesjofBM z&pGoZY#xfZM--^}(6h|coqk7J1jP-4ugj~0pDG^A@JTD*TR<`C) zR5Vulx%_z4A0nH27}cdVNsJQV@3@dL=53)tl)PsT!=?o{c-@!*%NS&-Xkqh9ru!7T z16J2cjRT`yyAiwFnqqUlrH`-j1?r3&sgTmuyleKdt!s-$bs9Pc6$ZIeOaY50qp%@T5ZfvN z&$8su^Wm+=HR^<7OBo8 zmi{+Fj%Fphh_kpj$Xe3<6JR@wHyo}d6^BX(O|lS>XK$gBW%2k*DCj;}RH`TED=O28iDAhvZYmONBInw6kN1B*A86 z$B3J^al*E(YQFS&+~n#CB|c2o$X|pi+W-1zB+Zp@#zr^^dK`OK5T;ny3jnD9VAP$$ zwFt=DAWOs1?j$nPQEwPX3{J&;vA zsJb(BKD^Gvz!r-{+f_!qI+Pb7K=Z3~Gpi=fcAv)E2TD`N<9 zT&K3G2xbR0ffBCHs8dTW0TU-#p( zm$Kbg5sx~MU7Bd(L3b&_l{*H4wjJL{5sI92FA?z&8l-U`Q<_z@GshRvSpmg}zTIsf zhFU#pv4`BsJ*!_yT*8UD3j(%5?akVA)|Nqxd+1(<9CCUx^E|S}PO5t@mxeulV!!SC zYlLQEnHS%(q_?BY5nT+Xk^X|0H%Y;i4!`iO_n zF*F8+pFOQIH*a_;3^P6$DEu&v(Ng4S$%@sBov-PG^LEd4JVhrbu^a^O$6WG zw~&vE$|9tyy!hchJK#O&l6OqhR1&Ii?cvhS*F7WcTVgZ0Luw9!Kj;2)N(2Szis<6QoqH@FfRSaeOk4_pC^~)3;L-oUvO^L60$RE+i;#n)D!1yz z?7j|9Bjv`*Sq$^A&5OIz&vCoJaQi(cp0%X?O-RwM*}rac=pAiv&YqA9Zk0W(GQY7* zU)=`Tv~8m;j@R&Pn6Fqv4ZdQyqukG?FrMo$dJSa6^{T%L?#ZKB!*R~f+7bsVo6rV=&i_uK$;-`0P zR&_5^GaP)mJ#W$J!pcViNCWk&!IQ^o!4JuNiBk4d*XEI(X)eOT zta`ySDu*3+KK4*i-i@nJCND0(+?;lR(!4;9^Y^SurUQ6ENK?$FMO!oGTx!&GnWgiS zfiG_o>)Y_ly@sOaM9h=VodyMXHSA)dw(N7~+HZkAduWp)BeU)-x@vgItJiA{^2eDy zs)`P_k+T7)Wuf5gwf(D(fpMa%;;TwrJc~_J053q$zv!l`tDHQe8#!lmFuTW!>NR0S zSKVlAQHwNqI6}sNH?{nvR*-sKm+_72cTwk-soS%y zs}t1EpH`$En@KE(SFSAMqMq(h%;0-xE%wKaU7@Tz!~0T6Gx8p{Xyo=7CR>U=XzVQ! zMdz04)~1;bFEWLC^~GD}5Y`>olB)YV;L<*N&Rw}OJnWqC!Z5ayPBZoDc<_4r>d5KiT?|kO4Lfj4$JkGKea2UDO>VL?E)PMxpZP-eH+9-dZek){DWAqwwak; z8CRRUx>7jDPB+oih~i?+ktMqv$>u0W30li9As1Rz*o01K=_Ve0>fqQ=_q!61T;9iY z*Ff4qzAZUQ-Zo2Bi>`L*El{_v=(kX|msK5yR_fxs1-l7v-7BIe>75Y_OV={XMY-m4 zuJ1^>w$r_$29)32$h0UVf`Zv$YFud^8w`$~U1;V}I~|4`oPB5q>Y_h}xUOT12-TKl zGfaAYfrA@y{OVt z^r<4WHxR8pgx3^3gX)Hdj&A}oyUt==xa2071geu3t~ph_NKmZX_H`>SB=`u2YZkx- zI(;3MZB*`1{|WD?5+U=1E|P>8y#z?j>VEc4LIPn+b7cqb(|0<^xq9KMH1){vWe9X> zu6oDMpObIs4rH04neo=_?|R=j0&D}`7CKMptUK$AI+nC)k}yLk>H>uFo?QKhTkY&6 z*E~)nsU}go$*x4NO}mCx`Tkh3x_4^Mepx)KmPG@4sY#sg%2o+g1AEEiszczK4mS}7 zQ-Q`b2;C0k%a@NGaUByV=DSF3GYiG@%oc^OQcgUE!oVq-|lK`ews-|CYUE3TU{&uzPp`OrEvS(%FoV5k~ z{0Ae4^{&)4eanC_jdT#ryivH*puimYhbU`RI0f0g1q>YlqoF? zotkc*m$^M2nPl;}ghDjlc8aej{Y9jFVZ>b`AROj@q%T;C_*EW3bI%tCd32$i>bMQ*~wNM zMu%7h?uNOp+_kiJ*H$Ieq4~fYw|K|)YCkp?I&eSoP`e-C4JRw~rn;Ej)eIIc{NN&K zPP&JJSJE`gg6gsk&>Mk=2OW6Ly0zO!Ew#{}VdIrtQZtXBP#kVW98)p3otIF*`$eiXnrw|HnVvWG^x5K8jq^$7u0>2!`qJ#>pmzIpafcR9JZcD)$BGhrv*zQ(EV#Uquy^Rg+<`imqA*=OXX-QM-^ zyD{oYB`u{GFy9iv0F+C_sfNj@ib!08T3oRtB`93uxWk$t?P;yyDUi=n+z6XtQvXyb z&(Eh-nS>-sr;vEui+>hEGrXGNqTm0QwkgOQdzBd?9~UTtxbEIYoRT}i zXP`cy1Q-x(bu^F|`WJzm`U@UE{~NE@#o~XFzF^Y#RdNPWk=_&zIOJr@-}7PuX!N+w z94kt!aqK(AG@rSFW9NW+C@K{iXYE6Ny4$j!LoADlf!MoQCQ`$M*;0?Msvq7E)A+r6 z^eNw?0iIol{+wUgB<%NGh+op`}> zQD{i$0sB;@D;sY+7ZPAn5#tI+tn&S8UADJ_)qXc^1GT4PX{VRvA|&%& zG8R9q*`I3G`Fch2edc*4oXFO5juYutIcpZ>T>#pf!TT)_DO>n+dpj#wGQ$|W@$-*7~t821`IHUQoP*qoJp|)}ps+yK_7ELJ=EJ1ZodOQ_1 zmG3s%{cvVn3Ev~HQk3B(Rdl@}QK6P)Z1?<~nHMOqLy=Ep{zTf4)Rn)t4{p^+<^^{> zBV%V1bW4hc&CPVsvsPVvEUE;n(Xq>|YcEW-WKnL&EOZLbtK*c6L1d|AuO=W=aPC+N z^`wUHrGldFzi@6zEO;zVuq3x>=Qif7HuPV*p$*^Z)AvJ;?5w3x(&wxOU1xXBqEy%) z_#)oXwvYO4dq)=)VaZzZSg6vdopbM^Wy6Z1DtT85FIgS~Yg z?xFQ$DES`q%JzHYzIadjx=;p=ii+y~A{wQ;jW$I5yUJISt=y45jyc{3uiFpnqnZNV zWc$XuOZ+f9Dy0M*Zo@fwe`c@uurKFmpvcI*jB>GB`B0**RqCgdA<(sc*LrcA1q8h? z!5eaIE7D21(N0xUqWYnEKSzht!{^8WmEA5o=Q8u}E=>yikZU-b9=*dGV1_PWAPu;K ztEMn-mZ>X9SngfWPTfltcdwh>S4yO-$nzL<3TKG9T$dMDPRpl~lXDE2psrOb^`!26x%P8g$#hj%(aV)U#SY1KC}?Er z2EiP$G`6Yzh9Wr-a$N^RAdTv7kZl~NfJNWkk4AE&n^wXG+A1&9^akt!A<4keJ^flK zAAC&0JXTbmW)IoAHWG)fu2CX0&32uGLVgVHY|7Fyfla-)b!3xA;8AkKvEFH(DOb+0 ztA^?8H?*m)@c8cli0&@HQyzoU{WT{V^&`NEyo2i+^|64q>864oXRsb!FNa*#TZ8@Y zw)Z?qSmv!b6>>fL*XT9B2MZr52X(X)8QcGRVYz2#8}&ls2Pg%6ODZWW*weDE=u`K? z4#6Y~Uk`_#**~^2&Xp~(oxQbEr$9e4#(s|4lnPPKEsLKU4{^q&JM)|HMG|$m*?X6UPBCU zRh^qNc~TbiCS zGn6Yoi)U`7M)5fv<+1|vYav0 zT}~zxnQb|Ml=fouLxUbs&%%t7|D(rry=+T+izB&>^J(i{+!oaj7ilDk{?YM4Wz*g6 zjLNO-Nop=r#cq4bN@<0Z!6{&*2YbQo>>%P1h&m(-*)!0bcLeBc|D1Uc`yf<>LWL`m ztBzGF#|-J`lY&S`4V^>A-00Q`%^nekghxP_4I>BG*gsfuXWnnAggU;ZuPK-&mADh3p~>8~*L$wvR#$wTdlE&iY%9<&$n_ z<`{OXfNs=FW^RC2zxe`r)O}~FQchB53ERpP1`D6!Ua>kwUQr#48F1oFqY2)7D^HTR zA5T+rfoRRTQ{;e53$l( z4gDr?(3J+&2g@O%O(v0`?RaVqZzxa*xI~i8IwptQ81S4wqRNAbBbfb7?FkULrHY9& z_7T>GB)^{(on5G)T$b@X>!wl9^k`-X);?>NfI#U=V)Q>%#sZbMP>FhuK_|xa~hly z>PXCPBFt4BP?65`WOzuY&~E$C7rKJwWM7tWI?b%Qi{=O~{ilZ2{@s!}6V9o*Yn~kD z!#P0p9-xncd;FC}gDS@!G6A5eGB5zqW;G4pjK*?8fK=U07=IaDgG4yKiZIRn1rAas6A0 z1JPHE^U3yrd{=S})D_wmV)pk)f!RoxTi|6vO{Tm_ zAYuD}ADznOAleSLG@DL52W34o`WyDQNLI4g<`C*If*jA$L?NeRLXc<}v#d~IFn=zL z))n<6SL=ntA*SDoZl47%P|pVv*oexLm0XMRnZJ%V7j-Yt>V%$*7EkzFWFDIP%_S=y zJ)<+lvy#%gNhB_>b#f_QrAmj}rmAF-(h=~mcDdyXQGDQ#VE>@G8nWCB_ISY8+ExmL zb1GM@V2N9#_*QN^!$57i9@P0&lzbNG=45KEj_y6iL!~JgyOodc8coiYx0hAb=K1l1 zF-)tv^{UjZa*H$kOi~zH^g80lRI|A_3?$ffo^xKgM9or~byDV7H@|?v)98(BUDhqN z_&8fy_!Ukm7k1V-%wDV*{qEHrfhBO1zD8vTWGtYbEVOGgH7~pzJ?|0s;p`lS;+)A= zRi4>QYMU2TMU#ut8Q{K)gS%!8Da|j@z*iDV#cvE<>`*XZhNYG_OI0u9N-clKYn?kD zg5T%v+Y%COYS@C+s|pvt#|HJ3)mt1h8S=dCAY!b{yCzq=ETRt1{31k%=KxlpW1fh;pt;{dQ%>&~ow?Xk%0 z1$73AE#G0^EszQtQfWn};5)}lWX5EajIQ~5pJ7#mL*?lVZDU*Q3-!T55-+!#WW(1C zW_Qa)|>l-{;dvc2;F2moo@2G)ZtpyQ}Pd=1iI^B zO6R$u#9g<1f!Ls^CCrlWW#ReI>as63e6|@0LKaK8&JWN11>qzhjihpq;PgH$LP%Gh z4H8k~bUoLEAp8>VuM$s}0-kw^I3rj$Sx73k4UL+-iW|!WQYY`i4t0i+z+>D>lG|mcKPXKX4QQ2UM5W&ELM_{c2|v(Bw^7`LlI-`o79k9)(#W}S z;4oH7)D<20s~_#Jr4Pr!E%_~DBORpE_kDCAtctuo2nqgc<>cRDB7@=YUYCGo7w88f z_@n*o1uo`?zJM-%#$BO;jk5Kt-$6->WS02}#Nz!eXQwhkag{MB@aGZX$+NALj!elZWer{jWUO_I4F9%sVYI7bckh6s?;m^ELO8l z=4D`O93UzF7Hr=GIg1ZD=_EmrrGjWw=2(D(J#ZKc`f?(RpYXhj&Gzj+7g%;ghS=PU z@!<)r1+&<7sC;DyAQyEzzoaUOpc~(tyQHcKeg#xdx}?=>%PvgO+IYy#mp=5rdl?fQ zr+I;@T11cNAF7U8dDf-|slEp_0H1TXlNnS=A#Otlr)suUh2Vs&G|CX1I(xINEFPYs ztpGblxyW1xo%Go`8S4A6M4?L7n8_C(;PO4wnOUjVMTXAq>lRU?MKb}s4coNfFhH&> zi0Y}PsvWp9$L+DCslKBqepl6AsYiK4IC(<@Y$`;MUn)|?iY)6Zh)Wx_4d1D`kGtl( z9-I2gw)3R}$&){&_(W||kS<^)OWYU=nhKo7B4+RV3Z%B~_1n@T$ZCnaY+Sl;h=j}D zPInb?V`eZ>ZLvWiZ2LxT>p7-Hs`HvxT#n+2G%G z=%v%iU?uu;F;XTA!?ATZ7S$<~lSm@eofHZ&Rj(_~1Wp>wilZG_XNRqt4XcP#$Wx;e z7IvG{9Q&L5{j1XyU^ecn76OS#!3lwRDm7^8OipS)9I%nS&%BRvc0d?s_IorF>u~lhJ#{}+h=1;9 zR+=B0*=)xAw7QukhE-J4otFUOty^0cYWz+?*l(XBH5a*sw^g$Tkmy6L(R?OHubxF{ z1Y!vvH!g@+H2U&_#XYc@R0TYoThZW5!L5=78Bw0Z@`&Tdusib7}}TK1$LT96vRY? z%*}!F5wjW~l`ufx{B(D+> zp5W6(ZbMl-WQe)&qm8j@4@g33LZCG%=H%4>!vGQGgFJ=bx6|%6pSp@t_{)8K@|kl& zn$E5IZFL8vMu~_XGNlM%R-pxCL!E+2RZcWnVG%i&!q+5!{C^Nvxv|Id7L>2CCztxJ z!%nxjT5WEP#yo8~RxsdsKKZ6Q=<#?C?cUai_Tm};+_!;f0A!n5<(F4Xi3;p4kbDly zaYmd1$h*Z|8{9W1mJ&A%GUVkztqu`T@+9TqVJUrfXN|aOLmot!Dy5X$il{wqismiU zD$@l{EUh_vbHP52ez^}ULpCP6 zC2YP!gz~$v8KZ*}w*SIbL+&8qH!5x?Xt`g-V|MU~E;GXTPg!a6+%7riLfvonHqWe( zGkb+%25fBJYP5i`-h|gda$gZtJBLzXk)U=4ibl|W+T2TgM`P2@B4IfC>eckkTnp6k z-_?IL=jQLxRpO%?!+Q$o(~6gwrR&6u*i(o|Y-MiNdZ?L=$wjLU{||k?ero-#Pn)QR ztxPEk;8a)DHY*5VTUQZpH@+8gtEV1fTZmDx*=N*~lLcvh21}9QrU5_&sotr+kp@!h zra>t;HAo+X|7K!!REIX_#2X}bgP@J3mdl(kNT#;kk(bE;IpPzN#A2#Zp(Yrtn{pRSXsg2I# zKMNw4!$@`Vu!#deWy(`rRCPaTA@p}zjmqisEVUO|6Y9u>2ke1v&|LA|W>hbqdLnd)yI4&&e3f_JcfN6i^nDV&0m;5iz2UsG-z``uOf6ZCEW zKXLHK-0yykjjjv7azSC`|Ex%^fDNMS)fv^}?nyRl&hMH@Gc!NpmjWf%M%_5_hKM&nT`&=sT#RD~8ciaM5ty)5G3-m}Scwrw^om;b|9i*Mq0luod&qVPxIf8JuPBVMg!L6C2 z_Xf$y``C!6P2m;5H+MLWsS-THR(X*OQpIzm4k51#E(nf#3YM{#CUL|Uy^~n484PoQ z>p<^V(WbjH?K;vVU1xU4{S!M7d{=D-X+Vmmi`}g6Ed2aICwJXTE9zQz1RbgKbExTp zcm1L=DK`}i;QtSh777bq`wHGmPrV_Mt5gDoo-x7o8Rxs|7?@1+L|@kwghGCbP*p8Y zv1fNT6$hf7kK?Z$ceL}3bN%hP9YPjlrZ}WI`VzL2n9F^+QEuFHY5IjS;BPcno}XfN z)55zFdzHrK9}@~&tyrX|mCx1$ox1kOu=b#aRh}L6AQV3nZAz77VI|hI1P3q5ieRtE zOxwdE%==bibZTr(-^`BR(!Dj&aN4uI7W6{^>?)RLadoLH$@K=I?XU81f8v|A+j zLh!`*6dQ(;bG1nO#LoCpOq$YjJckFW(cqYgkAbKpLW*aaHF8x*W;CXl`OL|`FcCd^ zeZl@!u~+bjG#{&TB;dul&={icC<*ttucq&D*_Em)bxu(Z|6TU3Gr=4=n&o6K7FpEX zTOV$^Ck@Y`A+4>Fo0LejrP>0xt2AwAkS&tXgPbHSmsRdz0s^#Lho!zyq>JOC(T?sQ zB(A!biim-8DMtTF17Ou4Kn}UIdS>rnQjxqVL>-9_msYS(u%xDKJT#29=lgl?eck35)Zm5ZJy09i1f8t%^m4!cC!a< z>R#D_=v_sCkhbEs2##zcB^~k$D}!7P-d>Wv2^u(0Ik$N5;_y-eB(@0kM{(N(qmhJA z$7hHsrLHI^09=>llmMjdMF%*Xnk;+B%R}3F9K5Qo?uT7ypE4;?e&M>7W2WMWqgK@9 z;JU8FRDQbzwpfa@%g{J20SYQ{Rh69;@+sPo)paBfA~%K0I_Hm7GZRn2siz6NJMY$= z>kOQ%T2jV>8vs0!zKr`{`tnQp;azTRM1hMx-f|1`i~??p$b$PCeroJDn*Oe9J1i8Kdx|o_ zKsYyt=KPz1L&vIcI~lArL6De9a`jS)W*iM{T-EU7y_vPplWKit{XjHN{ixVB9=$ab z*L0;DG*>Cm0(Y%?ajF4l_*=P>QhmtPs(hRE98&3`8u}=E@3zMfiC@=N;_G!(M=4#s z<|}g9GYau=7_iM&?R~v98A?}pR)l7+2~W|L7nIz0iSALAI`ewUFiy>!VO)~=F6A7P zqlb(M>NH6vSmg|WI=e8RxA4@tccsVBn=0J?#6GqX*>~WJKOt(83%F1ETKK?<$wkSzBXEh?Kca=uH!<85yL5|5F)y?`Ui%M|wUU=*$My^-huV6_UI?I&IC#$j z0kl6r&lphWa5`+xah1+qCN_y`sVoD%c;0=&@37y5x>YJmMb+lJf<0L%RiqqeK6q;u z9l7a>P(Jj|QRNX3@v2#(tee*$j^vBgN`Iwk9*?l<)HJRvDN=~nwG&s16iX*#GcQ+* z5qOd1N#ZPryfhG=vx&Q| z(%Q2aE9>5Sg($`YGL*wA+?vT3Rl`Tf(_}-p;zuY2Ilj5Ak!bNa9Ervtz_<=?jX1U5 zUTMYA$URCkP)CWVpapDmgf3L8pzFX<Y6;~k*cwWPyba5^E z;jX1lUp9Sr3Zmyu?v_->>tzme2qA7H?U@9<7CLc8f#&E6>0F82Hd}SJj`|B@@pV-+ z1xlC8d7PY5mg~%uSF{zS0ydIZO#nRpBeFkUdq@or=>ocyvKu?Sb$bhCT#)Y6ows+2 zJhrK?=gVo;{)p4Pi?LmR5A=*Ke#;>{4MJ}GgUOY=#XzOl7@JTO9`&_QOzT4|g z_8R}(x!`n&0&t)yC%wg2%_}Twbu2bkW!`!Y{=#CJ!)zT|p5#>074m)Wyi$0v6 zM1gSu#NN;Gsoc^tm7TKWSZb zwa;CDRn524JyK|S`^~PShDX>ytIoJ!bP@^%BEHo2#0MCr&de0+fWek>tHjGVGg*G~ zth!sAO1sc~Nw>O$%XDqO*9SQ(yAC6d`CJM|aM@(F_(hlD*__%){3m87e5lc%>5jXW zJO27JG0SG^*AzEzAu*{>7GezY_dQrg@fYf79+vkOR5n63xvTS^sx}p_$gozMbojGX zrB(#qP;*4>C2+M>@#Rk$rRDO_-2iI{SEaiJ#6U|^xA4xY#%oR`_fkV8h2bZnW^L83 zJ(JVnm7(Eo;}=!#OF~wvJU0l96!(R2MKtx@Dvvt92KI1Fr0R?oSgOlz)i7zP8WoB6 z#porJj%b_9wWVT3ru}Zf5~o8IZ8=*u*snt=g24p_jr9~AM56(?ufW5D8^BZhr4Uh1 z`W`qrjUBde_=F??>;O6q2Mep2Z{S(Uhx=FyxpB~SIn9+t@QNW9IH-iR51X=1Wy^ac zkd+lnzT{<;TZx7X99iT`Ld_&&WoNAH!vzNgV)W$>Tg9PBcKa0aJh&dnTxhn3v1BF_ zsK!s`9^Rg7!2Qw38--vJ@RQSgb(i4R!@yi%M|1GBAI9q zkm}};(Rcg8OkfqCv-2%B$EVGoqP;oZS@2<*d)rpe_^oFZ^FM_u_YjB%iMqK9g@Yrj za&MK1HdQSq_J@+hk|e$7^W|91wY9@H&t{zTh6+*P*dD9i*+C9XI0`QOFHIb$BMfgH z$&Q3PJz=Pt`wS^a)oBzVE9EvM>Nm0q3JIl2UKFub4(a@d+cvnC(Rj{y@ooHFvO z<~CR3up4S#qUwNC-2hJWUKv@;U|>^C`$e5Lzeby?3w3_25=k55#C8ODrvMi*o^69<^n|SL-1Td`DUk9O*_Nru-t!Brmb%N&D0#NyDR_y(3U`yB#^t@42-ZEjx8 zIImPkUuc^CnsxbL>E}z^yl&k$D=hx1yO?fk7j|JB8R#No#eK^lF@z6QuPu3_Dr+{p z&e2{z{JZF~nML$$G%gz?PB^kn{rI9!+WH}e^LA8CKactt!K263gOQWF4pHQeJf#av z>0~jfk~5`yKX6J+mf}Ui2Kpj?EUHr-T&xu-iw-cS)`{ir%NxQGA;jG&iJ(|e| zms6Z_GB1PequO0V7E7IxYP3BisvrB+U%8OFpKecsfxdeTRlbI%G)9eY89dQnYE;!8`4 zFseDFq6a^5rTnSfyre(xlGs7#tv88Ra8#}1sXm)~FE+dBdRdf0?}q3$>ngCmtDf*= z=MsDW05_#6_pO{yc58ySooUHA?}h86I1h5wAqf0awmE5uvj6Mhz$kBl3g7#>fX&nN zRRn2kUZn|3Q6;akbr1G!j+)QWb6t4@lCJ?XrMf`3n}e4C8>1F`oHK8;LKhi2P)zCv zw+7LrbOU&>beVo%b!Lb3JL%$83u&}z>U%{`I)k;sztqpd^3lDCR7&0TfhIImkjDfX zj`RYuJR8pPQwT%L+>n;Qg;W=!?VW;oDorJptXl5Z<_{q-Tl&vXzSv6?ccG1f#Bj;p zhIm!SF4ewIyD7+9%!l;zSX8c<&GZgsDLRxzG4!qtv{*Ya_MCh99~00);wt8z0n?$C z3j4iWLg<<$(3zWIC2SVT4c_4-KRPQ!K#+ac!cx0B>*6W$={1{_N<}i1rkygs6uD-0 zBn4wEobt4u9LAh#p+f_LKRfJ`Jk=2R?-MAhT~p09W0SNGj~rpNwF z0}u6^XKx>k{qB@EN%GBEjXVVJn*(#HkDKqX#)G^fP3Q~ANemLyvN)45r(q@F-VJXS zTk+5qX6qqmd*^<9@5TIuZ7n?wR!SE4)K}N~kU&I;s#kgvu9`iL8!vOrVNsYhk#8Pr zAPq;&E}dKwvL;{5lDVXe43hyuLYDo>H73}yhz8xwanD1oXN62=-6`&{bx!TsD`^u0rpxT`iupV9Bh_yl=13td%OqSHLM;&cQsqz6JVH zG&hiNWMg(FRL)mjc+Np`-?}Pg&Gn<2*{B?KB3)4S>;kyhIc-40=f&ErZf%k`owns} z5&)KmyhdaJ=Y0b2GsVoK#dQ4v?}L1|FnN#qRYVr8gm%@oK$38jUu>v*!O1R)`0gJ! zo4xr(==fuIfI0ftHqTs&RK9J}>(I^7KZ7qN4n2DtpbJ{AH!YQ;RRL}8v@>RAk7N0I z9mT9awRhq>g$?jNWuov|$ObrwH6f`^OE8+|H$pgs2X$2!%kW&kJ{KT~S4xg4urc_U z4~=)5gx;K=Hh=I3fVqbpFrx_Dk)HrcY{mm8_O3P?VBV*@c82T;P2?O`!?vP39~+U& z*as2mXggC485GNSm%{(c1U$D@h3Z=R>_Zjs;~h%iF*!KCA;`T^KX1JFYE^`(R4NgTO%?nU zpDTCFqKfzx%~$EuitrOOHeXN{n*}M1TRV<&w8qMJ_3CW%7qn9zFIN)y7(k378Fo&Jt8l!<9J zEBCx4yt*fRvo6Gn&y%t~Gh{on)~P&K$q&7k5SgSkWRFCmnKw~l%`$$JYtE_($FaHf zJ*(7h&SeJ}2Ce5Gj8kaa(zVn*m;w8o3(3u?*A3t*GRVCijhEVM=yNGdVHKYF#Uh9! z71g{*hLUf^tuM`AR+>`5bfkv z^?i47FYmyTq*8t^H_)oO>#e1fxqiuc+qwew;{=K@kYeywS7r34>sh?oRp#oq`f=j6 zJ9INzif~cRlpI4Zl%TF#h&``Kx~wfk$-n06pq zbw_n%32q-TgXCS$#Wr1tN6~Y$-KE-A_glwz1$WW{&a!4SKrAUyEn-d>pCxG4{k^D( zdd2nV;enPmP*rFvt*ThPl3HGB=}J^ zwkbR%x1n>ctt;b_{kqNK9K@Rhm}J;4nb+UG+$NOaQ@OS_w;=^3gaei3QS`5jn@fwN zzPTQl>?ifkEW*BnVkgduHZqT+Ze!ZRAb;MnTbZulW4ROHq$1s>*r63Os~XmZeZp^e zXUNu-RC#mz4gF5ec2{h@D}_7v6NQj!3$K0!b`y*}L8em_#Vy|Fx#RIgnO}vzSSS zjyBP&(!|cltJ8ES|>P)Qgc^Td+d!c{hd~-aCMi8yCfEl@%w70W_8=m{{JQH zZIT;FvNXZHo&vG!76EyH;36b4tE<9iDyvdDVmhT(6eD3IEggVyV1P4Ft3F0AdNJFX zYt&8pB%|NYd#VO{%&!W}TEsXl!_-%kw+C&p98dQgBb&s`@j@QWqnl$n}ZFLm&% zF;(?IeL3W&rhvXA3!k{z16t=PBlu?atw;HKzZyqj%lwbrJ~t-hl#FB!3G?0h*T%0~ zbf;R6GEwQk!Jj3kcJp6=`g)xy54=_L-Co9&zY^X@PUUr*9nWv8l2FCmQPKNb#II@j zo(^M!5BL3vx@U^Btq>G&Zv)M@6&NqSP`-ZZY`+k0#H6WHZBc)^{t0S}t_mvPCV5Tl zukyX(;OV>iMp2j9AxGU^&E);2$URbPzmHqdrU&mLZkYoLM+9l*+++lG#T~O|HA`{e z1hI;#1vnUqDZnr5wm9ZN?2HKu=-1no^S_HPCkf>rV^Ng~+@w>R`5Nh|kh`Xl%;%H+Q|Gm$z6UI}_Xagg@)d$rCa( z>W|?k#!H!>aOYxJkBGl}i*a3DzMsA?<(3HCXu8zQ5h#`p4s~}Ia%32SU}Q|t?d?Xw zP87qIu7j-|EjM zrPk(UcXy(U+}-&ZA5kD)*m8Qb9;ALK3r{@UB;_FXPCk8XJRolqrC9ol>s$rj)JQOD z;+7i}G8<{k`^752{ChtrHw1i!ZtEtaFkUdyZBQs+eJ+b<8WUv2(hGoy za8GpPZjJoO=$Xb5VJaS;h=c}ivz6`rM6?C}acgnirbKbrNYZ9%21c7Cw(stT1m^w`x4p;`%5DsZB*m#y#r{Cb^}*B2ba>MoA5t2HIb zH3`oB^1A-oBq~EQYHjKh)zb77ZWv2E6N#W2!F;w?nuNV<0@Rw>yWpBQIFihY5p!smY9Dn(5eInc*%eV=W5FE6Y+kW4BqF=ylAVa zvh%e1GxKIhZJ_7CPRe4*Qka7`r@yXJo%K`i~xb-|%+_ZGzzg zcz{`u21&viIo88mV`Q*|!ba)-qX;h#8|zOqJ;b$COZa+A)8-&5>H4j|)*xLUImU-2 z{IohJC-qum(40QPhOy!5?ghBjTWF#=b(d-cV;u#Kdc5z0U6n9nQ%;}X#^=Dy(?Q~s zQQ7IRzZPBV#w(($@=D1eV`SRwi8>L&#+@&FD_FS|o`6*DIt$}+8`$b8BwE=q&e6nc zy~;oJya-Pc0y)?x_N*haEjDb?z)SaJ;OTIJ+i1lgRBVxYljTiAB49{Q@^euXh*SGT zZHhL59vd{V_1m-0Uhk$G1F~)Q#%;$`!lANr2#k{zs5qu+_?LVdrqe6xv7Tx9`bUjH zr4Gwb1Ix=KS=Ixj0(Q2Vg`J%C8(wJ-0KqU~dFzIhw;Pm`T4@1VXp1b7-FNJ=g7FzAupl7PfF*}u2x`!eRQcGg?!5@TbHL9^XR3t1GzW`O_J+~2 zi!kUr16ceZXS%0bl$6o2Q|=aNdm`<~GcyQ1t{#PL2aO!Ms;F>z)SB{>Q{DPaBPN`; zXd{2mZ(iA_D0TRcLQ@N&L5e(8GPdiIiyb{q66(%B1-P30;zEt!v#jWtm!&l>jZ?YG z>~CcE43fm{)yTt?4g~WY9e25Tj0r8SCl&1+76GM5bSAt^xP!jbC_jOo4qw^r`b3iJ zx65uF`)5Ph%S!dv=NV29!{l9li_Ptd7XxghBi=>4#j;x%iK)7aIrQ|p|lkMDhocfZ3P6Mt*8Jmt${Igbu2vrC6Wso^-r;*-8a<75XH$m^XI#Ro!%^B?VetHxM}w-&*<#0Q*y;jtCD=75h>Y-2Buu~efy0%W z;~~o(Tes}~Mh!;W%4u;tL(~x0F&O$e#Db)65^5<_p&#%H{1Xt?qba{Fj zPcm2LP+_MT1VzS~SF<0?nn2L);HAk#=nZnasbMSi(@Bbkbb+Sm%c6QM94%PcHra#v z%I5tpIW_ghx{MD+Qxp%NU6*1JbT>R-JT3})iwGUKjxEH>JFE0?e@@Fd>;1y?-r^QG zC&99&bG+LaDB9G9gaj*!HAN<0$(cgbS4+Z*MJ?>Z`N^ReRj1p-Ct(>q^X-ORzYO23 z1Jhe$d6zLeBVY#8odd3}XH@QRd~dUNLCT34*^ePeeX7ZnENb=j@>FCTLKFM1HItp* z7aO8I6DDo*4tQTs{kVD0Z2o$&a2)qbbf9@K@AS#oOk~=I@xp1%&VzKA#LCsF))lAx z?%04Ug}6bo<_<1$zQkYcXJ$h=Eal3lhe6KT7e&9Tq?)-zN6Q(DBXRD9%CZ? zQ-@0W=t0|{gfw$@=4t1UuINPB^J6sDAJ=cHsq7o6?{g306G5YoPuat>ABrU20gnS1g7Ju4$5Kx}#XU^eK#Ph=QoT_PnX>&bzOyyCR>WtjZFGC*+SPhX$aj1#&W(hIgK(|Ty6Ajc>Fr2 zEM%#t;uxA7f9iW%(!vSVM?s}A>+2GA!xvDrM3ecjE$m=v$Qo}c9vLbE$N51+<(1Zz zvfbe{COD8Y+WGPx?XsMllmIGs!cSUdg@C0d^KEp$06{>$zr4@Wo`yFVGAD_PJZ5Tr+_{>bjr{}&-KMi1}dSa2J1iJdwE8Y6O(%!NRH?b+C z6`g>nx^E<&00&3+H0er3cchNlikw17Xdo0X?|WaJ?01>nPEbfTdIC&gebs2Nh}qV& zW9kpDxwr{hAz?{oW>bh|v;Fllz}`=B1`LsB@Vz| zBo?f_0u)$mmfJ+MeZSa+atR#EFKbz~}autF;^#JP?sr&Ycu zl7c2j5RDY7c($LJ5JUg{JiafiO;YDSCi7Op5sYXhODi&~TZr|I0P^8}_(P-36cVjj zXuXN}tvdJ#iHI1{v`ppl)EK6jTs=+?xQB3;BTK1Q6>((hb)ODonjIt@hJ)HK+cNAg zc7od&FJF@jPVxReU*+f#7(Ycey$2y_xmkzBG^-j`-X~pd@A1k;->*xnz9b9Sl)gQ3 zM3P*O*(y=?^B)zL}E*Eyrlrdz76ztqd;t^3*nTMPbXX`0Zy>*MIt zAGRiRD^0g8)VU30ci)%kRDeBwjp#e8pN3amw;;5~YiTkGPRG5Vg3*1BjE&=vt?!OY z)sbj}Y8BOvnYYa^ssdeR*jz3cr657mh(w|vOdh{A?4J(hKiR#U6R+Ce;RhwtAR(|^=X#e zcN0R*8%2gH$Uaeox#08kPV6zz0?46xl-xLN#E}||$30yQ>Z!zyQQwI@He>_mL6N18 zdnbtXt$USj84@yxSR*)sOo%&UIKv*?8C=+`0 zz@i=N_B#&K(a=+{^|oG|@%OjkLSq3#D!Ov7)<$L{HdY-%4VR-iDe#~YH;+>9m;(aq zQF9ZHz=LkS&HIaGro+?gRT|4`U|$C_hMA*pOq)strdoLt$iTQQ?-C_xsaaPmmLJ-@ zJOnLeT7JBnh5mH1&}fk&%FYjB?8 zOREuSnh2cdp#08c0&}855cPyuikC#%(Ag!NJWt!Z&?t1$eA=vOqF^* z57vKSzHv$`cxJ)VQO*0DJo2qe5#WPtKNOWfest$^rDiJWjCJy`?Wb|lGT2{B3LrzC zCEm14n?Gp>)>@+3Qx2|g%yL*pQ95%n5rEPEX>rKdw9KB+AV10P& z4_)?WD+E&m-*W+VJxYEYQ>6oXl`Yipb!!Li;luB#iL4)+1K;^Au3`-U}+VJ8kKo@43Rget;Q z2EUh3)k|=NVw(!f;|?8*OCy4Q#<0=^uB*Xx4-J!2qhoA3ViaMQD8QDGw}t>&QImwC z52uVLJ#B}u*0>i4-J7DPBhJP+jtj`V3@y>P(RZCW^6W1hbq&$ z?N&&;UwV*;_ULyUx7Jg@?RTe|uQTNXmB%v`WsP6b(4E%0FVpMAqa7ROY`e>pe@)sP zc;cvmYK?~@I?bg6py`yZrcA7Dm=r!cjjh9k z`2~myuCT-tN>g&*+)QaBj5R;xjD&l!j0L0JrE|Dd(;>q0`v9q+&LVu^H+3?QTDg@t zv`!|gshT0yXMf9HeY&PBb0Lp&(hsWF`&3tD{H}p^h(c@N%c_58Px-EcwoVhn!H(IS zR91aXNc%~wg9cl6*7&h&+94CF@#ONJm0Toj!^iM-j1kANRu_AlKxUF>pYsfCn9-hhO#G6U8llq`tp8{-^(PWcet0a_)ZCDG0^EW4M3sM|I1 z7KU0V;C##&|4iU_I@SW=h`1bb59mqy457Zuvy($7kJ&@Q;U%V>{#g7XJg^%y2OW5_ z9Zm>=7JAjQF@+qCKZUH)W_R^-G^3CNu#m}A|1=s>cMAVsd!kA*l^g8E4AIWvpkDv? zLFM#+{*I>&;iB|KymfN%6E99vZ6`WIN6<*c3$0B@igXC$FfBtZZznN_kbiWd5A`>c zD6Vwm!lHNGhtE`;W>`nqi9V@yxc>OIy#-?FZc7A#(OODfX*Y#JoLG-yR~&~9WL>&# zbKgrNM00sLV8WE$CU=)j(}<1GVGlJzMD#B1$r3&v#qKQRc>)u?)q;&d+GBmwGp{Uk z{X(iGvhtpA>Y-H=gNyp^r6#M#z}ktQGy+@>y|ZBwU*6br7EZSxnx{GE5^=RGr7;Co zhBR$j%2JTS+#j92sZZ1)bU7fBX1xY#S9OoBK~2-rv%oP)hHY|(=nj8~MgD@g9HrJv zFINExg8G%uc$go)&nCa`CF1Ra9z{tF^Eu)fsW|E^3)u3D(Tj&>3^d^7j?ZsD-Eyie z%bem~X%-`_LPHpR#~TID_cvr+N4dlAhu6z!Wqm#ZZ=j>q)_W~bZF@?mqQb%0Lon!zcs3KaybQ@gTQAStau-3??e#NJU73zxV2 z7UHp=hy5X#SGFFKxCV*@6S=434r0aIf_cpsVYQr;@5Yy_W@(JYKP!ud;=d)hfE8(C z3IrmLMXXthEZ<14(Nvd(y92jYG&#>{blUaH@EmWK*H1jZE_P`srX}Vl5~uUScj$>0 zn6+ul@jC;icK!0-3ucj;WuaZAsd6N!<*79X>P^iRdOVDeIgD0kn;W|8+zT^L@(i_K z2MyHPFYIIbDjuS2f}b%p+v-cNk!WXGn8uG3xtVR{@&V-{;S-)*Lc`_v?(R5TrK$Oy zKcV}6Fv92VuD*#vC-_nJt3#~GlvYr3VA zMzzW%lmWojm^Qv1@+_$gt&ZX&t`MFf|7QgwKk%6Ba{OpS>sOkZ{A0~?r%|2#z{sH*PhiQ!fdVFjF5?f8pnrO-q|Pzg;f z?^y+-*{)xRvX{g7eXztsu^$;xjj$%*J)EXKp&@bRpq@oa{+hLslRk zM58`t8U1gw6>=+z|XP7|$!uC|OqM zT-B6zFV!X>#g zk&1}|)17Y)r@S!7-Q`*F02glv)UU*Ltgfn0vVp`2Q@zE8^57kHHX{AsCCBNL8Ivd9 zj7~v2szDD!W`5vdTH0wV+(_vKBCsGRjv5L0$dHHfheA)4oxM>UCj9L&Y-U#=B zDc)QojeU~54-tuy(BInriN7ZA4(lu@oyCYcH?^=fm#VYU|m0khF zsh{ePYj*Ib(^W3_3Z;^RM@I}fvSd?bD~rmK&4`trru#!V;VTOyd9`r?g%r=*e)3Gi%dG(iheTfC zHxlduLwIP|J7EWf+x<9g@8|3~OSU=qz#Z4VgchsM*SlagdM-}thpVwc z+payfyxXqvAQGiQ_0PWO@RveFkkrCxL9Ls&3cg?+m1ttuB4m~+Hz6gQflGFym&j)O zPCxmj)=W@QGfshm!Y8_TH{)-kvc{bT;^)(47p*1xDuqswrsI~Vwieu~C*Mco@6T)we+Ob07G@82?~ylhjh!JJg?*mQ+1MMYj`p1+y;IsuxK51? zSW2^O_ieIfdtN+%X2o?=~q(@{p_X;WHS`J3tKdt=mxa)zB94P-_7CWS&5aQ}J zSk!B0pyc$-&z&%n?kF*Z@0iMU%m3@q(h-p@W)Q}}Oz=mfP?Gt;ab7`>sqVwa5S|C0 zP1xl9XRZF2Gp^E$&Q8QEXNkjDA$28!@;PR;cpD&K+3T}aDxDZ`B$ zC$Ew-Atpccg*|6kxjWVeSQ)4DQaJ!x7hp8549l+5-pmTVgLUfSU0} zq+a@tHtpzfi_T=pYMzLTm4l4pcc+GuX!JK5oKX>WM+p&cw$ zm`J?pFCj0m&n`mSXD)9N}MCjL#Z+1!*36yQ^iF}QduYE!&*p=kfY1QFNALK~hPEp68A zP%Nh6)ze_k?%SniO@%!GpDP+)$RpvSE-|mLMOHo>r-k}1|GUQQ>1hCC`x;80D6&7F zhB*9tHkIo?-EaQP_d4mvY~&~-lOd|S^8N`cO9|aHv=F3L>vqLr<|W+3P7kYpj|8tI z1maclW^!jWjOL3!bJvqSjnSJi-nJFd^TZoP4hONjDdym6h4FZa z(bYs}Qk)r>g8Xqmg^4kR#AtLGGl2+AY$5aLm&=jZms_)5XEfj)b012=aB^MXF^X(h zWsy}VWr?5L(a_#xscgc1NXstCpRPe>aUSF1xL1!(VFBR}zgU`ax-r|77nqbnPM=JI z8e0YB>eszZmwx`kPx!&Y+88)ymn#*gRkW$TpL!4bLJl^@nS;q4`OpE@+0w~@a zfx3Sikpkq0^%z?Oj`4uA(nsK45D!eAyE`uNKi4n$fy3nb`Yz93uRruSVa!dRrnBtb z(y8iouJAR@N8jhUWGEq;6}kgjxUgFRIj<6!2P|Ukdl%)ken+|8`lP21u2n z8yQ{BoDA0YYPkY(HjZ6p*cwotN(EDMA{2Ph2@93Jvd!Cc|eo%~NRwU}Gb4Wa@(5s2%?%aiC&c%eedT?hYIA zNrP^x?ABX-uB)Fq1?OAGAdLnt8bUpsp;17Xv0Op8NAv9Mgv0 zvHM=*AK62N{d9Ju7NE89qW_G6$dQ!)*-C1*_84etFEW@=5TG}O{uZPRSSEMa+nOr~ z4zMyNScPf>g)=%cqI(qZWT*^v`P3wdmdJgfb4%F!0-F|ciFS}V3`Ym3X8y5+@yJe0 zWw|f55z)3o?9@MjE3C66J)Y&>#c^SJF3?h#e#Z4)ju;v-I8gPz1_}56(jqXLe*FEc zm+Sj3qR}+eF9^TNy;l&h$bhzTke%KkuAZX-9Cc|Y;+)h zOn9#BCc~&OW&nN1lY%4Tb-a2^wtWBCMhHP5}-IGs>EljlwrMAsToeoV7 z^{vx*2zM7y;S$x8mB34_;?eu7oAZ4yt8Alc5gtPlo|>QVo6E7@S*|a0OG^@ z%2l3nZAqlfWh{JOqg}|q})@B8FHvZl`&ICXvybCbmKhe$P%E$ zV+o-r{dCrGOg<*qRI3#@CdP{*yFFBbkX_-&z6Z9im)Q71j>72zdsi~-*juRibIK&A z3@nooyw8&FbDP$GR2ECccTOMPPK;ZuQc%HNvz796;r}V39l`(|cl>E-NUG@^R(y7> zmYmM0Y}OivsH~XKb3ImaN%yO~f(uEvfs17OPGebdZRB}BAKuNe;Q$RjOyQ=Aman?k zGV?{tesdrWK@Q?YE+}bETX!T3DqPh7uG>cD%qZ*qceI{eDQT6XXuOW1Kvef=5@yi zJ1qRUq`e@I;w8`?xJg#SQ}!6UBlG3BA!a?CV~pVF?pbzs0E^7qFO6gw=!!rWH4_le zo?Z0lW1wLO0`QUOLcwZ6X$3qnh=+b`DRTLx^qC|3u)Zuqo9DOryFHwv`=u=o6E=jN z7B9h5$SctK$I8|#+o3PIO+$bQYrM1>L3SgNwL69u&uOqnp21W^GjTA1$(Qqy{#z(x z#_{XYIzUpE%hDuEs!ULp`I&NTnM3K(otjGTC}f;U-%wqZ6s*_19$`GIG%k9Alo;9X z?{KfEgo#O4UH11O3?aN_0uj@s!L?t=;UdT&D#vkEu4+seIpV$Kb|Wd{mtBtRO+|_& z`{n50<~nuq1G+kiWpukzb*>WA`@R>YG(%fi$yrkVisf7!>gb1>VtJ@>nsPuquKD`0 z_e z6R!3OpWLZC5n}{(nR7QUZ&{~*AcHI$oqK+Fh!U|iM0rtZLI2HU z0s0{KML|~&gzN|X=Q+=fy{BzURWS3YJdU&Ooy1U((Y!$7jD2&!lXw76DMDTE&hwi} zogS6Z4AzVCr#C6u6fS#SWgY*sChF1_X2ct@R7}=XuN5(63*%6sbPW8raNv2Sl3P zwK~>DOMJ6SDkdL>-Ei0@L;^fpIGAV&xEi{k7W4fDEXruV(j93BPW$B<+I(17+&H96 zAwR6^@ga)Qt{K9e$M9bw$wwrbRE`XVEPph9W6@n==~oSM1iStDylGCYiCyF)vrHrJ zFSd_bV;>?bSb{Ao9{`=u0&Lc~yxnWbXjuz1QouPemrTU(0QpGjQbNaAC$v#YFqBra zO=dL6?FEor0-oMqxDCso;oVzkPvCn~!prkF4c=ZrD3_)xqPRY zHyaArCw55e`dk2cucI~|uki5e^#=~8E;f{Bn2}P2)$HYI!nVV-f#MmMVT??@Tol+U8aWu~de}lbCEi zgF@`c%oC_h%9H7orUolvZ&I_dkHr@GDbgek`Re+w-|1<$y@efV0!^%dp$Y7ZXimk) ziLteXuUq@u7jGxyN=a(8rpKU?l$Q!8dDUdm@pAYRY*v00gd)DR4SM%PJAG&T2;MXA*?D}d!_0>0*O zv%8_pFS00kp+~V=>#Ngz)VYuQI1H3b)^t^$lWF4g#Cm0fUn!8 za$d0gEoM5dvfZvz*GK^6$<3+T#CzP&^g2kSD`QP4>ubFXvf>)CE0S0;p}>}!cOODU zJ~TC$yQ^AI8`nkanFs2br1*Cru9qA_2ew`P2Q4f4Q}uI>0?{sF(gqA`qCQGaHfxRf z_3ln1(3%}QwYnL^+;MppttQ%c%t)1rV~{kS$Y8M) z!dz?)uPAk?8rLgi`7Bl$Lp4)WAX5dD~WWnf7&-P#bKBpXlLP z^Gh=d5#CtoBLqCYjgPlVlQ|Ti@I-ro_Hrm~4G5cJTsem7p~>sI|E!UvVmVv64^;B| z|KU5tu?*dW0I{@sTnvGFjbK>tR%5*>N--R;yl8gT(sqd*S*eYp9k4;={y|}$Zxa~d zGUF0&ZfUQa2(H|~yU3!bX5^^F=H7Y?s5r2tnEE6Hu3~{sWpi&!+)ZI(`YuNz%y{%v zK8SA@z>4sb>R}_3w@Hu^EgT%WI0n1)^&0kPhqOF)JVk1?Zxi>!A4+N(0JytC3l#z| z74&_N)$&K(=QlwXgJA&`kD6*+-fP64PF6=qHgbIy^%$9}C@4%MA+_0IH(2U&12YM~ zl+e%NZ)PzfLRq2{X$8SVN(3uE76nJ`D_3GZKT`xf8XpuJN!?lvdkf+8v2aU~N>wU82`N!!-ZNV$6 zCg{><=!z5d>4?a1!`GdRk7ABXxD>JDA~*f!7%Vdm}=wpr%hgw;shFjy(s9m#Gjyr#2(LI7+g8w48+ zG(;eO*R^+vhg(vQXBmHgxH0sZg86*3JQ63_OPXn%=%N$_pBykGe}ZP}Mg700iPF&i z@t0$!OnbLAm+ARyjTcCI*kEY(@!T5kC)ypjlXwU<@!{Mg8rh+NmHV^0CPJ(%9a*kr zL%a+~NjIfd#hzTHNpTklu&E&O-J&wk8Xh}U=A4MUce>G-_k zkUDjp&h>z?<>k~+O205S>rjz|25>9YIR94PGwn5KXU+Gw*rO^gQwU}fPZ(m?d-l(G&~UhYVxh-ve!@>Xp2bg#$QIeW z^S9>iiP?qj#4xQ3)uz@g!B3=+t7xxr!Q(2X0k4Vvenn}8(Y5xxMb%bfI#c)X_ArMMl3L2mOIo#wGJ*AKW%Kh?GR{P3ji3B9%XygNtsR;K%kt1lRW$^bbIH6PdG)r} zrnAeoZEAvWx$XF5yZI#SLy0EJqP~r)T!r$m;!-=&)2qD{VY*kPZdRb(BK~$NuXccz zPsq32F}2LN>?h)|o+NfT*J^s*qNJxrpBe10|6lkLPp0(#?V!5m3gBjv)n8v5Sk--?V&=uEe9ev zlH8ltF6w^xP=>eh*iGIzhP&W;;KiZFkTbo=RMUi?>GN?4mn`&?)ZtlRcK;bpP~yTvDo^0Yry!&dJ3=4 zB8>k!p3qLmN%9c>b$Ayem?HAp_Vphn_{<|wR^9~evNHJAkDAZufh?_^wAC-A!wbc+ zrO#RV&VC%K8(TyTZ4SfXFl$%btY2iNdWU{K|R1>&?PEuDmY^PWIF`oR{~wAgwaJ zqW|?OsLo!L-)7Q+$yf-?^b;e=0}7L&EL?uADi$pITzC56J&9QWcaZx~_sss<>1aOH z?e_w9Wi7_(EeO5Jt&)Tn<~uY8XizQIJrecG*8DOzU|BwI)i& zplUsc1M(tV;Hm3P({SNhY%aOoge7vk&vUwnDe0H9L60%j%$0*vlG3`j{TV?crpg+u z7URubJqiGxKC|#PEFB#`-qu(iA4j=3>g+Ye9n8el{(M+#Xd+V(+}MqJ52!?eqpylY z!3?>UK)dEgbtNaZ1qEE8Dn51u3z=1xDJBgLl@h_UwWuEsbvO(wM|ot@>{~tvUy=QY zUIvM7WR4JeUs9n9wZ2m=dJ95|5!Cdu77^?QAuA=G260y2bJDa5_Yr#Nb#bL1$hNyh zDQLw-NZ{q5)^;~A=|KN<#LyUV2iXHmh}}k#v)dy(!{%V{u<9hkt&^ z`nA8*RQ&~1FXGvU8imgfod5^tML9kx=-~niD`!fOTCCZ`>Ghf?jqQ8Uh@_tPe4bDCOb!rIsMW?OpmY@SMlNEO4@}i?pzq$FKF-&J$qtQW%-7Mqy zH9VlPxd6RyGgiEgMn^Z=%WhPIxL$DYnK0y08i?Kw6JiSncZav9jJyaPD7=oou`E9J;I{i-Jj+0GVy83U9q~*H9#ZYwf%z>GuG93)a1ZZy zv)w74NnM%CD17Kq8j;ozntR`~Ag@0z82ZIK2@i?7Go}c%YbtP7n07wt21!cah`lu| zp)^KY9(j?KqN6FrT;?s(K}A7?ut zYuVA@M1!wy^M?rC;_AKQnqs0c&@hBOY)joXvoE%)>hk7E+h(Pf-pX4XiCt(bKqqjb zI}RaCc6!o`sobDa2P3_XE8-VFh!nRFD42d9kMlw)T|lo z5g!v?fIJrQ^z=yyFnN<@%NiXcu@g0N48@sHP%DTW_R? zBt#~}E8as7-f}=+Q2jnjhc)k|S#xK39T_rYNI}d${^-`dO5oyd8Hf&@p`@e@njEBj2O{BH!rkuJ zJHARHLZ+29GkvT_@iE+Pp`IbQWA8`(F&yo8@9>M{fYg=d%GQg*XlbHPwb#&z%dMz( zKxO_k>%;}kmM_PL6Y@Frs@!vf6vzqB?=TxActb(DRscru9PE61zF7@#XU)k2ePmrF zLQFbhD{g?2PURiyE5bcWVeH(5YsrI(kVmmV9M|pedyK^hVEboN)sMlD85@~A=ok9c61We=+DmnwmH|aQ5}=5666LNYVX|y1m$aZKFuCZYIkkj zosrg&w+Q!q|3V=G3~uxVahqPZh5GC8&faI5B6L8oIpbEuR}p z`yp$oMhC+?0^{sRtwo7T4eHV+&$hFx2^@eqV|kz`{>grn+P>;&LAl|_hEm-(32v01 zi$q_PSq|iVm45aZjR=|VbYcT-q^%cdo~U08b+@RU>Ffnzu#4}CCR$?|bs8Mj*K0M$ zF6}>lnxOU~|EH6*q0r|-8~8wt`|$4VMelrkaYLjPL)a@Txp7Np1qHmCt0Ls+MQDe& znK0lT>^ZduP*8{Kkwif8PpQU91yO`Z7(W2V!R~t=-}}wK9(%~uv_|eY2ELx=%X|5B z_!P-H%Wt{%F^zTkKsD~LIxJ2%I7sdFLM3Nr8(?8_mu!ixl~%z6ia$8piN<7|vmHil z`s!D2lZZ|M@mQ>n*#%)|x#61ZmBdS)pJss`r*tt7H}WFolqm3lw@M>WOH$VoSY-UG zL?^mLFNODXlZUY>Fb-qyqr0$D@|%dD|FD*kDUt>2h_==Xk3*Gs^0;1PmWLhQoV1ks ziJsH6o8lCVRI!W)Nsr1u;Zs$&?#wCPGTrw#(6)2iU9J)k?&IcnB;Vn3F6I#v`r%v; zX6MzODmL+^cyH{+>D4$1h2Z=0x0U1E+1*69jHmNYLD}x9+H-)l9$ajf?!WrI?&oG} zr_?eAG4<^OS<#DciLc_30q|2H32)D}>3I|`wCK{`_9B{8gaTx*BdPfM@dU@0niFlW zYC2y=y0GTe87Kk9V@*i$_dL>OHqrH^n3jfpC}yJbhxB=t_eq#Np3gRbOIaFUq)Fpb z)79l7N$#C~Ed`E*i~CHRBHfkMe_pQKdM5Pjvf>hS%d zsfo~-7&8N)bTH^nQz&B5#$HIs$`%%{?3QMsK>?OhZP<}2P!R|OWRz2q`Bq{eHCrdf zUyj1N0d80Kttgz)^Gi#Y@xnV=yxLWk&&LKOd3j^&c10ii7|vpNMSVdHamsD;Fe?dC zkI8pIX9L?GUGT0cKu-Zf;Q(Ue3nG3h&M_#Ryo zN**DKt4-?`s!{=iQd_$M(uN!ApdZMJ>PL#POB{%wM#hN$_22*Ja=yIRm4MV!BiX!+ zC=rA^`_)1V^8BzD0I6{K;ojQ3$?WVR^>JMhfQ16L)sQApM0M&V?1Rk2T$PsjphrpN zZo1fS+J!}u-Zs5NH71QcrA)T0F4Hx$E=$5}L`N&Gozz=wV0S@#a~?qqG;?%N_A@Ql zTAyphC{KI3mKp@Uj^`!dkM7G_@bt}!Nz_3PXKz(*0tG0iT-cSx#1q#+YSPu4j1eu@ zI<(SB+qh8fg~ej%i1&E2ba2q%T@hGhF9fI<)NUG;UVvQAFt%zDU5+317?9Q8H%nJn zTdNAgu>_i=F=p1;(*@p?yBKw~RU|6jyJoOp!x1UH-O#G`3iTQGly7Cpp`^swe3&|) z3X=bAFh#G81*|?JHPF{XI9<@8NXggJa7i|S2>+)WD3L3#NL@tMf5p)C<%Uninj9>C z^_Fk)zR;a%DtF>W|8mRvO{DS-Yx)*Mwq4VMMHuxD6^2|qBsNS|DUtgmH%Wm?+jE1X zO=;$BDqk67liHq81Xec(WI=^!BJC;Exw1r`*2 zt|5Z2AoLs@vGi=hy#*1r>0R8)Wi(;O=GB8glQGZx4eu4CF%)KrDX9 ztuo1vWRi)8Dr<;)uyJ(t8x8ahrA$L*Vvu95U>m%gyMbbB76~BvJ#FMSX{a>$^O!E& z;@r0-^O2|kR}I8Y>fM=d?7SBFSnH{|s1mTgy}zkT!E80bqq@+Ez0hdwgCufFFeQ;<@u$LVAC()xAa8;;#oN7+i zvj7H6XT$CVl7;ZONq}Y4IAC~F`=y_RMjcujW&S~p+Q?4$tpP@;|0pPnwvB#_qvEpx zDJg8R%BbxR$et*%8llBb2^7$-yeq~Dmg;ELiI{LA3oW9csOmdViiLzPenY~cYBa$?9T zFWu?LIjWi_5~bXBK{+%rn#$wlC_!<7FmL3oqR5LQpH**aZl>IU3j^xmlKx!Q)857G zft)$5YP;}JGTfd68phq4!EU=nBQX!$_oj@VYJU=o*>2WDtLYR&0V+N&v^c*$dit|w z&;#l{?Ct=h+<{N|@^Zs0+S1=dn-y48RLP@ue2%wl4cfg|=i&XYkjNWmUI&WSsUJot zm2W=H-(TwbAtkzMq^CyK_C~CJ{ehzFKV9`{1egi=V*$v78pYda^|y7*dl97`$$;{l zzpSjiiq5Ia%oC!EN=)rhh~tm?@@d0hQvA^)qxmU>UV;zH?{%P2v}p`^f%F5>mtB?XTP*U zfX%h|3oj<0_U||Djn*Ogj>af@u9U+)a@qy2JSH4<(>|h~On@$xf zr2W7>SXHc0&nwx6T$MRn@9|w8fo)oy)AcLzi$Dkg%K^ z95EcF=0R!@_Eswt2YDi~D{W;wZivg?Z0E!xANT;5?0}+(vMS-{HY*`0sKvj+4l&xw zN|zLs@a64_#I=)uOwsrzWJI!BSCA&6{PEi+#H9wP`!q^N#wI~Lbe&c+lA?jpIbK8~ zk;;uqiO+AIQ)9-6AzJ94}E&P^bMUs;!5XR}DV(R4qsGOtet z4Z$lV#5CxWkvjza|3ldS__J%oE^VsRbi$-)t75ZmviWpPip=fYqYZl11m7`d3U|nI z2r)d$Btou1R91s(=CJHMfRl-nEs`aIQ z47xL=SG{4cYw58ONB1@^%qs=eK`bTPb#_lf6YpjdXIp2!U&X1Fg;5Jg`G)gKhFQe1 zd%64Z-2dPL7p-k5B15}9G>AlnF>()B%aBmQmi>qE^mJwRw%vRw&E392t-N-}CU! z7&X5Xi;;l#?aOb~?}*LyCN4SL9T>L7`!PeAkYIP!8u)8*^L{I)NjuJdx7)OHs$W=l z{Pou3p+mcaWQk@;Op9%(@Nd?bAs!YA;qjlh{p=Ke={btS_~qp84>n~{o}X4I%9!e5 zJ}8+=eg~~Cn+***T84s@D`KJ;>OY@GDc|txQeOAwFsFvd&mNUhzK0#hkF-hagt&U} z%y2pYoZa~0q>2{0wd^p~kJb%UzX=ErG~RaQfP?FV_h>&L>O{&6(l9G8Y(i0Bh<*u@ z$#4zl18oH3lu6kk7oZ1wh+CkWJ>cYx=Z7HfuE_^ zlnT{A*p~Fw7HX1b&Z3lnY+aoRbQ{txBm&%~DFCe3zq1hoT1Bz@8*+TOWWI4ric)q| zo!C+2p(|dq{R5{}Du&$+*^P)HR;8B!IzZ~(KB_N0Ek2~*AZUqde(Ii6GH*X|T4g}O zizN?2%r=#w>QlZ*k*ir^6*7A`^QY-Z-kB~qXlR+TrB<~hk2H*xFa_$C*FW9bGIa@w zPpTV^M}AS_yY@=***g}{`vG8(+Wz2=jeLSnU>9YH`llDFP}}R$Vd$JqZ^{=|kKuG0 zvQNC5%Zhy#3iE7KoGjrkfx32T|76_Jki11Iw6`N)** z&UEt23uR=B6qHHE`ZiCy=zH-qf)V&S-@^td1UHeY6Nk6AEP#5A>}sYbcfHxDhk8t6 z4>y(8H}4iX&T=?~Z|67jENLJh*8%Q4NO+-;kc;`gnIIA%NiVIM&WMRlnE>p`nj^i2 z#W0CI^?6V+p3s~-FOvL;g$^ICX}Kp&IBcv}sf3-$$F`JP_hhmzX7BC5ByCWaX``N2 z^p)(pK`t2AF)sg_+J@@{#8fncd_o6R-Thsx=$QlDuDZKx%VNqxMDhRBL~U!1amC)ltZDe|-{P4j8H zIStlq8%(6CS;#ww#(_?d^^-~6uQqC@THAQ&iYC3L+vHI_|MOcD#mNp-3Yn{iZy06F zh6pzsjD(`vfBM~|_?$4boyO+NH zx6NT9AO=ANOP3N~Z1NKAIivJA)KN_SgvZxIi8#KPIS^uped_7fb(u|C;I=n-e*1TW>%SIez z+z#?1T10!s>tg$PI$Q2xZyAHO>@1002PfwZfi)<&LqTE61wEx2(KvirQ|shX58OLSCL$dcPUFb!h9 z$Zp7f_&}q_Z6^B~+%$D)$Yq*fQunth)MWbAf^!zV76*H3^$hIvvm`{;u z-Ix6YbaRn_tSjew+@pK(IKMo$rqAPf_WwK2a{(`&41cDXrYc9R^D)y< zGbqbst4|@lV*e=jjZ((is=|ly2J#J8WE29O!R$o;O1DJb=4S-U9}rZ$E>WGnUut#y zj0IHk*SUNBn*R1_H(tN%5vLlbU2-Vofp-44ppw^`rrt3){9g)wITF?J++~s5Rpz}L zU#|Fy@pqEu#6-WzGj&7_g-ZjpkOkXrRfF)${e;e zH{J)*mFgd)yK7!)^e&tMYWXS6Ukm@Ua;elAW4_PU6wS*!i-Di<)F&y?c5|irGKe+c z%SPoD{~~3ibtsTFCi0R;9`Ed6d>;f9MphE=Vl?{i<`PuHspesOPDx2-MdmCR6@MyJ zvX!hJ!qUvM+I*p$pTZKt!54Vl#^i$fv)^oX*y!!xDzusZG@$pBCw57wIxMjZT7%40r)-7491bIWX)v9msX)uR= zVpbuIX;<3ZW?a$YAA0*y^#hdV?=87>sOgm^-W@j3>b&)+zE4X)N5v_)Lp3F%gVrn6 ztrWNFa#3l(uGCAhUVm^mNg1k_C%u6-EUi$xFPc0$9m)UycBxTXVsfy(U6fS)+!HFv zM+8c?e%MQDtn}=cM&8(}q5im=A4?PVrwHaCFheTnuiHIoC^&(>^W4A|MWbe9Y__8e zkrrJKFn~I$QsxS9JE;nnjVAX`3B%|e2GB2;eYYDjkEg~^Q~F;S75>q*>G8{>qXNN% z^&6PVNGRufZrc>Rs-a+fS8l^?(~BPu3dyON%q9J8E>1X(PLpDW!t{CGEiQOdSU`6U znq_81B4N{EX4>~_PE~bf=7{d+UND39c??YUrWB-)_g2pi#+6FboY12WvjvOhkth=$ z6l7_Irv{C$5PSep_@6%$b2Ivj19g|fq>RdF>Z_H*fj3x{waDG?ROPoBS}^0o*0ey=w{u$TOFKh{q_@QRP# zN)DX>A{bJEYmq%hDBf;+1IYd1o+&c|;PCF3L2v_@TV11JWo^&6kKkqJ0QqXWN7J!V%3?7G{$ zOhL5ywZMT~1iSCmoE*dA&kRpg|EdAwhfNGMTJ6H9Bcal8?d{W@kPRpZzOTBph z@FpQ@M~KSU|ATtUHlnCE0O6kO7t}>~usAc-&okZ05c$;l;+*8xKhl8#Rb%m(E(3WQ3Ctn-63+$UrB^&ZFpI7RC9Ir0& zwjy;d#I@Bs5EjJd^37JPR>&e1*9D7VL4uIZbKR@|C>|z%h%3q$$lKO}%l}Sl!JBDG zKbXrd#7?_ATtGU7v-u zwfg@r`%5S^t)uWSl6#QfT+Iuy&s{h<+h61v`S^6=#@b)w_ZCYMX#9)n=5dpMsKt~m z`LIiP0*2SP9H#58#22#IpN1qFh+5cOlw_7*^jT!qP~u@Qy1W;+-gN|n&R`uH%{}~N z&E4v6y&2m;UICVYqpl93^2>NTx7|4PT;6ir^BmKX$eD0zZ%2Sdvk#XSgT6#}ukF1O zW*Am}@zgbDVd1tBN9)GA-~5bs#?)!sV34uu)Fk^kUrsTg&^-zxzhM#`NKiYKW%cdd5wrTa8=4?Re z{W?)Jd^~-+Q)ycDMx#g1>ym>nq{$$qxjo-Y2y=QS)Gl=Tjg?fdzwfFl`uVZrpta%u zuN6KZ&`1;|Ag8ip=^4M-zARL0KxKDwN%b7&Tys-A4kHX;`;-`#@M8} zTNFOhEN4|C!dHz%Z>*Rp=Ub1rt@LQQ3VZQWm|#m)3bB5XBtnOi@wwB$vb87CO>fp* z5p9&sqrGd5{(2dx{$_+P(E}vb2cfsa`;d1M{Ea29N4 zg?N0AVBdZc?LEg$u$)8VTNwAZboMH1xO)%W*K|*&*PTpBq6I0nyokskpW$?pl>7FV zb9Sv5-`begHrkotAw`S3dQx`%TEa>XK7?$gb ztReh5M{9L8hr=g+h8p@<8)1Wyd7P8|F_gwGQ5<8PKzNvWhHpd5s+MpfOjQ+;HNO<2 zJfi`$o&^t0c#-_^8cz2z)L=4iojcCk%7C*}H>$>8NC2bOOJVaB!^ z-}9DtQCYJCS}(8JZ=Sasfu?yO4yVa52ky?L$(y@2y78;0I1|+v9=e`c^AzgOnYjAB zD4aIp*S_`?s(OzX4GVxxL&Ke#vXN%ef!|=;fH}n)m)r@h)L-}yu`1e4pR7n?&A6Q? z{=}M4F!|t3?Db8}#bxl6TC*%BQ`6AW1!B5z%V zXiC}9AABh}d%~o!IBn`Q?}IlU?vMB=U^>U9Ge_dX5*ONy>H=?rw)5%JunmTEOfb+a zf~+4Nyks8u-^6IZQg5P{p7lzCb82ldOtJy4`R(2}LTRm`;Erhn0QYpmS$MIb`8oCa)i?74AGfwsJkN)&50Z49TuR% zx6}mZuV>T~z*7wCKkQ3=380bSZ>pMR*D*v5bTCG9NOJV!5&vp%7UfcS1gA_7U-oW- z`{|5&$UP<4>Y-Q+xArp`gYTDy;L$SGFC9)_1%M9+GD5)j`1W|nQ*^?)ETrf`^c>Y( z^y;~%BMnct6B;~qn$vY-@h@=rz`tT+rh#pPAm?-rQs=JM5?xHG7QDYf5U$?9C86bk zc_oeS6DjDh(q!ZeEJ|L1rb=_ZdEVFD>vj}1BnNy*aFK2qhowlid%&JiA5u{71lw+9 zh@IY1Tnp6e0-Uj1w~CR8pc95r_VU_%#)CruQfK}5p&c(Bq! z`$IeY8A4pxuA}AeeNUVxevp89w;V(z3#X^los_+e=3KDZ^A$+@*q0mC++){R)D8&k z&28$ae6Zc=956b$P7^qu33s#6;Gf`9VzK1rz7#8+AyDCuhdzYV0HFr4%3P?jfa`Xd zbns;&dk!56qBfZ@e1~(Jba!;tmcE<`vis`4e1wY9%3@+#>$+fBLmbqLh*Pj8r(000 z_qB|9y|99399e<>3Ok{n`hI!dV|9x0BWFjUkGuW%>nZ%2Y zghwhrd{u5BkE^QG@@santY1Y$;$*pqZ?H*Bt%#*sn6#BCDgF85pq zcyLp#h92Uj_<@&W;7X+tOzUf#4hXa#xtci(0jvsq-uW#Vzi6Gj50#*U0Jhbo@qOu4WP| znTh6zZOqF-63T`0K@UuR>hVVEyKH+jj58!(Y1mL`Z`XAen{YbY{kuTF7lI1?&2>DV z29!u#$v=(QU(2@3%j&sRb*BVl{<`!~1cKrq_H)^%B~!2-clLs0bLn4?Z7$KxKIlDO zPt~fvd^JV0-j%dP#16z?X?8OHyPNLsV)DoPUAx4W3YB`)jyhACDYXRIhTZ~@=@CqN zBdAgE7;+Ym-MiiPnzq~_1W00@!G+;?!_asvXtA>@>N;N&;E8{oIUG-Qmwqo1j@yH*JW>&7W#c z_i`;7J?+wV!KNvg6h_TSV5G$6Pp}G>9a7mEBjkqhxf1S(iqZuL&?SL>Y|op#~!C8(bW&hcc8IhP?J%|;=8^Kh%x{Z>JW<)`cDTNIpP1Z5b6%H< z1@b~)*r|}YgV8p^5%aCt+Dsi$G8vYB%S_|CaxEIZ+$Ozd@UEtmQp1Y|VT~FA4Jfz$ z0~6hcjSZcLl`mh~x8)^fBk1{S(MZl%F@+6|yJVeJe+68c%;R5oY1KK{)c}A7jL1c^ zfNPyJ^I2|#$7?T|qc9kbW~!6mp`;>(n3+x{CK-A=+syA@dY$|Rn21#=#?U-TQe=c| z0go{*GCanScTbGk)E&EamRAFhg%x=vsBt1nRsZ3B^K)3<&?iB?6vr_Ci~!Kp7dd+@Sgn4@NnZF7Xa=L5>_rt)Mpo?c5DRLa31KR7?cox*UUoJV|k5)r49;8?@AG z4xJv!H#gCa;0a?Q@+=GCo22(vteZJgVbihuppPyUBQ1Y6Gly?)?GVA)SuWw)qR1`w zRFT?*8)USo$UQ%+!El_5wbxs{->YS{&@7oz<_k68?oKv5hnwird!I|T2^hsuv_P@=7`@%1?bGG9dGjQ*4NV7 znpEODbNI zzYcLg{7C}QYw{El_36JH(*4~oiN`Y90mO}!5KRGV{#jDSC?XcmOS=dhc_wc=&NFuE zcr>Mj`Yq)X0qjfCd172DlMBg?2BR*tZ&`{z59wNFvni}I$<4}P-0S|Nkpakj+})k$ zrG)IKV>8}iU!MnSRtFUCBYdvwj)>~hmy?+`{>2YLA?6n7VnuPD@dGXC2~FJV63s+d z12|Gr*|y)sd_)aRhL@sCp=m{LZebYx?QAFa4?!~EU;@Q;IIs-Yv2$^eTF@sD6hBW3 zp&6502HfN=6t@^;vVJ4s{p(vdfp`nT;##`{JUQ7*t-nbQj6 z3&jSs*}^P<{Xia=$NDIb8t06A)l2;NCPE%HSp6f}TP|ul5TN8b8($r!;+ww?EY3n? z2S<&VavxoG9*6ROUWQ%0E07(lf1C>bNoBH~oQ#YqjAp438dJ=0Nck-mf`vqy-D*BI z8E)-07qG_D1S`E@&$c{2C82!)!g3*o94{x~JD8cm4IE*y&&X%B(my6O^uN`*`1#iJ zoIj<*t&;QG3}NEzvA9M~l1`IEO90ZjZ2R^->sln3Z-Nn*ljrrGq3ri7ch_=oO-})bZIGnHT`&vWCsw3bW)SbwMmh5LQQ1Vcyo{|v& z$erv-X9*W}mhC^{3A?^paR<=gN>?~1f?bwQz(`x-BM0IsNrSdFI;*y!fuT)`ZrA^F z96y$RF2%x(*tUn(7^<1fcy|IkfT4P&7mMda1OAEE%kNhz3ihhW9pPJ=P6&I2xbrwD zFkG`OL;XP0Ox!X!OPC#K7Ui0v>W1A!3E0GUm3I?P(ziK!zj#$w^Rc*x)H72FTJrM$ z<7wh3@mI{<^iRYk%LMA^C;lATD zo?6$am(Y8OI;u%gA0_LeJX7=RoI}=&^76F17-S_D4Q6Ic@^a|jh4}VBDX#y~U?1Pj zR>en=)%;}1t>I+;Qwioc4*}Tq$D8*iPiXY34|UVw{l@;tzDJ2ROWxP6(nzoW9FQVL ztS&<~m*Zzfi(Rw9#Y6X+11H7t(2E6bYL1#FtAuF1hsuj}_PKtntW@aibKj*!FNkVWqgc@z=*HIwS2_y~WJMu=D+%$8|`||TSMW5=!KN*U# zZTBFbpcAKre1_!W607m+n-V|S6OFrE680if$fTPDtyQFL_>0X~ysMkz4Wu_suF{WN z_k`e?$4mKiAdJFB*d?{o+)~O>nZz5Ymby*^4xBxCb#rvfO-4B16R zk zV>e*62Rv_QEZq+*a5#2J`c&WYFmv(g_F2|)sj5s263s#?P;50E9PGYQW6UW=QN!9* zH~QmxJ#|6f_c@Rl7dEJ0$tnJgL?4xp!d%xXgbcgpZ-;8^xnAO=^T_mp_Z!NiCsBSGpoQf5}V z1>C_OyG1;ortEHD%Wsk}&;Vc!>%%s6DEe_^NUju&dPd zA*$do{CjgD+9A7SzM*JUk{=K7h6)2poX~`+9I6SHyXC zVG)WRh|+RgQo{hNVX=KRF*_p`hhYYyq}`=`H=|Kt&wuVE0u!!dO*F^0jIL6rSjJ~k z9_y>vTG^_@UqMIf^vJv~(2}hI&B~(_mxsETaqnueu@JRiAjCr;%C%935T}R_NO24{ zAHa!8F*7*bTD?P_D|&|&YMpx{gaPj{yEDepj2oz=BlNOI>@7BM9B z(FN{8pcQASJk|w=!J-v*f^iVhUIGK^O4e)fblI!ErWgc<=HWHssQXi|tjXXcBpn;G zn)$|DL^da1sdE9@Z>|)*d1_v1)ODDY`LT5M0)c>HQ}Q|@+$2Q3jmw7JxB9~YpQ?AF zbXB(N^dWbRfbcb_L7vIlhha~jfMRT!n!jI+<20HHx3e)(4eON47bZRk&X;`-`Q*rA z0D99QQ`bv9j2WUn6n9ufs90`w{`myBD0A8W6u9r;v2}c>)AJi9g%(;0p;-AW*vDN` zd7$ojBEFiE{kVDjkN>+~^fh1MHOk)Y>FJZC z?9mFAwZ@pfDwSZvGDzzA3cGo=2G8hg;nO?@PgO7IOrgm0A;UBTukCTW`JNfg0tn&b zztzCj5GAxFI>YOcW~QeBMMg(q@9?Mxb#Q=MdM3wCYQ7&w&nOR!{1M)qAh=`l(9Fn1 zJf&8nPhXIKg+e6$4(na=I&mJJ6muFGysU`3F&d1K4;Pxb3|(Klfq~krdaTwv9bSyE z`Zl^8z8s@umBDknIRc}FfJO)E>DCMTU+NCL4fb?Js@ZaIL* z;9QhCl2M$nIH*TVgxQwEzSgYaNZZKzN?)V7Bh}|^NSF;Ao?(+6$FwRVi_aH%-}0yX zP}c`%YMy?%N6&!|J6r?|eUM+L*lVruu2%s=$`aixdmf{CAg4^A0k|T9fhp zo{}^a|K$^E)z@b)-InO!2{;x;n}Wq0UsC|{VkwKC&l(W83-IfNa-zo9`bt2q5+KliX(@?v60fPia8_{?AuaL^P4qP&E;a@nJPl3qgkH)HZp zQ%CjJyD_*drnpdvzci@ZIq!XMC?^|-PV6$L;Y|@C$U1c`AG9$EsLe--@0ogoExK9R z`EC3rPF7yNPa_+1TBBlm@pO7d#<-!m=;ja{!>;U%zkiauT;%X6e3UA6_0Rm+xZw`w z0Znoqf(2<{RXlP4!MCMhf)tBeo4x`3j0}y%X?UZ}e|p^;B4uKFr&8b^>VMuEFU1i( zD@d_@k>ubNT&x%(Ln`Eoo0mN~Z&0-4rrfWp!UxH$gXeJ=%`LRa8gXTKd-rAK`$i#_ zjk&kGhzWc_Ip=;F>;8LY4&khC6bwyoLX?Kq5|%k3YivqHL9Flc{#0Y&PL!1v?1hQW z@djUrOCa@8pX<*;7|7{Hr%_kWY4JJLIAXuQEN-fHQ>e~*8d=B~s_@S>zjSv9ag@(% z)qoSRIC<=p!WbZ7bs_;X4Oi)y6D8PebL*w{eD?dzf2}1QOqw)e70jg*z^*9?{5qT} zx^dGEZYZ&}8>9wnlYfRE>XWSL(+d$0vLbF(&4u?pHMOs?H9 znzi33S{LekQ}vvDa4H_muRwsH&cloe!L_c-w^7Nl&Z=5(^@CWHV5kwgcA@FfdKCgz zY)dfXLvAz-hCyaW=QdMDR|ab-#wPF#SqOAfep&%DQO?pHL*+vK?{{}I=wv@Yrrk*_ zzFL#_)99A)LPuozYcyOW{%wg$rh6GgVV+LvE9#LJ3nWi3ifvEhdSrVN#gxv<17SR+ zUX9V<%xmv&$`7ajhxiAx($r@&Fm>tVaqz3WUa^W?6Exvbo$$FD#4vqn;E zn@=5QeDFgwsqHX*77+3SMbgQ-RE@i3f%i?0fL238<`Bh(^Vz*>X51FMZ(8@B2yhEh z!~i#8>j7p2j#(%78IE~9F%Raq@v3Z^_gj4u(d(nMBudTu@obLD0#q4*3l0)KHHite2FhxyW0(Yf`O?jDS##y(=-!HrR z7rQUvZ4Q;DH7#+)yP1y9Xh;bh*u)FUZ;3pO#OIWM}ektcW0g$f;dWI|{{R1#( zWe+BX|AD)vtVlXKO0t3i{5O`8K3P;gbpN?l;%6{}ukGDz63wZWlS&93kuVImq2_r; zY1&DUcRd96?Cgm_eHm%tC#^nL=T8d!?Uu${<*P%*L`(`hzhAfxmLL<(pLIy-Q1iWU zh1#nfJHhxoah^Ixm$3CV34{Z;K^7CV6Mk*=Hc?aCz6){;CBv>wK)nh>2!w|7(BOf; z9X&A?PBpC;zlkB+C3_<4J>U@9fgu~Co<qy@{g zBaid^c9B2gVsDI07w4WAv5i9;?NFIOHHIy}?4L%Yj?t(12^B;ZG}#+y%1yML3lwj( z`@bsp$I9FDv}AOQ6_aG6#FPVQoI*|DS!Y1sVM0NA>vx@j_|u*9^}WU z3w6zEyevV_n(V|ki5ej|(n~F?#vk?9=fEL?(0@|vAYY5Jp3(`Lah@YnnLJ7BH!Nv1 zUFw};0Q*^1T6n4Je+dbz`xcJK?`L#t`gY+koD!%-F7az6ooV?jaDi63?Z8*bRVn(Vu>sn9Y4dd;SN>V|N$zFwxiwW=j4v`zlBHAn0$#r(5J z0(e*On7#IcR9VkPr zK$sWPo!*8{B(MuB>$B?slF%NRUV21PI-;K_mxjX~#@G>mEQzt|t@fA2mhFOOg>F#m zam2k-Kk2{xZbwA*2h4AK2$VBoLfx>WD0kMV-dd2#{~@!`j`ck^fh}vg=DgIxO8|y~ zebG&&n)$6Tw2n?DBB6qL^vs(EK0Q^UZ1qg7Fayr zSe`QuC|eH>|kenPK1aTg&irQop99kJ2f(Rq#2mt=(^pA>f*uBDurX zANj*AvRcEN+wWgynh2m{xS0IFt1!TadmqjnNY%#F)yYXVQ z*Pfx&-c|Sr{)#stK0{WPBfOuwxoQ5?VAdp6>hnI&RNN28goLf|+pUvqB)0N$%+ZAZ#@(vs z-VV59r6w;WO{hDj}Nw+xqa#~H?&y(zv+Iw87q!zaVqTW$Ll2mGD zO}L&TPBZE0H96|9{;Y%ArdXY6v2>EPBc+lw{(jj9=%%k5|1MUxkQf4lg9+6+c0^TZbCdHXOSjd?U*P_l zt9;TLp_T}G4`h~vlxm95xTje|cj-rYgMe9Ka$%p!6q=XA#Jfmb*unLI-fYoaI@X&D zVps4}!UMW7GjnqWR%M1q+JS{C3VlumLxp5wL=3BT$V#n=wIuT*So_9oBsxYBMM9iH~r zACx5UO>=;c0x0`ttyrHl{N;bZQ)cbrC6nB!6>_p&T#JA`G z@N>erAdDV(ttl^NewUsgHCWd#I8qVxRy9V`@i&?SQJ-tMgmtMLC#*Q|T%!(#ANYQ^ zw#rQ#PJ@6{z3LDd(F==tX}kcbU@Z{fpWuN8OAR25Pj%UWE474^>G?PMdA4VHXxRPX zJAJ^=wz@h3B^sOslIU-3R#KL9#MGx!2=#-$Ux1D(hz9zom&$z`8@P&Ef1Q!p|I66B zEH{#*=Yo5G3e-+ALvl5%xkOWmfbGi_SYf<%yvP5?n6fMlUp=N-;Q$826_ zUgSK<_;>k#_W-kI#!P1FN|Hca+<*Ts--U1dCBTzCc5xNs^Yk>yfQKlzW#i%ixK#H* zle-KX>P}Y8B!XaLJyDS+>6qKB=FsR*FyRLrl6qcW5_g@9c@I0|F~85J{Vtk&RLtG8 z^Mlc6Fh(fYBxj|STlJHxbN=bdJ3_hX8*cmDz+C-NlJ+n46FKFH?4*REJG&GM>J@PU z&@2;{&qBAF1eiFyAEKt?sMl#wO*$^@^Gi-)JiT1qd8el7fy?Zha;u&S@Fn*_iNBnl zsBu9py1}M*csQbHlF}@uPZPZI6NlwToSArqb#K&jK--b8x3y12V(gob;wcC9qdu=D z7cm35?AqPnY|$u?Gm>JILU_hzg2R-CP!K_*MKq;=-^TGt33P$h6WG4Ei*3)@z7-i9 zPDWe@lXnSAaEic|fVWH>X+%lz9`6sHzwNeZ5$H&@TL>OjlE5ZE%5BSEZR2_WzxbrF z4g3X=`Co&AL?L0GhvTn6xpcPdor`F1<<4Qxb?i^Y0=1Jwdn;|cZ*d%eESzTjaS?RC zpgNJvQ6jjf>aGq9khQViL%!gKk)4A{pP|K7BfE;q0}{k1wcij@YoiMc5h<-D*klM8dJ0(M(W_t+`lacuJ_NNc*OMIoJT*sK_cJ>JX?gid zoNj|hHt+X!E5pc(|1WXHaAUvem>;ff^Y!oDc+y4+>s##E?;{^D@G8YbO8&~_%)C9` zVYb1K(uF?wlYtY=}j)*Vu7j(Z1*Gj(Uh1=+UrV8U(4BFtU-G zUaH}M)KaCgtQ}y<72}Hw&dVE8!+NSq`G?P)lRIk`DJd?N5N__$dgLH5?R9J{RAKw| zqnd512LhJa0c!YTS{HB%wD||_bGEPkk%bl}?lp5f2BN*2Tw|;jo?Wt_WdGj=LxZgo z-kV+=%^go6bKs$DMf-_!(7->>JF4OHffDP#pSvF&C6vp<+b;yS`kgN z7kfZrBL`?FF!hy|p;>F^2*WF8rU$eY*2Jli*%%Px7``qE*M*f$u+~IBVeY!9F}i>L z)NwE}Kpdm9Zm*OYr_qONU0w`gpTnlM+Y$C6d-nLv$}U|f?ufD|ibygUZ~G*j9Y71+ zyBTjrGT%sQtp(z-I=x!-gu}HO%ODgHL$)6C*rnuz6)WKCj4^POG8?At64!gwjv9ez z;nIP-o;CM{qhjPcSd;P4+Q4(;1xWKq#6!+^B~US|j`Oex|Z;l0&H#ul8&{lxVWDKI)?+!|&N5eNC4QJ|Lv}3%y>V zq*@H;Vr_e#{__Ihi^i8nwf8l6jCz05`0Vr-w;bK??@8$3i0TzT zu+ey!7M~1Eo)_S5UAo}f6dR9dSJf@^H0q2Fc=)JooIJ9(=`8;CDD%> zg9eFZepkAFbUoC`PokBm;x#nHTR0GC!) z=@vaexpXp5t_c$WF)p*Cv$+1}*$9EIBfM$MH#2fNTxWKWy9whX=<_mu(G~vN+)?S< zD_=7d!^|piPJJ@u0YMOG@Gsvc2)0fgnkXdxCI!OdN{8lge_xQ^*;GVhy%d6SbXDUir+^mTX-FoVX; zZ}oJDE{F%QODErl{JF+Yo)OdQtSAq)L+E6vl^|ygX=QsZ-4G9TuG=D=SSQcc_KB)`fH&IjuR8EL)lUm1@gk_wQ2cqo z`t1Z5Tc1LfYVI;Eq(RRTBp;@QLJp$jPCC=>Wwi31dvT=q#;+biM_v*k-iN`uuW| z>~c?f7oGxl^!K5;Moov%!7Se9#4tfVKRbB)*qx_tE z^p%^UkulYeh}uEidztK3PB0fQO$YTY1ykw|s&Ur6|~5i-H%7#l#hBh4D68 zgk1uZR^TnNf=^Lv?z8ARu~A>r7{|!v!dxLdUxS$ zCf$5hGot9=uW7(B*+GDt&8^QFk=MYM`I02?vNW27%kqHwMz{Y*1q9hKYhE zx{QpfyZfi!T~wW5eY%oMb?J`3+LvjkIs0*D>2bIDGac$;JJT;++Xx4e<)Z6FyV3i+ z`O@0Ct5!Jhx%O1b3Z+M7qWYC?7J{%sgWANhJ%FY*!Ho^z$bCL^Q+!zgd4;7`tS}(PUGn^!R4Sy$uCVtgN28mpzUkpUU!#sqTQoo z!4YF;c@P5>3KlDQ3n`o=JQ;1{Nsa^TutZuX6PHdojplAQVh+<;trnbt0TFu|bF9$s z11H?y#!&(4ibSaS zU7O@S${4;YmDH78FUrd7IMP4iW%oQ zrn^mIq;ILR`|6&m9o%lT#XESPEMwL->#{CDlanx)(E>$F=W8JaaS*Oj5yV{TDbA?d2 zitTXgy=I^)(pSQhsW*G%ZzBGGOcmEt=e77vK3&&AHy&@B)3ig=DY_MbHA?+ zXrmB%yPPc?5fbSM)$aZv+=cu8t!n1T#t%)^`0^&i1XeD>uGwFjT|JL>sA(8a|1n}; zhqSSot)k>l;|EpQpjKOjX&YguaXZld38e~(n%uXZll!&;9&x(zcx7__Bxk5_$F;|z zV`u4*Bd<*da&^My&@n*UiJDSoK~a^^nv10>CRO^&tUULI)5`_R>hk7U18odv3H|)6 zLw9F^5KD@m(2S3(Ov{#p#j8IhQ8Xj$o3Sh+^rs_r7Iz(U?beg8*N810SyUtk#{bYL z!Vt3w=`d4E?q&y1dA^*ubrs1z2jt6gbA}xTI&ny=tJB%o8Iwp5ZHTc2M8;RYZCoSb z^f_KuI7wuVo+St8)48#Yi_F~i4Sk#}tc3qhPJW8(-Xzko!3T?5E&(4H=)~q(7ED5k z&0{Mh6Fu%$zQC9Hn2@~}gG>)uhr01VsMk6D&v`qRcqUu<@wz&}R=(UnT~5n64n5P{ z8rxzvr9kd^H9)p#6f5@boJ)gY(i*IocuK&ZcWj@N9No+*@HEE=Ou!+8ALd`*Hr6vOVy z?VYOg#TG$CjgaVTexuMsWWzH8Q^8TbO?yE$4<+Q(np%B(j3N&_l9>8nK5=J*h#_@vxu@-IX1__osn-(`T(>e6a|zJupk|HPUqSMA zlfUf(r}Gk2JGbDdB(GDaeanaI202rwnlsb)g)C zs-;%3hU`tC-^%(8p3ZEVLQnfnKcuZj9)mTW18p)sU-s$i=89lUDXchV8MN)nL=Z3- zD$DuS4;cY|O6wzsa8F_Gp84>AirlEQL5(p)O*aN+ys|iAgMm??qAgn<691Y;Nl$nl$E!=NL z`o3YrfaZ~gj!w%u74X8NtHZ}6E^qDS_&pOqI&NVwf5FuLSkI)p&7YrxSQNO{{NOd% zDvNFvfUFWMoyb za4fW(Nu3R-qA`|=x5WD0nG;?Z{c5*?-WFUK@ltv@Pk?rR7bTUTptn7kUuS}hj zI1-{p*2--*qpI^c$*P%qmfys8KX-Cbq1P9cx92fYUhqsX!X%V9tUE0gib<>;3&Wpw zWD2Nvw_S<(7h_p_xrH(09(U}~hz;6wddGA?Bpnw}B-~FI2)1j{o!!Q;$YgV(7MV`_ zCC}Y0Lwr%3qSwpwu;`{*kT)~HfLXCI1@s9H6r~t(m?UrnYmf|+#1ixFn}~F7#a^u( zhuoLYHp##v!fCi3$SDMHq2}?|F0J%I0bERJ6^NPmd)0qy=yS3(>zSgg2MxYKm=B4< z$W3dz0)sWlmOz}x)A;i%xAqSkDxhZH{;rKRHh-G_{`YG#6@@m&k=TjYs*{4TvVrxg ziFu6uVaBGQH7E1*kmqZj$13rQJQhkC7sTN)l^FH zQiTvF)X0s}CM8Y*0vp~@V@Rtl@_Tz4agsig27}ZC9+|b2ny+C#I<`>mf=y!L3YMh9 zb<4@AwT%m_RV~Kjl~kE}T(FaDZf{*Zi;;IH&jg(VQbQV}?Q-k3E~Mo_X6qtI6P;r8 zPrb2+x^f;YE-4NeGO&{FH%GS386i88W25>6z>(h>I>>LF?Uat1Kz^!*t08z72abmF z3JO$MmNE)Qvao1A(VFydZTcM9&ej*QM@D5*bPbJkoISe0Glx4 zPxU+l=WltPPTjN|0riF8zZ!x~Hyt?X+UgOt`SotvH8%p%Z$30mB2nTzPs~RFWeo9V z@b$AP@ob1X^uyUI|7>A#nblm}BklMOoD{R_kg-smCHxV>wCRF07H^#=KFRq_g#Y1` z*qUNlPyuQhOT2s94e3vpb=&WG=X9n?L3VW&=GIpN5!i|umg|jp*FO;DtF!PQ)e$^< zN;SaKk=tjeah9dBRW>y^zc;34-;m058VG1;tgO0K#gPh{g0x3c1x{f3l-q%|;y(;loq3==LN5pS z8P(;C=JyLw$0A_BwJa>ua4K+_-EIDek+^|_y6{YH7AgTgVP5%|IW{6>ygW&pM?Ua^HZupcnK57DIGqG1yjMH%PT zh_It%#{^qzr)WBO*P#4z*-exrlAr60zF<^Pl9Lb0hwTk%gFY$M`c|vBr%B}t1Ku`d zDT=IyxjC3!9di1+&|&uLN&%F5k_sEnL5b!OdmxJxrx;L5kIN4}I* zb`^LqLQ*1?(|GnB`Mi<{jdY$6K(%(HMc}iTI^3A_Vs)Gqutuc?4S!n?X0?s@;uJM& zm^0$@A8J_bU<6FD7#%q+u5o-Zq4*!SsrgU)b~Y!k#d;9Y?olJi#b3k=0cGSt7Ruv2 zcaHa^G0YO=v_qnezi)2o1%!^q2wuhOyuTJf%8}nKLc)Fe#Kp7L9Z%0mIo~K+2D9c- zI;;W4<`Zy0NnJ-MQq2rvAGS9mMR~S2lOIl;wtQ}rETbk@g-|F0ONnM4@MtviG}fcj zy+?W1cPl8B9ys9xp%hgrW?{i&uAxA~ho{gsO;+AX2aSDiJ&& zJ9ddL(;#~jMXpxWR$_Uoc2}wzk+NJh^%d@1n?M7+>ZDq5wZN+*z54hW389(gs77gznLmE@$cT^Tf{&b!uB+^>yLe);l%- z*B#)19rT^xy54y@;Ks~Cuz6K4-VnYDZ)%+2&$y!ODdt{=)5S~&C5VHpWnPB81@x7I z-XP9W$DIoUEf7ur(CMxR3hjX>Xs;_FDg8vPAj%Wk*i%k3<|$ighkKaj}HOsZ4-*64(nG{ePW!Wo@G@zoo zNwHkhml|FFO_sN(l^^PD?rRltrrR}{IwUn&P&s`tQ(B#`sM^Km#?nVgO`sraxpUCHLP{=+!8a`uPOA_fc9K6XNM3>c;_%?m+NxOQ>!Vml@B^|5Y}@9zY3Er7o_WV1%OIp&RcVh&emUu zqT-Wma~pGiv20Qa;}Kd$&$N_r3e5n%n?ruLudssF$Y0bx%){jl?0mQ6eW5JKwyp9O zd^fd}l)k(B87hITQ!Z<+7b{mX6$Q}3LQt5z&>VHfkr6=tEL(~T1uPjR8W_~oJ6f8l zAHnhv^N75}iLm7wv;a#}eOYVgfwD)n?3n&12jC=4jUndrm!;VGx$R>O1!QfEAP_x^;imcG%64iLd)cspzHdy9VUpOcBl4FJ*lOS@@h|7f?ZYc8Wm4EJ!It zdK*v)w*LOH$ccx8eFn;OKcs0&dKEs`gX%Kw&$O$tHCj`{_)-`GdjRE1;jw)lUxrgfbj291ANXLxM zPc`D;N4?E;nb{^(t6mdhMqGx=z|13AN61i7+JL0Dqd=kda6edRQ55g_M6crUIaHlL zfBN+0+%)^!^NYzZyUH~Up_Dw4EX@zeog!io=g#CW7d9igdQps+<-M1%ots3iJ-E|k znsI9XNJF zR4|PxQ6#PBw*qis?YsdSl;`n)LX%0t8Cym;dR7vpPqbI5K|rVY0M-98Xt}R>Sv@p(Pain$PT&ymB)8{TyQRyT#Vy ztAaaNVy$QodFw8l^~BOh3#Az?mb)<}>Cn?u6SL7F)y+)-Mmo_(6yQk0plU*IM-ncc zo>(NL(z?pHH@*|4;z9t|la(=USsO+7W>%K7A@@4XW-9Ucw zEFojLYk#fF^pZuZMY2{MjaI#LTW210o9d&1_Xrodb3HZ&X1dIA31=TqKW_d>&G*JL zHbt7^Y<^>LDzSMS@3?*f1lah(9yJOcHrlH|7h;BX>FvHHig>BPg$KQjU{B`{;_R(+ z6yj_@w1cjBEv1<0@ctFV%kolJ^8jxeE zd3A({fDDy184$J=?wCXfa!aN|Y>4B_ka_<|_S+Agz;H1Q6R#6d(&8gMo-vF8hW8zD zzyetwJw{#5H3sj;LT1gmvz{8G)68@-l_;R2HP1&o8?_|^V5{)Z^L%O$-&#gIeFcT! zP2E(@_kv+f7DGL~?Eo(syV4^;5>NR_MTkk=A#sQeH3#YM*s%1|u^=6dV*1#5z|>8o zu0#?;0BX?re~cD=_gd2C%7d}c?-?hrsb3JyS6=upabt%{+>tUcFf zvmaOboS$ziRm_D-Se7=4zf~>oMo$7}$>|yGua~ifc2o3hpGmq8!D$-+-t@I88wMJ; zM<0T#O%h$X)h&|}G8%5_hP@lg+wpsMb|12bp1DY(?Du4R}PL2ouj!Wh| zb=SK3IU8{a9?<7@0Cx|C=j@|w#1ST@)O2naZVUvkeN`R?cRJTj8TLt>qLBk1K~^uC zn^IBDt<*zT&4^^$HiIO%Rwj-)B#cy_Eg#gst5yweq;3SQSf7ymTPM)Ue^KDCv1w8t zW(g`i9AsWkqfy+M%cOK+-PLA?;*Bm-=ckYZJsGap%ntts#loL68yP z!ObjYRdm6-7|PZF@!GoO^|Cy&HPj)z;VDm9?o$5j@0A0tgS zRO?#5py7C86{_5bFdy|bzXtA}IHh%KnX`N36!W_-5RbOlnrM_kCv!$tqACA_&m??4 z?U|%_$dG%$NCB@PKZc|n3q<7|ZrIV5SOmeiI+&oo3^}az12a&HI&vs=iiu`aTMT2> zVSlKVfz+gqJ(cx?$4`wTd<~D+^1;vj?$)C)z1eJfy0V@5DD$5-I`zCNYuzpqX1Q(t z?2g(HlQ3zZrIFO1pV!U}nC=L}uUDzD+SuONCLi`{s1ouQ9a`I{oixY!@B#XwxDtxc z@KS*$^!#_t`1}#-#oz`kmcS$2I!QYjY~p;=E#wIo6aKr+FQd6&*{TpNK<6#?Rsu*< z(jPIX;pM#L0Is*K)}oPNk7L+_ta0ns;XWMZW7y`7#XDFl2ILf6 z#%PhXZC{b5Vw{(CsL9-Q-nfgUrUL*bLI7MzIS%XgKJ~8P3Lp&q#|BpY z0G>x^F8_GLqG(P}>FcSx`#%vy0Zk!L+O9nxT4UL_V6r;yeq39AvTc*EEkE*Z^H+5g z7_?kJQKf=Yty3dm>7-4LNNEQqt>t-J>}<^|LU3LNnMo8)>!f*ueaW=3Tla3^T@@{k zs!k~mPsl1$pJoTL`J;H-glb)LL{*rlz^8>QG&^cWW6zC(^=;lPZm#$f9(MD?Eq|uN7+owpgZkN(x0;AC6!Skan1DcaUmod1-Q@`3YTjBaQRZc0 z?{m^4IR*CO6+c8|TWB>z^5(ConJ0{gHyZQKbXV=&bDDH{|WhlyYj~l1fR65#xX&Y z9UNb4g4P(eK7Kr%^582&vVrIg)vmvlcufbviV`d);1SZ4FiO!balf8mrablL%2q`b zetqAagzAe`?O2#PizNX$V$dC@IjtI zLnC20)lWF?L zLc3;$C8KnFwDdD1rC89z8IAnS%gHq4i&dPaBMGDQWhAiurzu>3Q!%tzUJ6n&oSGw# zUvS#t;d5j@9xcy~%9Y?lV;G#uy3Ni>Tm9ObpTaxm8;l3V!*z_UT{v>uGvTjaCOTNumN(ksFr-&r6NlmX))QrUK zc~Ov0#JQ-}3^4F{-AIhh#9370LX9&&X{g~hb^PP_ZneDDLp5pe+81->w&y7f`>j~T zuqS_VW&S}NrPQ6FT-9SKOw_@4X<}KqlF6uBnyhO`h#y~y85t`tiU5f*`3}LN&O6dp z9A5UJi-h*iX#g4VhS4eEuA8107gGb6(@)?Y$E6H!$z`6{+hu2F$KEvm)gW|`{-s|y z-WxLqbVvOtq?6m?-^M0eg17KC)9vx}E#`%}C}N+671G;q>ZUozb2 zZqo%;Tfm5l6vSI4J@qfrZD47oC?$D{4o`Q(1yT&ve%#1q(Xyy1+Pl?)>UR2%`ScYQ z8g}kP=P7C9(avL}`#r&OT)fO8ILFe9(nfOX=P7z&ze(_{aR{3lDxVv=WkbvlDSnqf zZOe6Sasj;aka?XTd8!wh1VD0m*yh8|n--bf$XwKyKdu1sSTuF%)pF_K%Y(BQPjjL# z_H`WJa6S6IWu`vB=_m}~mu{LGsNoP=nk;p-6Uq%#meQeEqD#X+!8PA{nK;jm;x?0Q z72nqtcj!P)28mb05|G=weuN!;27J3RRy>@V^Igsp(t%J(x!e5Pd;=~|+dB(L3rV>C zAPWlIZT>ROZ)AncK0d|tsPBf41=|VGfKW9%FDX|548DY`I|pIw1o3-tY22mw3$Dn( z5%PB z^;wjT)anV0VvXcoI3r8TWYUob_WZ?2%!}rh7vwO!gS(z3ff~-+f+x1PUOE~k#jBd?U$*m zQiMrW6^lf3Z<Z4(PE5|W96qSUL_rAEhXZK@nI^%SfP+E9)ezNKU! zAsj4z%r_)u2GVDUQi2<^?altIOwXIjKDv(mJWI9(3T^y#K>>Y?q#@CFRPYw;<9gL6 zz|{$fFHr?EK)T!2=Y$MJvV3S`NGYRPlAx9>6D*rRKFMiWd@L8mdvE54fOp&ht^v$% zD)N_3k)JL3p}y}u8K$y^=u;OUM#G{-3h{rFvO)j$KHv1Q{UCn0shGQIQLCGEbk=X; z_b`|!s%nTHm*8(hO`I)b(xyJ^>5UK-uD1|B9GdQQQQO44 zxm!blFKu=zIYO$=HDz-b{a*?v%${wWc%|qx{>7uH=xMV{yEx!y*<#rVi8Rm~j1^jr z$T)>vvG|fBxjA&QL_javp+IbR>83XAZj0Nu=Lpegz4bXL7X-+fxL6+d7N**bk_*gU zHq~p>QNqa3Z^Qn=bJVoK>EKZPADvFmHual&bQ8NzlG3sWNfU54sNBf$!)j40CR`Td zYKC`@0()Rud_t@x(bCzJXlI4t9|V{s0WN)HV^fZ04LoyCDWK88V|z27rpC7fE@?K3 z6%L8qf8~bRU7oNz$W4BxQ3u`Cj0p_slu(vq=N0e#ifxplCRj_Wy~6t=8L#90O1tI# zC`FYv%_?oqSg3Omf`sH_pvF4 zfChF+Uc#;Xu@ZF+yWWY8*0%q z?i?eCcp~mQk%~~^_I@x5gzlpy<1q8r& zQ+-O?0WB{tUAFwOHBat;Z0dHK#C+fa1WL80*Uup$5I8fD>g&f5c&D_I(p{+u?A(8* zy&-y0SbaqIF$9hynIO;M8E>VQs z*UN!h|Aha7RP!FAlUqO?#kYEsLQ!uB$9tg;XWf;Z_AOo95XL(bKfO3!i`_(_ZZk^t zg{l;u<4@$iQ$oIUSp4<8*mhXd7+BufB{9^rSYMu~0hv$$uCa9dj z8SEGgrk>IKSje9Bz_%5)% z(SC96TkKu4yC!@4`n?QQ^_)V@z8`llkg5r$go0UgdAld_P*;*=zzDRR`wpw?n7zdeqNh9M2_%=I<>-BNSY}|Cnxm*Jx&>I3@|M`u4xy$c--C=A`v^_&JC`@N2oDBUq#(AB+N&S_Kc=P!lQv$dNG zLi4uOTH?_>)yHZ?KSX#(`7)~>0yCD<(S$)ju9E(+0n~=#>cUR5bS`*eY z^p~}{f|3Lh1y<+nRm}}6J|kdBAV#+Ur9Ygftd;_zA8g|$)kGp9)e0Hcgk>SVp&_8k zds95SE-jX`#l*GUjPZ~>Nm+{4lF61j_+DjGl=$zGAb8h@)TQWUuKnul#&DX$KNB~W zKu-0{^Rtj8IdR=3`5$>yUEKe2nwPrv>Ur_=`7{c0qLEt-#B?{HH@0Lc6>P{?wu@<} z=KPsz1OMy4|5uJC+$O_L%WNeTzVtj*snri!>q6QOL}cRmNuzs|$UuA1dIkw}mc494 zsQ(boTGqvU12=EC71aH2vn+&*;_}km>Y9!{OMQz#zq`wd)W?UJRibmgZ*kBXN!`=4 zb;*(n_qJm|KQauweQGS{ayh2k#8WK;YNY_lK%>(Pk{3wJh06dq`Ts=b^vkS=Uywq4 zLTo;EHrhSpE}QzjfH>L+te2weS}1dSS<+ubZcjopO2PmdJYh^t#{b8YG0~L8m~^bLTOqkVY-q%D> ze}L`5VUl(k#+l#3&*Ic3@<`#{hf{A{7pfB4XrUlx3ky^#k9DH>QBANJ zb(Sb!4R-5TrZF#XH$nH#fZWB@da03(+!RaU*8tpfdnGZ#dTE6| z$j()Li&@O1*`*e5=X-$@A+(v%B`Te5tWwi(H3clxlj$t1Shd{<^PuG{6v4P)sJ-Mt z_1eT&1U4cKz*V{qxlOza_sM+0wDN(#-UvZ?BIt|$ioux~NYJzfqb)Mw#g0+aBKn>AT(o6mv8ZS{TawgVBe(rVGS6Z~nB`@+J z53yv+e9VIO=`_D<8kZ;67XYi?(5#hY^pjK-xYHRa{_tAE^ZB6Ddc82Ye;-kb1GUF2 z7a+N5Y*ivoQ_XXo0K=&8B!%SM*cP1`F29y|sgO&)9HcHM38|GrO-xB{Vsfm<=R$R{ zq@L7+kWmI(rH1$}y4nvcl0QaSVrZPGMo10RiL$z5R^&BD_0CL$^;yUoHYN4pf`<2j zF!^AVJgXo6pm_bzguZqnH{0cmOE`Ca{-tPZ%NAcK0ZiN%&p$SEjA8DaifjXp~mZ_IayeFApgrapI)ZnIQV*ibi*Hs zZ;I`Q<%gp@WCg(8?uCnC?T&@+m4PPH{!d< z<=@it)#zQM*6!3%XvCp@++FrB5s1UgZ6L4k zcJv z0Xf+yPy1%?2{nx|f8x4lf3;hbv)s(%j`r-Uzem0&%3b3$g;sqWyYAI}NlEjmH%4pO zP3+xMgE2Sx+Saul&P9CP4aHmV&Fq?6yY_jtS7kwe;IB(Z=O4IyAh@!>{y-&unED<@ z<33+513+(o5SoOO&rX*p^yN)#a0FO0c=TsA6%xRJM$71+%x8t*(Bs9@^ijrKW@?}F zuk#`u9!XsF0wZpiIGtmb^*p|-S8%sJPwUQJ8lfek#zf&=>fa>TP>oRF=$zl0yhDk4 zoDH^nWqugHRL-Jnua-H0jxB*4vs5`*05w6ssjx|b2Os=ToV1gAr>vyjH|xck2q5{E zyPR50=OTOy}+&n?}+7pNy0Xj(TOeLz$Ff{`_&We2)SD)?w^A= zPGXmsN6Q35hqFAPkgAGrNfI4Ke1mu^43m5hlf3D53L8MJA@hwe0lmxIaJ;eaONH?Q z?F&JCCUlRCU!XueB9HCpW}F!UW~V(Jj+^@cwHKp=cnYKWimcO?LTzCr-rLJ-+YV`H z;1e?*QkL2ni8pwtujN7K`=W+~@yB&$V=fvp*R_%ZRyAU2`58YFICZ;b!{x>)V zdsfqtni1g>_@yvw0K+srlPGyI5*ooyY$V7qbVshbsKEr@`WYD9$5WGH^|T8fuQr? z?+moJn4i}>vq$V@kz%zx1MgDgM#%uV;P^oD#SdZG2 zia5|a8FwLYDat79eL}niQ83d^;_qFw6NOdp?sG;{_rxV^#0w&(Y%pSA5qK?yGKpsw z+6q`gcmdgrN7dRhTGz+fjs$s6-dsv)lsYij(#LLN(P9pE)ZLtt8@+J(b+lV4Z3yZb zX3m`Bx(ya81xQ-DK_m~q0_g`f?SK5B-KYjuI-@XDXD2c{#Hr#n)j&>=@_{J|{aqi8 z+fvvd{670mAbd*SX*VtGv`n^HAX(ElQqS==0I%u&^ljL?kX$8{P4rD2vuCE)w4f4A zxb*LEt5$3f-14pb1Ol|BPyuv6-JQ3r4fl**4ubAs+OuG=Y$qRC<0foN>ZZCK`>4r; z1wCuzlQdm)tT!Q>{OBk4rBlqRC*MRFlunCp+I2-P%c_Q$=0nb2_C`8UspA}wI;qY# zsY#K3E}k)o*3>Up>5*+>0Md6t+`i(HetIR)x8tNdLOF?8nH@ZQ(jkF`$Kqb_M5K1T zoL@IK(fwf5)y#~0=1v<{kbBizvs})qq9gzYdiptO|1pfWmZ*jmFE1#<6+k2Kv6hPh zou7{6KQo9f771D*Yr~jI#DWJTOn=!d<1mrOth)Zy>8w~v9NTg051o$B`-?_^9YA@R zGtCCW1f5CG4+z+S5UYlYc__5IjZh;F+tc)T>y4Q6Nlv71*>Li*-HDBSE6U*-8^B*h zU}m*TpV?g$aXAWpb^9;blz^y zmTc>r0J%0p z9M5TGNWTrGmuS)~b4vXchR5{WTg`PMDswI0T7l{xCt1^;%i4rKadHh&%LBIAEGBlh z^Ijmr;+%fnJpeTz7J&!8@t^N2BqT58$sNJeHbC&O3i4uTs1~9jyJ00CQ~ZpOgset1 z^Zhr_RUy>3gSv{6-VtHz+xt4nY83+*PK$4x&3hMe=Hg)4uB_`N%If}{8aHRw*eb&7 zsQ^Sr5peZp8ro}!@;K;*Hm<&pPC?!2xn(U&(stJqbiLD8^TB!*+M6VfM4D9ncC#w6uZ(cYpXg%vp^IQ#9}|E)p(Oh0L?(p!gPWilaXsg% z>pR8EG*U?_GBU7pzH2`5T!a~ohZsb9v3yKaLOGfut4 zyrpZ6#uUti#ARb>EhA>rmnCFNF|LX0!OV4*tD9zT6P+M+F|F{;omaT4Ha~wJ`qpsbqrYDRNbx*SC(Ql1>~>(yoe$$9csX$IaHWX|JT9$TiSawJ>|j`SLdH zrog@@e+HNS98x|I@z-oZ1~?cnM7TJeX+V&N3I)UU`k$I&c9@(p*H(O_IXpq1XwehN@j5>V=8tEa{mG=ghBlrCUX8OpoaCXDw9C=O}pr2~Y$lUF3-NMAjW z9N0f0ahiJ>(WhbmM3HL4tC7}a0q$&t{qu835@S=BB3IR&r`rsgA3Z^P`sO=rI=_y_ z7f#=FPpkL&5|ETA-&0#N#TtDnH6}giLpksqlJieje({M<0;o_SSPc9t}8B!&?GkK zBiN2W2cHBffRIP=zqkk<41}(y#RGa5tbk7_Rf;j`@fxr0Pd7iqlnH6I*5iEjRyx_z zp~N27?V8K!#?2~q-pTB|{)7){!q_u@A*BEmh2tof-#eOBKRZn{qFrNw>DYBba0C_M zSHxW=lLq+|fUeK`^lP!&RFDDkaG6iq$t6__0@Zr50$5C-bKHRuG#d{{{XW~No_JIi z6VvJIG6#^IgT5U3XVc3-qSW8gaV=t8NZPVBn}mq1}t=p4@=2}~=! zzfM#V`u&a%3R~Zg`)tgh_p|BPyeY9q zM#DuN>mCOVqa5~5UDOB~k&wBXg&;|B{eX_~p_R1i`oI7Cmk7R#fAHJpvxfGsw z&KHR!ATG|wuC33$bc_ma=Gv^0zYV*wFAd8F81eXPt$JkErxvHpT82Z^1Y>)_#0X@npo_obZ+3p`CC*u z(bYC4wZO|{dcM+HJ|G9OChm>$6iDYc_YQ1IlFnw9gwx_&nU4j_X!wRch4GvWww)Yn zNqeUlR&^cj8jNS*(e7`ALsYuBKhe3LMd^bP4~vE?U9hw@dJiG8R)NoA*@r7d{~--6LEfG)J-#4`345|YMlydpU9>vs@mp6p2(u~44#p*X z2zveTf6a;a>9>8TL;G@tKwh_Xi?+ZNWDg{5!zC>t(`>f)3OQV3`meD=!N}o+A;}=4 zojiv-YulKy)UZc*-U_AGSPcjQY-5C}ya9Iqjt7&(HQScS_7A$IP5Wpi@y|gyx`SDp zfW;!I{>Q7nzzH^l>6Q|F2Ug}ean@+KFTQ-nj&z8*rkEE_8l)MlKp)3j2N>e8wHeN` zwjwjSe;U6}C|Zwd&^(y?l$y0{cGEJfJ3FXgniD+AECsXGVoJA$FNq<}y4t(CSc|H0 zren4c8YeWk5MS9Up4M1ATV?udy0M$7cN7H4IswA5lq z>r?C_KPb>q^{d~8YoI-vH)u+`6XCxqQEezNoTA`trvqTm8g|Z|2R(ry8lw*(EId5= z*=V!VdfNNBOU$^Kmd}pJ?5Y>^>vmRyQNGiahTOK&<^qlK>FbBQmxn~rAyLnUr#;VJ zc5gcjWr{yX8$KPH?432p6l6Nco;wBsnHFP4hEdiSAI*Md@W13|qGYlwGJ;L65^@gX z*&tcPz(N}>5VWMvaB@gl1Nzkyb=b`oZ6vWYWrR+g#e~|SfZl_s(ahGA|MZ3may%p5 z@|&R&(S%iA?V0I~_VjIk?YS0Gp+axFv^A#Bq%3PYJ<*=yays2=r{mYNRlbN$t?r0q z0R%wLVT1C7=3LB9=Qtui)YO8pEUcNkdnm)w5O&}DlvOb!YYe32k2N`U)vSZ?7k5LBX5HsmwpOF?BCrm`6%eg6y0% ze@L7VF`3tuctpoU)uO_y0VrZP29MFw5qNzIx-n|`y$_zm3(kYMks9He0aB?qBt4gK zLaHk=RLR3Tn^jG+mODY0MTVBtps*h3%uiLjDSUD2jpj@W20NNGGfz&_@J!utII4T3 zVBF4Qu0J}7HeMOQ^|fN$;X#%UJ|?Xk2o^ES_%5zP8wrKUW3)s|B;(!mZkfZZezAcT zk;s9T75EV0W37*dqwhO99nK3(a-_8f&(miVoJFGpwa((QZCfj^OJX0~?Bm4tVB)d? zE#;%t;mSyv!uRTYA-ADmr}}EWuFtFSUfx)kO{&@UxbJX_p=yL$>%BJzb^~LTCFk;A zqYhWEyEMdcw+0r_=jA@Se31IeL?Rdwq!r_Bga)+5uHEjms;`z0Gl6fi@e1Z>AgN1B zBt1E5bs0kbonRXT?PD#~Y`LJ(C%Xi(_+}()oW`a&E zTD^|?+Tl=rc|3zzeox9Or3Ix4a157S%K~VE)3={)enZpylJwBN^lZkl9x-85-|mls_%$AZs*GU8yBzkZoSz zE;n1Y>(g9HkwLKP`s6%rm9P(-gpmPYf%*G%&b&pwhxWyEP7r759F(0y+1gMA@(oZX zu}R^Bs=~h{(Rv2&YQG~*!_rxI{ZTCX`t$fvewnE+H^S=$cxl;~yiG)V{IL1mG2ibS z%)cna>lCWi#x=M;nc%7%mwWYsxTD#D_@n-wou%(K)FQD=ZNsLN&Q7aK9#R@5zE{O| zn+2Im;B0L^msr+MXoSP5?#7>N609+49(I&coFS-vo7um7C9qq{YPW=QXx-=13-3%g zaj(*l{kyh@S-f;mCit zi~S(mBdOA>PZ_25wjK;z7GhG>YJES{$C;_!zaX{x(|p?f(7#s=x09-XentR8kiw>4 zd>p19Q#%nM^T3@=1E^j%U#ll6-85^e0f|M0m--0C<=RJqZ9>YFTwe~4)=xJE=oCbw0A($6C;3>0 z-Yypj@S9qa+`!h)3BiP#x~ebn#F__4%;InmZ1NC2m)NA2ls(jYQn21SHk3_xooA4b#o#QLGIP!PPQYnXwHNNHu{<;gL&U-5oIMn z-z^pIrxESEFflJJUsEm@nl@2?HP8@Kif!C!-CFdJkMyzJ9&#NX{2N5kk7u4hu}^-g z=?c1q`g|$s+^vl--v?<00u8Y3TFKv_8~z?@kDzj{VEfb%TVYjj%NjXjkA2YpQ=&$I zn>)}sCKdM&!-nB)f+#HJ+h2~I7+u}`PaZ_uZdv7Xy;m{k=x&nOlE5f3RNr?dcyWda zReK4#eGi;z;7ZC9PDd2d(<%Qv%-f@0i;;1VjdW%w>)FKtWQxwOn!g1FjR>e*3Xdmn zZ;&0sxpsK-pEuz!buJ(BWjW8OKu(q>*>J?#H_Xf2 zWQmj ztApL1b`uL_-aF|CrWGvsPxy7Z+BKoTcY=Qu#7dKd#^Aah^y~Fo&3qeg0Cji0^|aF+)I=nI$PWr<1+U4fQ7 zFDbCkv~>xK`9RZ&r-?(y9$fmV!c&)-{$POwiv)Fwt!ZM@rVst*{+x9a#X|Po8MJD3 z@*DjFpI3nrZ2-wULhcI?SbFoFLr(AX)+53+F=o^KQSvtO$iaQf8HdXtJGkTV8yllN z)Ky9Ax^JUqNO`JH`{e*eK)Jtq7>+4UoKoow2c09Mx|$qUpb~$ZMjcYH*eg^y(~RI8 zI_GuicAI2{6bTJT2m8}>IlOWHX$G4+Z>RALY+)_H)^0Y9fz4)$iShc-Y!C;ZJvp1ZRmWd2_oGmup5%W2NtotcW)nBFqta%0yVKgF zh2xA;e(!>&b589ojrt)GL+=UvVrM@D<4_&xa5;+**}L&1FY~rF!?weXAmtG{suCd% z=AtL?@%)k1P~e8H=@+pzC^~r}}f-Yax zo!b!a*`=syZGBq+b#*eg+jy02%`|hJfNzE)_LwvfgN|)qbA-IxBb|f#c%8Ok_51dk z=JXAyXj23Y^V>vaZ>hmX=@yvYn11JRyJ(gxusn^I;)CjiX{c(xa>$-dhcImJxU2+> zoJDC4>yEh}_5}IwxcS$*p$E#~oCP}Llw}Ps;>)lJ>7qq?o^JgKngmWJw(lX?#pMT} z18a@m^tz{~n|u;QKq8e9_K(_Y>J6V>E*fL1KfmNF>w>QtIk8uZZ!!wX-yr0y9h>cN zni8%z3_?%+%sv=`wlF`JDVe?HvW3RkyVX=*1l{An4F?v5ahjuCDc}wBT11U0X= zFvL3TOP(xWP{xHwp!MA;#kklAJR5qLQj_%IJ_`hFuN@QyYF)|!8}IYb%Wl-A*XwGx z(qB>__snOyukgea^R(k)*WS`U?CkiskbfRx0Hlqun`B1ks%Ckre*|TGSWN7f@pYcT z#ro%Ot7OZLi8I~QK7BfdVQZ=Y488o``O~Mzi+-atjnbvl3J|qaV(%*%IH6bm+c9{m zQX~v|ABej2ci0z18+%&07qftLL@cuJgh9zwHLNG6QZW)xTH9y z&~lg4i7v6fu(wVqo~GwZylu?MF2`tPW>3%F07U&pe@ddIQX__H%yMoH&o$VchNo;5 z;)t{YT*|2)>ZaHHK)mN2>fLP0L^~`v5E@^ggw=C6Q3E19gp%nZ;5K5pI)xAQDLtgU z1`%elyP)m7s4{4n`K@SY(B$WQe?_y$t5Ru+*^M&jU}!|FX8DMWqegnN@rQj1okW`5 zJCSw`>M?`_;r5#Lmu=hjK zX;W*m3#nOO0G4Fh2asfLAY*`Pf+@iSW;E2$Mk9>tjx%KzNj3{cqhLe!kDnA7uVGgOV>`x-4 z0klT)w27oG?16$Xgp@1@1VSoq*Tf(&5@sat%7KD$r$W@6|FwEdGnKKWP|AjkVe4)4 zcTF8junJmYk))2M;PZXfj4hy(cPHxb(Qb{`wittnk}e0$sE9o~A#%*bb^V9D+JhB>ud-_Pzm4T#sm<>1{YS31PYXHm2--Jd$HR64^ZR<$zjZ zOX2w=>|hPu>0G^Skp9~&TsuzA`)}djjGsCPl>kaM+w)E~lTWa(TW+|*U;-4M{|J>(rmTy0)V$#H}s$HYCg0LpK1EhuX`2o)6c+ zH`fjOC;3#fA%a}MTonn*<&Q0nv5|V=x6ld3t(f+sE@;Nye#>)$08XWk-j{;Dk~vECu5oO z0C>Td-Vf#=W=Ur?Fr53J6H{1!=f@^Ac>C8G>ooJokNQ(uhqL$Oq^SWK-_otfXGj`~ za;~wm6zX&iwf702YoEN@1d`p$t9leeIUYbgEP>FXFlmoe2(2)8l_akZrWfm}A>x59 zc6M?BnVGip^Q|?$LX$p?*RTvOGj=D}tRx&9zwo^}V8^(7wEapc7g4CUPOh!r?ld+o#S-GtP+=K9nPgZq9=u;Zwa%dKiJ71OFxpca1|vrM?ZnEwDc?i0Q;jBIU6m3^#RFubC zw++XFCTzOYbdnqbU0kDYaMvj)TM}d-Gx+K0@;0#v*=1*;Q@|E!U#W@0$cCZj!(odU z-R(j&YrRlulI*YTofB&{EOuj@4#SfrsV#K0v(o^|d0x)iXd@-4C9&_`LFB}V12Y1U zG})8;-s&*D1Er<@RL_I$bSiI`l~?Jp#-M{kQrefOC{Hn@&N4&lQ0yy1{E%jo`+;Sy z-nh;li%7*F4w zQfbKajT$CVg*s_1A#$!_zq8)vd*O0e4EScy#BN-B!U)44Ml|M-|gm@@oJE%v?0 z{|@I&As>NJM{UY3EyrL9pRJmn#j3#2ABYttbYVIvk+AGoUO#G4bS+gQZzpB1rS(&E zDzDIOTjp8&)`1JH*=-IgxEbtuApz7-jTyAu&Ae;62GQm|Qov@{grSx_d`?_(Nhqx~ zU)(b4B;}gwV0Z&{D5zPnV0b%CeaP;lq3R3%Y1n=I+pgN=3^X-6^7}g4SfkmI4IXY< z>T2d}!*np@f5X zSM&-RPp>rr31FB1Nt~MTEtezd;_R*coNGtx26J`~6LFxMf^OGLwrcyc*&zd`Teuoc z?)I|5B4Me^TUhZL%j=~8PZgf4;VM$%J9^nsy6nYv(wMF@hv7(7f26iVkwl?a#G_?T za5V5)@%?3YU7FLKY)8Yqv;2=I79?y8iaLk)5c$>RZ*SgxX^f4)yrm3;e)nxenPF-w zULELO=m~aYyUki!Bj>omkdy7(@HK*2YKqjo)9Ihn^zKY1A2pw(qkG;<4D}tLyldQ>2NJ8 zE5R64kakq-upm}tjG4mTSDf1uo9AjXb)HW_R^*w1N5gX$zG?HW`$+gP4d(~Gd?5Ff zy%rfJX9@i2Q>bP%capQb8DEV0QKkOr6AXWYdL9hEdb#T8Bo)%*WR^WH+th26cSBRe z*yecs)wMe#TwhCV#?wjKROIp62?-FbjU6eHiBQ~0?rIk*hU__@ek{=`-b*0{a@OB%{fpwCP8`c7X z6+6n$e2amO4ZDhsb!!>s_Z-jj0%_3#Ur3bdy2;LFUfz2=i~ z6K+TTU8l?O=sJl@jJ4pRDb#_AvfP@c3DM^TZ1x~0vgKi#h*aPlsyV30QDPL zxt~T8O5Z&mmqTW{CF1RS+DPb8{Y7Dj{ATI-H zc7~L$C1l+8EOXHCG6eP`T*jMm+J|LTSwE#WV6c&f2B|+@``zZ>$9h^m20Gd9L3UQI z`4oiMJnOmFuV@BuRX}S*eOC=Nrn4pCRdxm1Hmw*022C34qv;%saG*2jHiJ*pk)EY! z<;(99A0-JNjNF>d(QJ_;`A2;T!T(c#A*uM_e&;r*9=Ly$6(v=#AIIbP+^g)=d-g6H zDXlGP0|p53%QI>1rLkEZqAR>2 z-jv4Y6GalKpiE=tM@l6d3FH-mutKJLp@dnkBog8 zCb_GwJ>r11$0(zYf8uJ%$kKY)eD>*Gbb@5~%SA=k9C{8YGjJ;Vh9G|1%OP;)f8Pus zZq*0Yo|_ZdaRG>wX3~l#qzWZ`12qBDt7vG8Zf|3+@yDgL1Rv@d@z4&5yEY*>OiL$f z#QiNhT`Pna{!<^#Eo;eQOC?sSt}+$3rse}At9QSX4$gFVyPRX1)`W~{*ws7U-p5o2Z3@K-)(tN>dQ&Vi6#W#45O1^} zrF>Gd5g6|kzoo3;Zu2uJwg+LH_qNKeOKYV#XzC@O@lg%Sv{eJVYb742W4e(gSH5$T^|vXztbbipIfb%nqfdp_p)xo197D^X<0|pw z#pl?Vrkh){bT%=Sd~fN45(w(__AvOnIo-0=+~bIRl;pF<2>xsx@}av}PqAcl*|hai z4hwb!Lu-e65YcjA=P;6n`|b-QptRX&K))bo4S3oKGvd2kzw7mUr= zee=`nJcsM5rYg5~q4!05+rFUMiHsn|PqYr_tEB~o)7X}npDfMR<*6bqaoTtA+;6Jx zjC}ef=<^(BYcixe49d23T26#@0x79-L znaeorxtC}fzky7J_J_aggL#e>wCYp4b|JtjevPlPwG!&ru@x`Wfq{?Jk>obFmUa=? zHRY83yhD06FcR$^8Ht2>IvuB>#=(PK&eitjh$zG{K7E?y-$tqOc$w;;#(({m=uUYL zSzbN`)><-@Ra)fU4W6_yfR2X31L29|IiR9GrkCm_zS;sWoLMCr7HABuLuYU3+uih- z+dI*si&6{g_1;FTWQPOR1)Rdosa(iFA;hm{Ax}u5v;i`$>`8eyh(&d5l4u~U9&R&D zZ{Ns7JAZJ_B;R%=rZKQ1^Bbt(cDI4obw9#lYY?Oae(}`x;P;sTyRbL1X zA?~nxz|{lvSK6EW)Zy$lH>wno+izZ@S7kO)TwrJCEAb#<9ChC4(Jq0!Zxb=`ve>L9Cg}zr$cInEXO5S=LfbH6*kK;E2XD zgG!^K3vc7}#o=H8`0d9w-Z&Zsa`GFGQHiwJo*m!W@>805$wG56dl^x2QaV+J!)*3m z>Y^?}%Ho50ui-cf$h*|GnX4K{U<6=^Idjdx*|WFg3G@}-+egtw@TX|;&XDUz`sXUP zE}h`;h}3TmZ{{Pa)hzKl1?x=sCkD;GPETKJK*!ba3u`XL=@lDjAz1_71H83#iZC4h zZQ475C6)v1)!xoqRsU8J(VU-Z_cx^iHeDp%z%DYT3ah%aXzsw;GsoA|p>&_Gec$Qw zT|`_|I@u76y|Kbyb}0#gJ$1*BVX1TB8CStu$uS>hO}(162d+p3*>QC}9CkfwLkf(g zam(p09=l2D>Fi>?IEj{HLNf_Ui>kvarsSppEo=OOkw~m0F)K((T6VD&N>14AH12L1 z#)*qAA^G)7Z|TP}-6Nj4^8kQwUkwztkq;p4dkJ+m8H0oe&gE!KXzxlC-WO{n&|%LW zMX@{@r+^qhEt6xDuIm^%QPpl!H1&j$3Id z(5$CfaR-&Ees-aM=tZdewM|%Te-u5Jqpl{{W?q7I1FYbapOyblh9yL&et<&dDQ+DP zX(!W)l7j@-I2U@RoIffrJ6~LMZa!YS2hS#VdyEGA@5TdCB|jLnp*ynf?*z4w+>A&l zu;dcjGN@^3KqyaAd6|tw23)tjL6g2xN)%pE-{Tes0ZybSJiDLqo_I9>5-7T-+;P2oy z`rD-DZKwm41Vuehzs3&#|6ys!Lxg6)1T&Y^R0u_|HD!t*0dt?5@<4*_Hihie|TR3Uw*{`BvUs{IBgw$v!f)9(3BIqVWx`NLNN~HU&{D zcM$++YY>vTl7D!u#nAbH^swXc%Lwpa2F%~>u0C|qJLwlrSY!SFZaGl09}+@p<+XyT zjYFELBRs(Axs?WR9Vn?dgXly>@d%oIkj#^<66TrQCgv)83Q{~?!NS{h)~oezLKYz) zh`_6bDrB!ZC{ct$Yp}q z=`l9_mTmGqMt2xkj(c#9Yf&@vS@TkiLY7-5cz=m+Z>#(WH6DIzmU|TLst@*&EU*M<<|!WB-IZVBQ0lnAdUR)ZDdVDW7S!i$Rebl^vZ>rXOBEJw zuWkd_kwj2Mg2J`|8q}jD35hCREc>!N)t`mK@jGtyd8kpQCC=w?$yU!j(YwwWMWW{A zVPLXHfc68|f4#{ns!=`$fD}Jgi zewUE=XX2EY7wXH!m{bmmJ?&EmqK&|sVg^@|jxy!%sV>2kp%4Qbgd)%HWuunhB(w3< zEnxW-4C#LJzRWBOQz2)1ZZJdl=~&eLu=z81*}SD6HowI^D{Vx6g9yGw1yDp-ldi=D zZ+LW1XGxy~Mjg>0Qd)r)e(IEnh?sl!LVk^_I@PpT4EBXbH45U~-7BXYeId zF18y(3`;I5Z1BPEQ<6lAi574$FVGfYr><76>KXtuHU&xLVEE&#d*?2~A6Ro=TN@ky3RQ#9{}&>Wk2jvPASFrDW)shzHFrTWBuMGinG;05|oV< zE#s{G>Al<2G~7#H+UXcXBZDQl|5h3R9@m|KlbMZYis-z}bzXw0;SOq24M|Ll{^=5C zj_gPAklAj6T1|}lkti}F?-+82J$eo|J}QhovI*Lq}k3_)kM0egqtjtd}p zgkkMN+`^POt?VIvDY;@na}-DDStB^hTWYw23{UDvEKh&f{H_h7>n1Vz1MfHwov!&s z(IRj)Xi~i2FKc%fpgd|icxAX2<&cm%)D(-KIGcZAh#rZ(%##k?$D?Ul_u{fifluE@ zG?avXLtQrYyFZTYZj9M0)RLx}HU_|mzke4&+4nsHxK@u=5qK}l1KTV}y)}4D;jE^15@t4x&4+ps(1EStL!_$? z6}~mrZ6LUF8%UZ?7G?97(;&Tw&4cd~&+wt#f(3BjKi=|ik;V+xN@wY$K}}MI?&Fob zoKzlB<)YxHY`TjR-jDM``)9@&YCOKUPb*5n-R7V9;3W3IA?>I+5k=H+8DYq+c?U!q zmGb}YoK?dk!K0(y3Gs}Pe4T9eudo{u-w>fIm*yN=5b zu2bEgjE+Wn0=g&UVKGB7^lIvYs0TT*R6q2|ffKE?*4(!0KvcP@nMs7PT@7l5Jd$aJ z&F5P093@>KB4%j86He`%@}2;IxbTrIxqWNwDi}jh5cR`qtj!(jldB#({?4=D4PJ&aa3k1B5r-dxv=?eslTDSCW z2Zva~D|1G76RqVZFWUN`v5z@MT&a2%kJCC3y--{T2X!ewS#V6$3AB1>7Q;U5O%_8y zJxSF$mm3;3B)O}F*VJM*qj?AYz?=B%n4}guVtE>b(^KVRL0!2OiIizvq8Euu zs*h~;BhZzE1b2OhWmc*hH0p@Bk?c3Q-IapEVHAP9Uck0^s{fh{3yfrzPPJkM7%~H< zeu1QWnu*AL_zF+zDn~1|Ca1_>)xanzk#+3bzySbIH1l~Ihz>p`Gt9$Ac_>kdi%9=N zqGtzM0MHmBt-vz9p3XmZ-&)t{ws<*jwa*^dT7L*Z2F`aZ!}u?!`ahmtFQ@v?Kc8#R z43(oXH!5dWrd@1$*-txlH=SIJijPZYe*kqdps-GMoh0Oe-R;z6U`Y85H_-0fvNr5; zo=#2C8}(hh#-HbG&nN7}z|nI+;*I0aWfI}V84un?X_qY}`@!>m`SBMVWg+rP^UvJYnHTF%lQkU&^Uf-mkXTMK6FS5I^f<0>j zjSJyI3>K2OCu1iA*0|gp+rNU8C>skwYa2CNhV>o2?xsx8#g+!s<#{v9gwhE`2e5Ba zVNF*%9uJ$@d-efL>ur*u($Nc5sh%DT&!lw3P0_!my^sgNX{_;o!su<+3?EwPbBEGt zZfPQc3Mr36M{4EMHSlKr5i!Lqof5;%;3tfx)a0n!oiC&F$mVKXdkUk(^Q*J4Lm=$l zkylzsA*6LS9}Lw)E}Wcth{O^{-P8X0R`$wx0xrPT8|9F0l{`eM*P{)6dr4b!Uk_;* z+UniLxW6`Y>(aDs-KUHbZDB)`tx!&U`UXtsb`0kxzU#K`xQMpnu1kB-F;?HMdv1oz z#Kj?uHN&L44+8<%whm}I?PbI)4TeswN81>GT-NFdovX^W11ZOTV4PeCZ`R0eaNniW zr@{Q_ui+x!89Ke&|Cq+l0vm^YZJ-EjUc_!goH>ZNoY2a$*1y@!teaSwZ3ryBP|}Pd zUixyIhsXsPz0*q#aMHx9OKsH@DP#)vLuO;Xf)PTia0?AFe+O+qhE>cM0xRc+7b92N z)z$JQ8d?(Iz;uKL8wp3zps41Qu5aW0?O{+-mC(DptN-1>JIPI4SGyv*B@`aGwu4;}*m zO>&2pwDO!~6VTEdISIjA`ls>qc$MrA`*s$RejJHi-6gnnt?7l?z?yiizS<$}$&Kpj z9}at5e%GGQ;DnRL`#4CiwCT%)+WIq6XWf1F3bj+O<2jt8`RD+8O8uYb`l_ui=qXe~ z+esiNJ1}WG8&?@vNOPBD?<(H=>g8MDzTVbf$h~933@gn*fq+1j_1(Z2s%?oCwmMNWy3=K8dDotV?5b`3q^oksIju=3x^`{xR z)Jg2=4aZ`>y%US52v(|Huu-MtIq1{}M z%HM%y+qF!v6@~Do)xKUU6s5gdMnf4TP2v1)#$-db1(+$P!!9AghS;r3e6}@J^9*Q| z1muxtxSYV(3}0h1=reKNHgSwh12zT|z>HPP1J6dTeX%RC;utSq{ptZeE z^JTX!y;;ZOe7#-2YN!vFV3e%O@J`m1YkGoLmAm${y7d}@YJe+s?zcB~DUDc3rkVpT zJLsP+*Zn%fQ3e%KGn;Owj&rZ@aK9`e6ZqK@9{D5nk$U^K%@Gji<^Y((cn{tBdiK=y8knB+XDl!95ev zzo8X89amVoO#`%&_o^F~UksPoO;1?|S$gR1~4{`r;@19X)F$ z03LAxXfCp%aA&O}$0o#30fg%l$r*#el+j3R$Ww3)&58eER6>H2ulggZTS94cmQL{yqL=B0t-UEiIT+CfXN z?JGWLWT-(A;tgiOX(f4!d7#VZ%|2 z?*`V_t?m9en#@@bJ2(gC<({tL0Q0+ZAS&9{?JuZDl!AzZ*VyD)^aomqCQLt?YeZ&2 z5HM65-S~+eNa>ErfSRP*A&;YaX(3(Gs4wL}nV3c^OTKQ3xUSqOcy%&JUnjA}3Wn%^ zyEivHVl2!{%kz=_F)hds-peEVI^{5So(?c8rry3A{+;SANsH0VseAu|zRLoDr(pjlz1t|@l$Wjlg=J)_4gr%z;FEU}F}95p z5V~`(7U?=E=w*aeth0wkU6;jlkm&QU^3TS8(k>kZx8Yu9dvm}ByNcMN>#bI59=2Zs zhv!?xW;T}$Bw1wOQbZBZ{8K<)7i~1GVQTK7H^GwjT^b}5p|2lFH30T^u-f7Y`&*D5 zp}78W^Si(ellz$0M0j_v@++NPT`HxhUoR&B=D)1Ee(fi{e1k_tP1t?5w+Cx<-b&2v zkJq%8-`;#S;Ev?ueK{7dB>zG^S}3Nnw0QFuPc6Eg7Ja`-YsZ;2qHAaLp}J)@zYRl5 z#u_rWBzM!4fgetw2=5-u5}1QV%hptMX|kD4k%g(Vx#Z&fp4N{??-J^zR@Y(Li}cUS zIs@C^-&ypu4mL1C3Uk!LRE)@J5yPZt)g?mC6itu3dEd8&wUO4C{M3QWB_av%ONXuF z&9e;w{MtBv)v3wV2+6M|jfsiy(is*X0n%8USsDv)&Q%c@C>M3QBL!N+s^6$>RTRgz z{h477w^n)BHO7NmoWJ7&o1Oym#?kTm74MRMpV(Z9xW!e$$F*v>kd22=Zcbv$k=m&!Ib32+mlD7wg124^Y9%D zCrOb#pHm4@1wKk5K%U&$ZS6Xfej zmHKG--OlddO$0L}pr?@$juunRNI=!+tdoW*AMxkgex>#l)-+CG@9}bMUL9<*?Q2$y zoyjaY2+Hc#LQ;3^MJEmjh>s9#DUf=U&TQXvz>+GKrx7A4UtQfh{`RUwm$_lnJ-CO5=+UPwwBQ z@(LW9y*;UR*DW%S{oM{vw~6 zCwZ~MX4%ZIg=*bpXUcAmH)o}LQy_VDqNOtJr6NChqRU7snOI)~&qdoQM!WPin0{1CQzilXO*y>4IswNV?%ngp`boP>h6;v~&Q* zf&pZrzNC9M>-mlTvC;FmSJgmfk4$YU!$|`6P`!E|=W#X;#;sN=-l>$)620XFyiSj2 zQ>Y?r^J@M#PVttFph_Ab%L96MYlr+gp1eXE69Jv8_hCAho>0ITu64TY>P9&^^iIEK zqoy^kK&F_7r*%)xYoYAvm8Q6~3{Q;~n0^?Yp~TW$48&CHsIYWtgVdCL6pFUA~~aIao4V=;QP$+ zwfrbnaE>d(9K!Ho(>!GBU+W&{ETM^oj}kH*P#?2v*=h zqEMdKrq8qbeFrVE=cGQOrm5d-&w<0$-3I&5MvD^%wS-wRgZqkK?%#0}H9n2{GW48l z$}}BmI~ZiCl?G7f@R&Aw?`#*F`hu*)T`LFIHqz__;#N{EX2nx>ncgm3lTC=pP|w#y zVhNb>GYj+Oh0+#0>Wn%>$1>k4n@xL3^DMo+xN)3+y5L0StJ0dR8x2>oQ`vrvl~4RK)&2;7;q04DWcm{{?SNVS4tk7Dz(cDAgqw9=COX*EJu~!(mfvl<|Z*;>y z@}vNzqPMr*W<||M9g`f5Iu$L}@-QVy26?#OqR8weG}&9CKN~~LswOA(bP{#xgLhIw z+Oc9qFvodfV=RJ!b>th)R7@FZK9QO+V>&=RNnyNE_-d&7V zt!Nx7Go#weeNP35CoZbPee?)#Gg0JKKiiGXpGqI*z6dyoMBkZ%D0XEpXB)V+K!`*N&=DT{ z*S*)Z@Wx9B?5+r8a4-XO^Y|?dne#wi4s~Y-{Mlj z?=AR$YDD+y`?0Rw&o3*F&K>~~hnG)}=?L6`X)WuF5@sk%l&?fBF7bR+LlK)pD>I1L1?%;t?c@K?{7D&A00ZZ4KC!KbcY|oJo6`#l4&JaLVSuaeVWufnG z5D)Jl-JzG7Fg4&QX&|46VTQ!`2}R|i4pExfROW;HxUk7M&ljB5;rMzA zWmf_Rqs=Tyup7;Q`-RZz8{$7B3s0VxS>8iKiU8ptu#VPbAG_tU+mWVMU$d1XY0PwF z<+q61dLdn{bK@wDTg2UXh}1giL)T=3zO*K+AdMb?$UroPrwUtL-;rDd3aNdemFw6D zCqZMTIE&P5kL$vGn+Mx}Ta5{gk2WJrLnV&7j_69U6C5Uz4V7Y0SW08O$i@NKE zo1!4JWjS%F42iAtn%bD^FPHL57-zD&=_P;8H&Dljr99RpN+LQ~S!w2cir8uG%J-Hm z-AsCm$BZUK%YPN^P{b6^?oBEgcFm~)XU~b%ms8AFN^_vOwjbYgk>>k$2$Z4W1JE>D zM2r)k8)73xf*XEVs8MXfMgQi>1c<}6GZHXl!TDZ&X2Tw(4yeH!0%|4OC8U3P2jW~ zKQ%LO=IuYk-5Q036GwWQU| z^$M`pKi2@NZq2`3>#Jr7TjW5OObIMpX>HFA{?_~7&7CfRf@Oo8E-MF-t%r|i!d(G1 z3jwP7$>-_mYKQH&=`nn=;MP+^HBLGMwYZBGx6@Waek7*Jd$%LK6r#27ni)~d+$2>A z+(L-jF464odI`P4L6R!Uj=n5Z0!J3)k%4hFy7&DLhC}P~ZM*ldn@)P%rTyFEjK_lz zUvsnzOm^^ve;gl9{10q;rAL}6#QcnJsvZ3O=z}R~sB1u)hK=1|#@wLa=cnR+#|t|+ zI=i}YMH)7-{@vQ`8mElSYy6~-tv&I48g^=@n2qI7F6p0!rUu}k+B<8>tw z3b=A{WpRTc*aQzAWb(7hme>Bpc*yw9F9;2P%l! z3wSY#!6WHIN9jPT0Wf(BMapGfnzdV%kX^SNo+g()mD0D3Ng(fs5|or020iCd(@X?t zwELC({1uBfWQVuhu`;(s!s>|mI*zX!7fD}JO2RM`r$kJ(ckp&P(p0Ra^pr+vdN_?t zO!Cf1bsmzsopxkXFRZbb!(1N`9K8ZcT#p469rh4LA#w7I>eKPW@?)%V+f`T1Ef8uS z13mHUbmEv2@Zyl)a`Q9vT=cBSAblH+sxC{c=3#RTm0(*LORv?ow9>5!?hw{|ND)CX zP&oLFSR%*FvCLrNfl6UGnOmlJphm4~U{Ss5=A=1HnlAaFz096pAQ>ADW5iL@{*pGk zktiW6mYJ`^ke2$$_U`b^&iTc74~0bn0h@Z-Y@Yk3$)<&PR7~PS#W;1gO@r-uLyaa7 zLxQisUyEM3b1s2JtC}g&ntE(4m+9Q?mMknKwV|V8ttj~r8e5KhGny^LxZC<#BffRn zr4CH~g#@MIFJ7q>xcbP4sK`8&Fo#147J|^jq-iU+-fkK%+W>c^STbZ0{yJY}%Bk`; ze%uQmJLDVqi^aK`#?<@S{GsmtXQ#|HCto6~o$94@ULmmOv+jEP*Bsw|`4WvG0;L{j zXcg>Y)?TbW*I6o()1mZd+qH%`Ipa~y{8tE3FS!zCgn`I*R&O$TgR%%7l)|2U!cFnk zv^@=lCO`$)#oMMiY`Op&?ooqO6n7SQD3wnrV$IRsAi#23EMWGxvsi&+ zCS z(%u}Fs6SIttHZ*V&Q$z?xfqF;oXo^=cpAOfEi%q`!`6!@`N3_86QX9H=Y8WSzi8Ol zTXSL{H$2qCd}rb}k*m&1L@VkZhG;JH+#EvftvgcOZw%GNkT$|`w$h;qeGZ&x*Dp^*MAmY-F-lPx zJP$`nSZtVOmjpa<0CdZ@moN!i%?1bU*e`W=y=S=;K`zwd6w+&`zdeA)^o5{}`oDJY zjYM&i5g;a#Kl9*_*b1S%A<$Y=%w*rWGh!xPAWKN7T*Lkf99Y@jMvAvsQb(s5i=AQF zbSk=TG)c9S@)tey~(;l`m@N;Qsgg`>1 zRH!P6DVTyDf24W+LrobQ$T+=RC5JdHO$b;Jf1Q7x1=IzyulWVU61G7U zNGVc-aABEU?-{KNJga&R@RjcBmo3bVr&eRz*QlXvHVMJTvwXn-Zh_W~3|1H8xo7Y3L2=X_ z3B_Xlzcm>5LQ#tqgZ4uY zpX&%%f`CL)rH&}iZhpXoOi387jB1?IL3@VD(2?T#vXnqzCDk;fzFG&aMW-0h`eQ*>+1_IS5>^V;g1;PtxIrWzN}FB_g9 zXuN<5-DF|&3tF8awj0XJ`q;mLPH+;tg`?QhXcd;7Dum&!J}8(-kI&QbTTK<0b0;n= zy&c5WWB@8_tv5_^>WIq`v9M=UB=y)ETmNo_)7 zlL(^lkydWK(w~zK_%0F{9c%}PepA;nEzl+>InVWU+I3_mlF3Wzo*u%VCuAr+21)qs zU*?*=HxsWHSrtv^maj^p9i0eI4*9ZBlHMP#_sv;oX|LA)(sLME@xkS+_i-hBM$o_6 zOM7Q9H;R6#^Fk%l2j*TrTm_^M3bR0f^xIm@FT7_&C#)q1@RKx1IE|0zFiBe(z^EI| z>G51Tpg$iUkK_h_Cf`kd3uhzz)*AZZJ@88G!d=mGyI=SzDA9tM}A%nuJn)E!%X2HDYcRd6N`#{L> zak5T+Yohuv`0|B(Q(4N7D>WlBFA-KBS#7OdyV;VR&Y|cfzp}ecz2JNAyN9vuX1#Ph zg7|5DrnRtKUhdj@ZUktDQ*&E(^X9(Z@qKpY#puVMVB}0}M0P2s{0K7rB6;TEwJTeLotB0d;(AaT9u}j$y@voF)7;Urp zvCxNB3yEVvYq}WHbH2Pz^5>K@r+Z`I(H9k{cyUTxLlV0=z66&yH3bF#c^yYIABYoL ztc}U;c1K`7m{@{@aAqV89fSbBpm`AQ-I&_?Ow8@DE!3vWd^7((5rKETBm2DtKN}+-gEsMiYnV zAu2t*n_4tdG}x4LdFqxGl=C#ao-Zd2HXdad6`7xV7(Xe9g!{)&r0o&XaTtq$S$jy8 z<6GaR=lWV|g@)$c+GdHJV-`2{jRDh5>p`sN!$^1B)BN~DRqBD}?$dGkMmn>=FI=U# zby$wEP|-M+fX(=&WfStHkA+Js0odS%WfyV=pJl zmDi`JMQbTb2tD2-zq(h7^RC|C82a(YvE~{1D=GJ3TEQ5q1$Zc2Fx&P7l%SqCLEEnH z;;)y(gGx8?ps>oZCq&~^eFKL3N!@Q;56=3-Ygpb&^f@eYZI3|F&#kh>cc;XGqQK!+ z)aMegY_Fke+TF`0>z>jHs&dDtr;FsR@Z9TeFZHHWXj+Yu)5LknWo;yNSi$A{F+5Y0 zEuH<|*3o8U4wy5YfwJRi_Z{=*T9bC#&jGp#tQ;d7xyn+er!th=&4M5}nw98Y1THRG zJFc`$Uws%*tV@9&wNBvc)zi_-wE-U8pOe{vpgz*4;SwGu$$VYvzbOoOhlWnMoJ7Yb zWzH}4M;p)-QdVtm++vJ9gA6}jM~_(QZE<4ickyonjCs>;LBAcoi9Lk(Aqq{~lyDqa zm(||L)Zi!7h4v%*Wq={0s6pX|GXr>5n7p>=SIBSPg$m!TtWi2!#R(%4lvRhO~W921N8Jy#NiEbH23o z6iRKc*cD*)ldI{4p=3Vh*cw5rt%8{ibEeBu^k6lml;P70PgL>I?TFQ+u(E;32V$2h zju>dJvdGqo`H&?8Z&bDDP}Vy8XeM|7`n;b{P5-bP+(IL_Q1B>GpEfw$57#s}MAwBG z@9Eqn-|E975)M3CblBj(J7SyKeZkMuP^`-aY*b+rtm&!5G`64K(R%p3kHdS(Mu|5Z zdKY%6Ko7bPg68mHT^PJb@}4@$%S)#(8u=KITwJRMXbm8JLCnzOhX!Jkr#Ji=qUDi$m0E%8_&^xlP*SM;K3;BmFgEs{>2{kDXG zZh#r${-r*nXF_E#WQZsDb6Bc@6Y?u^)kcWNeCUzA32F1Ud-S14c~r14A8P6LA|%sv zj3lgM)&t44Z!)V09l`Xd>EBr$EMx^K^r=z5-t~q{N)ymrev!Xvr6zp$@p+g%@M8j`f8l^rASqCy{x%fs_Q36WxG0Gn>2|W4#-LySO7gh!oL#l_d#h_ zQV^|wyp_5^o<~oF>+kK^9FG2+!x#+Trng{}l+@W|*KOA}U5%^2v7CNnOi&I&y7Mtb zl?}e;hUSKdQ|?w`B!Cvdqdxe#h(&nB9ua<0SJd9~xt{-$dtelf+2bl^|7|=yPL^0J z^E;mG!dPzIoSpkx2b;_^Lb#MJ;ei>^>zxz#6Bmt^3NsLf0=QrBKaGW6W-oUf^|4K6R8 zm~-}iyiin9keIqz30bn*lsc{+)~sgrP`^P8!>DTM$8GjSVp)$CD>ZNSm&2`CRXZf?osKCj5jU?P^&=9$~cFaij+s%9F3_~EZ}V9MYnZ|bNa3IZkJI`;p^*dBqb}g>>sQhjQnlG$5ypLj$-1nR3GLcG~ZhuHkX25|z2d zNq#KP*rQD z#zF*JVzM?cUdb7KlbITz3f*wv2+A|41S?ctFOUMrS*rxjK~M~C7TLW_h|N~a9KM#w zkUh2JY@iIb#ftCFfpGhuuyN54cYT*W0w`iHORaBr%z==pv|+H$U6vez;0W_}fe*Ge zfaQD6osqD~`Gh!lcc9i#mY@-LJGyfn z1n!5fRQQu)uX7&yebymDPS70HHV$KX1Uy{J8jm@?i5Z$Lh0qZ1czhy{>=KVfVUv0p z?Sk&ae(ss~#0;N1cfJO6Bai4j0xL* zM{wySc5Z}2I?)fnm-jPIs4jZM7-!zwKt*r0EO?Lz5$M;Dv2l*1?1~qhmC*vtu7ojK$tb3 zF0bb>eeXhhYbe(DHWC1uAI!g#YJ^f|Z}VCYa{h|d(Pj`ED=mnvAWAJ7vSuoy8s6<1 zHa431!B!9KKXvVa*L)sTZF1=tJLnoA*sRAHR7qGFyhnebSxIEOQeE7dW<#8vLy8sA zQ2<|29MsM9D3eWQM0m<~oi0ZnftOc9FgZO4FF`7vI_bBh$$q}| z>RZh~?gl=k|0qPZ6|><1smKd~(@fLrgFo9WNU#IsBR-QcOY(s@wQ}fhkWtGE|GA+qK%K~SvNhR?MW!R!Bx6Pfqik3Cp ze_SxgnOU`O-Y9sWNA&(V02&=$YK6t;*SzL596!B1AB!wN#$Ap~75@gg?Ci3kQ#E7h4+sGwU7#jBQb$Ye} z*)tWdeRomoIIP)}O^*gS%+2W*)8+uz|I|*wpYjN5GjJjAH9oIr?&FU(ZvgnJzGVpn zo&B}^x{_a~X$!bDWQASAkkYBFkoxi9~M^w(NtgvbyP4-<98At^y`4FQWmn+=MG*%UAlv(aDh zKDp8a@5ecqDDY1uiGd&v_ej4JsP}TcLbMHUVj;@L5@&4c+-?3n_~N0{9QeAM^N?w7 z6UIoBi&ts~K8;~S{db0CDq-j@lHyOt z9hR~v0-FNonEq~IScCtSfYj4@=)3lOWvFh`>?c@eG5$sCN#!_RCExqa>vX@HEfHCzjn88T18-tQ=s}*A2|CA>#^l=wkIidir5Q!p9yj`i1>Js7miJ?^(q$oQ zS!A2bOqav!iyg&n*%PRosrR4T+WMuw;}Nl*BXKIz31hVy;sDAzDt+VNYych$hq`A| zI&qL`Sn+7dUia#zt(w*5g$e5PbjQ@5g59&V7FSnEI0uzBF`jxr?l_;h=?v9^H~{8} zCKS+p(Yv?*8PbH~N~n5$5ZK_otGF!_{R0pVHrVhgW2WFCL-@HOe*?oLQDMBL8}tWU*X6RERb{zEJDj{RS+U6NSRZ!EmN=%@rwaEupY?Gc1m74?5~u z+6n{LF|7l!z0v2_07H#m9EshSDl{_lDL0(-pq%1c`F;H-)_M0!eNS%*&#^y(@pe*$ zN96clRzK`H)9OWJTfgnL{}ou`&H&k|ue9Suv--A@HkOXXvUG1}2nnQNNv~wSZxCOQ zDfKZLALMBg{Ga>B{2tLDX7GL3&Gz0Y!wefL1~-v`jPc!z>Nj>Q#3rVE4swZB)u28? z#Q-v3EPQ^Qq0h+MN1aDT0g(HCn0*TW+O>idsF`T4rZalTJ1GV;WCg9sJ?iOHtl~Mh zCAn&Q4@>96i0iD!c?%}4*h}jMCa1P}FE+>qmmm`9WgAu%kWGN5mb(E63pE-Zsn(W# zMa@I;4D5W46=e-iWPA;8<2wQz5 zz{b|RPOVm|Qtj_D0(vq<{*N+i=y!pYek03w!**e#6Z@OR*3;Sb#%g-RiM4zMfOyFfe`LFZwHX z0l+2Gjw4#nstK0rkC-O;)f9&08yh9n6IDMsxmJxU3Um>Md~K|FSwb^}u<3>-aW?pb z1oULq;0e8-(+nG{Nrh=7Kbo<`==Qa~k(d5JPQNUCJ^EM0>B*W+eBS(eh6-+dAvj2p zfhwb;3yJP!MM1gwj$FzAUtc^amDtM(ffv=c|YiP`2r%u(jP1LwmCj#CTc(t)xnK&RGI zm(oWdYfY!&@v?{M#oJU9;ZEG-z%vPr=Y8@T$N@7JsYXoWVu?`;O^DO=_lH zFzWvpxR`*-`FSU-s9$~^nj!3hN+SF*uM9%FGrs0X)=+GA#9O;bPHsRiE`uJ=Yrf59`+IH$k<#s?9J|>aHB6CRm z;0i!0Dd^}&m$c+|@4h8Aa&)%+Kns_x)_EZ+-^#>Tu^<5N)cJFhBK2cC*KTfJT<-8X z@8{c$3cc3ilKJwMbTcatbm@ooPU|HYzty2Vv-{ArgftR*QmTnCmr_%fmOkcuHd3Mo zC{ymyHu+ibkjP?}NZUJP0!f#fdZxeBBkL^23Fu!(YDnqfcscB!zDz0TXAs6oJEs>{ z>Wk67J`g&)9B_vVrd37Qadypq8dzr;l=UeLsCm5f#S<;!D4qU$zP_6Q~{`O%y zMmc@3TssRgM2-%}XH8JtHqY6$`kb%`_>#lJJ}QwEakpmY3P8GqLS5U1?X2=9jtQwwJ^tTo&{#^jTf>5S|oSX9^b6)AW2{Q7mB;#7ugc@RaPC|JrSLYJfJR zh}8eh7*y~1XJOcL3pu}|^~atF*& zp1wl#O0m}Lf$a^OBdnJ?U6e>u>!xz2F7SzKw4Kne8D~(=(h4S+WlZ5QO)DRbRKky1 zxG@{q{pW4&T#&~1^rDkz@w`YyjS*Rp!V%xnmJxzGXqAJ%?-&&_+O26 zUc^qNH|U2%4Vym%XxQZ? zTH^=R6e0_<{yF5+KH?u%! z@rkF`($O?#A5}LiF2;a^Evj!y2}2A-Oqep6noe9y%fbT-+hJ%BVB!*q=1vp6#j;q0 z6;13!a#%;HNUBvI+e;4ZnIY6RMcVgx)MJjQjsPm$tCO)BciqaM3sR9#iJULBXw~4o zO%5es{dTJz+?rdv!l5lp)V2+vu1IMds&`iQ7r&;6Srr z_%pLProK+rgm#winQ{SmXk<}K^pD4Q!8y$j5{-iICw}NuBt0{gfoIBXNtExf->bE9?o9Gi#IF7kDVYyI6H(^mQul8-5%Oh zk-CyzHLpCaQUlg~F)Fd*!~c5OJu^F@R2O2$Il7c-mBXp#4R8#N+8WoxFkz9{@k9tB z!8$x8L;W}vtO6?>7I1QC-AISklL&i4_c|0`H>o*_(N{;9(n`^r5eV|EWgAHqDLvro zxn;M6xr&3Cg-QUc=lVQyJck3BOwT}&XQGg-__RY52%B@_kY z9tiyrg5$lck}biOD11JVE=yjC6_i6tk|1sy8%8o$7aBI*N$M-%dDv;4jTULXn(uoE z3Cyq`g)M?4ML+xemPV!5`n6>LdfZ(ehqL~2I5&)M*K9_+J)U8YA4N*v`9>dxiRix? zRlePt5=L%T%R`(7#~9h;BXSj3E|l86K?32+_@C79$WzJ(>^tSpnlRNUH39Z^&jUH_ z^KlKgwEy(C%^&q5PPC=uj57%LeN5VJVp>YeF?-(N@>6#mqwf(_)Ous-3P|ARRV}T| zeX2DGWsr}Ja-)$CgHFlr6T;0W{bGqunOj4;T#QhCumu4F`bI4kMf5O{6a|SI@XF=* z$bKIm9)uc}s7{kX%7Kh&qOJ@ia>4Oe2)Mv)s&ZIfPyMtP z$IjElbMr9p&E!@6IUMEtE-JqM;jh8Od$5m3JU?s&o9@M@{K?O*KGHW!rIHKSKabyo z#K0le@!Ed3)8%Ua_|xW3^SdQ5;6-9;WZn13Xrkq^WZ&xY8V+hDbE^K+zQ?zR_Ju*Y zR;KBmF!@BKwF(0ex$Fli&+$$bj;DACw+_CB8-nh|f@QK39}n4OAq6I^@IvXeq_{X7 zd+xkytnqc`9rKCuzmpiN3;MSC{o`GWM|5*L$MJd3FSF+!;FF;w1$gwopX+K+lWQ**qU18=bCj~q#s4%BZ{~(^8`tm*>GivoN)?yXOb2?i`S=N zw)+ptGuJUIE@M-K~WMQOkMYEzK6DC zCzZ?V!OyPNhIKYf}lk%#SSXus4Q!v!iG`A>KqK0K^q7>eerS%0CE3~Nz6u!NMbheAyrX}=;*0@)zm`Tzb3v#KlmW<7egpSagw zyN2E$&xL5tsFgb+Uet_$2(o4NdKesE>Z+mCbBH?&Mfcnkcz+k(9+T7Cm;Owb%ggHu z=Gik3BKy!4o=RLZDx=l zTZYvx-*U4-3Es&jKwL;E$}W_F&Zp^VKSsu4UZzG;=<;;c>s?fu!RZWdLP_e;ZeeSY zQ)vAd`Nx=nHyS)?HME9(zMQ)i7}rv!U+R5+(T4^&&&Or!M&fQf6Kw7WfJsxt+a>zSp-mx8%=XZ|Jl0@J zFK9`!g#GihOhOX6`K9l2autSQwK#7U+mxxxCnyN(4dKza1zwB)a~e$sn=Bpj6f)En zq|iy)Qp(4^=F+?Q9337B>~!rGraXPipX#gbv2?xGJ`{R>#C*g*W6)D&`lMziMo1>N z3)(~Eou+KjO1`16I;=aACHgF$s7cnjX6SB&krte_#?p%( zUx3ih5nnNXAyp0M;nAw1d^zgCQfN5{)OGcwoYG1)%!bjSJU1-*>j!k?=MUyT9>(uR zc|O4jr>sQs2+%z-&CULvn8oXvAChjhBjr?5gnLi1t<^l{6V1)Vkh4?+VwZo*t+Ae< zu%F4lqe4HUgjk7933f^n#yd!aHGwP|OR$O=!gP$K5KybJN!2K>&6eMdx`HlCx09iu!3WFm4%G`hrjTB> zP5TtiCGXyQ9h5HIyU2VrEi86z7q?k7Hr2c|2GqPT+`i`;jWw)ve<;9?VZBIU9qUiX zPSTp`RG%Rg9ZMQ?*;|DoI%o$ycMbH|jc+rvAvjZro3fZf^=4?HKpz%8o1ViGQ+Pc( zE;P{^Ha0|IC72u-Bho4x8^*Qi&7kvBIfU|27&P{0x@)x3BO~+Zwd?7D_lp6Pl=8Go zdbBAyV@`+$y4itY0(bw%Q_VL;P;V#{033XR3KX$1Itt;;Dser%n}rKIO-xd4O`cHe zfWLUGY|AS@bS&TpU`sdzE+0AY5WZDNCaiZlHmUWPD+7VaPDCwaNH@IeiTmNFO2nH9 z00Fh&1MYaut-!14OFc_Ua33>sCpUBdG zP6&e}Du(wa*J>Fp^>mqL?STO=5^dxaB=Bm7ya505QBbU&R%po4wm7>?vLV=;3>2?m zKqTkT!pY~APj!+qvTZ)BukqDxRpYI`m2|Cg`bnBE9#_oPDLId?SsG=IIJ8hi+1j zwNZrZ=6A%sBC(4afQ2C1vN<4hV-elMQrITK7wph@ba2SMxC*+CR3YWug!+zZATY7W zKTTQE_`7U5w0R${WKLX~s$L>F+AdFZ?#>bG6k4Yb(VTR67SD+ohzD}PBzlH7w5Hs2 zsyu9n0ZkU5d{Yd|i>6zDnco|7teZZsl(_~bJk6ae8y!xcwdlwCQeMMItHb`PSwHCU zce{fdN%C1m`6EyDMN3Ig-i|B}WLPiD6L`EpQzOC~Qs{Pjz$ABYyJ^FIL;^uF=!Tr9 zaKG`RWa?CZ*%LZ9o)TF^n4aT)Fh{8wP>SeIPsBDy($4m1r*pjR6n1oV%y=RYAnBw0 zgnU|?pM75)Sh#-@Ne(I7IoPJNf$>4I^K`i%Ofh)B!YBtK+-tMD8$e1MM+N%vTIV@?U1#twIbw0zp(OFtefa@N2zKd!B(8gN3t%2;BO9%1X7#nTf^GC-xafn&+`15k5}W9*+(kn|NOq+ zU-$^?EBLvT4gL?_NH_6(9mvst`oFAChT|Obu)t3_Q|K9u1U80txA@tVqf=7OShj7F zvgwsDi^27w2^HR5vT~OT4=_}_FY5LNqE@pQ+%}q zpXl7fAd_Ia-E<~-tk2XC+QuPgo{bug9XGhCpBuPHZD6E#xUnTfSA2-YK55xoNQQJq zqC!$HooApbsI8owl@e=;6zEGTLJ}nrooVQ;m>So}5;X{Q1}?Ivjshg}JI>n#L{%%5 znuT>iv(Wiq*a|+v^O3DBDEYrr7~BNtmD;E-URn$)yJU~dOS1~0+p!-B$$~CgTc!RmBs9BiO=hjWQ(e5Sg zpszF77^s;5o)`)L(X(Y6yOcix)aJh9dw63AxN%#XeEnFK4pLNirs{wlvLSdNcJPVY z+|N4{ZB5j>^AD;|8jN0)sA$}-^c^rVr4I{HK|ws5&MS>YTVL{>BYR&~4drsHF{P*n zO+|`fqvyANg+vDLoxX-Qb`gQu$`(hp45fDH22mB z1&E8;Bf-O^K@#j&ze)CnaTMEZI*7<~Jk(|zGa>O*wHd+VG4 zS{3qBeoBq3yY4l4%x*;A+bA8rkh22c4xZ;scn;$&MvhM>>UriKvcP++nK5DgYnVVY zZ7vPWF`e?jGv%h<_VKZCw2_;9d4RQOv`SBB(73UqB=yxMWbK{y?)7*>cb~32N{d(k zf5XlyFTtut6PJ;269MVAVTeIb1YTGx){siOBs&+({|-cGPxAL0^D7KxWS|7 z*3oloqu^=j9-=M$v{lgUE!LCS-j1WbCYYS>InOZqEy=`n<%U~q6vH-nQWorNlB`Z^ z!Al;vPZtEPDwKq($DRv5Fx@`o!Pgl7$-Bun$nCPqj0H&m$|ilV)Cvxjd?=gPeW0$J zFAw+_63xZT=g}x36=&XPETGliAHGg}8 z`AC8bMJ41bkbZYf$gp#n=QZ~3SSSKuZ1oNlvBsJ#AQhYEr@l4)6dnz7I*Gh0KfS1wBt8S3haTY?s41=$Oq*>^u?$Gq-EAdBwO-iQyg@u?Iy4I&%V)NE|`TT+^ zWOxh623myaW)ntI!O#<`Cb;)aKqa-wBmGFca_fLMq-*`i5*&^cp{Nh*G)R^nnujDT zhQ>*9GGbuS2-?LNpH|^N)oh`0s=IM`^uGdBAVFrmIkcV{A)}>Fx(7bHWwtsbxu}vW z7$Ke9aC(B!1&xf4r)J$3B4E%N)4}tRVAryH#RB!NC93(!9{p(VmJ(8F$n&9pTpFQ~ zvlN*{?whMSOOd*cWOh*#EmqMtP%~3=rgf)aB}M3dvsn#Gw}sm2b1mU!w^5V0=>Wb= zdP+*dUFJpspRy3{tZCPWBQ212Pp@6>skH+>y0&4tO9hIH&MTmkijW^e{vK zHO;%pAVWwj72rNQ2Jn#@+Lzm<)ZxD<3t~fuXoTpnZijDNVUg>(2l={P6XS0@h8uE2 zR#K-BpCZEC@&YfJr3nmHG6kjo*2msKKaN@p{-m`c)6K#tk7JHWhxaj>j`{N4h{V!m zkE=ldAVO&mWs0&b&EHWA6q6PM%E$fE1*-Z%q>ZC2%vD)884_I2XQ4N)x?sCHw^RqL z`GeBk&&V^w{Cna#LX~;x?(Qzo?n}1$wM-IgXi2zGsBCRB(ixFQx zpzCy!iGkQgqTAABKOe>4VCDp)!WQueT{=>Ne>ivcf_h+<6jts5eHZ&*v$gTh;T3(8 z^fg1^7;^($EGf&e6TdmN-?JA*NVX6xF0w_gI zw_%PEYP^|WHM#gNHCqcYhpQGZO*st4I0Ehr4J{n) z^$tum(zsj3RktJ~$(XsJQBRep=@DLGg3T|X*8j_q)(k@7IDI$6iqgDRWf(x>F>42j zr7XCbPC$NbC*am|aPCOhqcs129p}^YG#m$w?^g377Hq>70oPsTp63rY{ z01mLe#mDPiLtcvp?wHP{YMhoB2h9N!(KMIjnga7R%!atw(i}q))MJ~71cuyrd8+(A zf_<*(uWny0Wi&@o)OCp89x+Z1p!zOtvv7Do1b-9eUVSXV=(;`XBiyl!i7#7%g&hj` z`Gvn4lq@#rS+cq34iQg+SzwntgQ4%zO9o0xaL}N-Ve7%m$_#X7rgM-BHQwe)?dOk$ zC4aWyHo@Y>Rv_lQC4A_SdrbQ!DR9<~PRWdtKJU8y$1&}}*cj^qS$I{6^S`|@2h;IJ zpnG5{*)+NwC9b`Cz=!T&4!6P4V<@5)eu6gk}nrcbB;vyd9mh@ntI^+6I0> z*{d?)uPW#|p`Lf;dZ?ZmfJ)ZfhxskXG1!t;+KBWLx#Z&Yc972+ znQHw|;|Y%-!u?6)!zQTLx9$K648cCUL>eAs&69J@(i|<-_}Mi0DROK>_3McXjhw?2 zDsSWzG|>o_Dx4Ns_L4!mQ|o)U#_XzhkdeBCPl<9!`h9A4Bg{aQSMV6_?@rHPbB{QD zuhoU)749oe5h$RroG%Zcys4kaX7It?Bn25Tt{{))kk*TKo;7B|mDGnHLN0&4ri8DV zv?qq4zOQ!RtiMU_`g! z`4*M$YgIc7h`0CtfzrQ6?KWzZefDhJ6OzaC<+H%DU`E+O%x+exi^OXl}usQdz zl!OW0+ol!c0Mhc&sb=8)1Zd!0WVo!@Jv27M=UaF;Asy0(|#X+YlZ$kSl z3V?uC>;W6qq9&cX$iJsl%!`3VP^&>#sxI(XmQ|iH{FjAy>Pr>Pv=}q%!hg*eCN4;A1EozXS95G+6Tj3MycI zKbbK)p9EWY9keJd_cy!kcK1K|h&*Ci8(rKf4n{pN(K5D}MT3jWP^P$rIktS-lO5}l zGTot4pDLh%G2tlLf!Ji!x^X2?Yd)ip>R#^_q7_Q?;0C{sHSsf~qj3PprcOC3h8>58 z{k0e&Rd2`e&vUC!eyi2;*IugvC#Bm?DJmX{;e3zEY=38_{>j>HZ}q#*Ym_nHaGusv zLy*v6iii(TMq9Kd@V9;DSk!L@>H1y>I&=MwM^t}qk)q?uIYO&~D+UQMI0_;vue>%K zCJ})??4)10i{7bbW&;!R&jdJI0-8H5-#+)BL#g9wBwo`eN0ZIyx-Vi%_HvbgpuDG# z9#p0hNt5Ayp&;U0b93Xluw@78@7{HZe6+vq&gbZn!uco_blWNOn0I-5xTgG1sFKu! znCJ|(jUuag>AYIR;EM}+Ns3&z1Dc+UC6dk-8QmMs_!xH9Y7&`AcDsu3VKUXF5gyW; znyc)uF_E5^F-pT@M7q!)#<8w_S{@<2=|=w9;#2Fs)*P0EQUQci$Sz@ODbDCpR8=VY z;k#b4s_m9fJq_z`?CcOk`#VXX(%P*Fi)qXy6(4A&^op_)UJD1L_c>-&yP2{7=20)T zzyw|LkprO9@H*{i0VdB4K^4B*ecT>al4^n?QMhY6DJK-chg*U4s6sZFBwDNih zmmyvbd|A77LNKm+1e|hxPgxKP%=r2uS-U@6@8i)Hl1pD>+R~;Q)VSyKkj9Qw*v7w& zkC&!9Z}aVwdZYgy*+3GzDGR}n-YG(ZD8jP^1KIPrX`#M?leizl zk8KDubga#y^Rt_$9M55P9>V2PjdE4_NwoV@Xdi~uxZRgEL;4J~7~EV*ui_vo@9QaN z>rnSaYY(!L!D3hq(uMo(R{bLIMy6zcbU>EN@=6OF@1x{6EAS0nZ>p`T&{`nA7JLhH zO0xa5B+VL8L{&I!ix96f}^ zL{m46(McA1W5)tUCJ>tNCHB}pF@7&jmzt@vqUc%*~^E$fw zAm#$XOI@aZ4-(cV(0Fb1;MXXth{N|!Bw~P<^mc0LGj3rzHta}u4zbDhLF~PkWGI3e2cJCzhB+=>@u>O) zTQk~B8+vcSZBH-8$Nm#Rz(Rzqlvy}TmN&P2sgG1#c)&6i`PGrvbfay_k-QWyYAU@~ zbcDWAv+k9S0cTz=AkdewzH&H}KoTKU3c?L4pUifJL7L7c8}^d6H_J+|ihM-7q4w@* zf)v>`0Ub^brH1{}m-JPlF=59-2@or}hI}PW-sjw;iTzaKzU>t6u8I1t3Vv(&Z`W+F zbdJwdlxy3Ma8R62fhXt$kT5y{-+8hX5S7*EkYNvz6EVmbrBEJRWXaU|@lwS%?RfoC z%COPs9q3I$jcAi|x{UWYs*r8FZTdQkjH%LeA z3F@ss8UT5QMyyF~iIy53mU&NI@1}lhq4aEhD>on^)dj!7P3FPWFFpa0PHdGpviR0} zT>kx9NCSllAw(ame+Fg^-3tfzmnGX4*`Zd9Z-bOTyeXti1RS2PMY18}g7$*cag1Z& zY1Z0q$Dw|x)c{-qusFaYbiTtAii4BNQc=cq7U278Qcy_!hw zVC^l5w-`U~w zP#-rVA5)lm+!4{q_=Zjy;O9DB=6@ZwleugC0+r1JK^E84tF) zmx@dRhN(Eh4aLmw+&`D?N~zFI5%UYnnqe7Fz|9m^-5CVL*Cfy(2#44X3wb}lS{b1hng@rly6#`H3rq_ z0E)8I$TNFxYkKT3jGVK{6pS4tr_l_T0T)|{Nk z=2u`A1#*ANf~y1ILvX0gX49XnHK3!AGKxp5)v$l>Pb1$`BVfO@u6k&;r1DgV2CIxI zT|FH~>5#!$GeVufH2D#|%n&%oXaX&lmG(@hg+YBhC9fr?DUl1i zH4|WQTm02hO5%mxW3(qPCa>qBRlNw*4646PQMAljw}sc7*m3U-jm}ONV5@2YD_Ou8 zbF~&@U#B0~>a(sxTT-s)Fdrr=yx(K@1|35zw#!MwX(10-yQ=I!8ymx#ZtZ288U zIPXz^n1AMR39*UO`CD|nC;(fx!_)anBjWxOb}4V#{hU@ExJ^*szHR!x&}`; zd~xRwMIBg|9QBh?H?!rnN;U%3^RT~2f!5mk=D@U6Fj~|`j3feLP=hrkj=SB0VlIF` zB`8k6aVmC7@(X_daeN+ACzZ5` znn)ZM1=EJ-z8sTmzilh`0TY!m)hD9gJ#?*T=x@|dL#JrGzx!M!R^hlmC+&}+85Xh0 zf>+8|R6`Ut^muBUpuO=l%eZ>K59&?X3B?6;f4Ycc?=Bqhz$4Q zOvoDaX5%-k8UbB8@qlLHdhOkwyxa7D z-*-nfaTv~R=lz|oXJ&m`cgYFmia0SXwPR_$rz5+*#YHdo7H^qq*?Hevws@?I2i&O5lO^EPKfH+%G zTPc2}_~fA%f8Q1`&^903>PY8Yww?MoBIK&k02{PgC}3oY^%}OgN!X)g)?zuYF!E`F zrBa=TSE|6A$~It_e%Z@hEeoAEx2X`eO%LuRw0FnsA(XarJK>&s^wT&2O^O8vgj+V) z$anOIL01ooJvw!mhTK*^Bm}u@xq0ANJ!Hsr09( z2tsxLTcOK`7&M=n@)+mqB;W_w0KXB!QPq9X;4^;4UerccU&Eb2k;SdtD2e`}6vkhTeb$X9#0 z7znUR6)e;}w$jlRiObS9R;*d6#sue_GsTj6Q(N&(#;`5#;rTqaq}&q@$RX(V-fe=t z>~LOd3K#0=PiN0wHvb>|n#I$R7dhm_a0xcK;HzjRlA~e^vxro>*5_VhW83OAwF4%p!Vq!D>MZLLed7WWk0=kdV9vni8!^T6`WPG zZ~~LkBc37MeQUHkg}}JbMCw4)qrB6rJkLP#zn*l>@~h2-s}7K4>N$ql@9K_LsgnH8 zddg3SfnwjQ%$BfF>U|O$;51vRMR%WJ$b`M6N`u2u15i%o$eteT)l*X6S*<>=Pvic0XAE~+*!HLbZ(R} zb8*-~hb5ZO4jsRG5Ju`cj!&vIE|=$LfxLRz zL7kZFm`nG=4O<0Z0>stToh-fs0a8xX6p56HvV(D)zVE(+$@*)wEK*I+X?N* zh_L{u!EP~I=LSS;vnZT0y&9EL0M%XdteQd}Xw61kwroyAf`2+U+!C!>*hRt{@?d#L ziXYaBU}w`pkh7$X-i9OfNsYlVEF|;0gLB3ZTAoE_`mX<^3++<30bVRFuZLn%NITwo zJ=2fJFp6{cv3v^7{F5p6#?kszTcq?qrX&qcn|d_;j8nhsC|Xq`Qx9d%a82`q)Vyf0 z9#KpTxC6I^G$G1wJ`DT8YNksEJ6g6k5?-AK2UHDEDMP9(IJgu~Xjft|>tpXhc^t!R zvljC=f(>gRqTj)3I6UlQ9NvV`%HVuUdIq;yWf>g{B$zALvSQn>Qeb&}az?n;Q8cjDTS7k`nhKB^%aBfQO zO?C5SXf)SFWZWszfqIBtI5MSAnnW&2H=Ivv2imLs`*?OOr5B{qi7=4qq%6aM3frZY zp?l0B0P!V2jM#zY}2F5z?hm} zk%~swrLF|f8PIucYo_TZmT&iyM832xGqKRns%1Z z1UrU35tN;xn)*5cllP^*)e8@Ibn>}A&@d^1^J2&ReN5$Fq3DOYQR#xf9H39HIS&ZS zT;S`@(gl;6$Rbe6mf*ipmfv3KS_Ij?d${%_7#D?XJcbR3oSXhmg)KMGF<@JF<==W( zn>_7_f#^`iMkmduM^Cm7mJ1nI4A<|OP7$?~s#B(N*<6<_VJ7_9DQkEX_M0sm%D7~f zPM(UN@I6Td$f~90v+jaO6k6?^W;*=1MWHnZwb#s= z6f{k#I1izY5_CB2$azb}lrv{oDK`_(rsG2A6_u!O)fBb+dRIvhi@>M>Hz^!wb!E5a zX#L8&n38|_Qg^gx3@QAexDKwPK;-5a4~HlkB||^#dCJ&;Ko^;a&rau2UG?=GwSFrL zV~>0FE6HC8-c_?U*lq&<4Ou|!JxhkH8$wEATVa0Jhzmc|sbXW9tv61)i{`Ym=(p>S zAOSo9cSsaGxM{gpx`*e_Eua!sFW5*SoUmW{TYi=xo{^4+^|qTrEOD@IPSfvH>q zpOhZPhA>P#OpfJ{c<^Q0Glqbto+5GWeo0J=8ED$?I_6>=gVEtHy+~~@x6nG~ms6+; z?w`KwYNCJuJwU?0iPCXbG)f#-<7|47`WXIH16MWcp3Q!~xi!G7Xs|*j5ZF?TjZU*>kdHOX!zhPOFx< z7_8JNpmkGs(5R@I>QYkHme=I8*k7LgGLn&rEV2=e>jsZKbJ31*%sz zu;0d$PioQh7u&cDwu&N+j^FT$Wq{I-x1^FOxKD;mX^SzN>2}|@jbBx>P_9)V+d=hn zG&G(bf9$Uv;)092&B4LDtO@(uXqXHh!@`w}fktwqI=R(WJCO56lZ)2si#hA=L|>-6 z&2Mw#RMliL>`D4&3)cY(TkSSNaM?+W@$QY5sne$$5zuu?Gr0k zZ+gLf^v4O8RjebPMIX-8@3u_a^s-4@gk}&u;=mgUQc!|2Zh@Zl6Vbi7#gH~2v_94& zixL-T5JHR&mk|CDZh?Odju2}5ZJ>X5a~zOG2VfSUU24?EvVTWRA}}7+ zy9whIdu@)Ehp+{4KJu{2tCdAfSC7<{Oe5UYf&EJ;R|zEeZ4+C@UT=m34f$CC7sI;x zw)w~U{EC)=1f){^LL+RI(BBj%gX$1Q4$lfgZO)c(j+}}p>`2$YIWzItSBZWZQ-KU6 zgC;n5A)#@gEW}Ov%&yIoAqHwq2oN8^RfC(*?i%k?G;s99s5D2haYaFtqAEu(FUoC` zo7!GN<9!uAHjM(0sF;b}2In{}G0m`B`HcU{G#^(lmNRz_b0z^x9%c zQ7LpPJYkK!*0Q;yXjp^8`%FFf6%U^1V_8CPR_M!XhmoJ6|YyHR{fv%AC zt+G)qPDx4Xp-6iONCY45re{vPiGW6RFr0F37T53(?caV1aORCtkiOm;N(9GrAB1E= z?Dtpsn<>|c#efWobiL8;!%Z_JvU);74(u=^v>1Q32{mTFL`x+k8!2c3O@BHp8;jxQ zuHSS}vFEQ*Xsx1mv%OY=1Tyz1F>YP8YvWr2%wfqL<}z*41Vz0BnKY5jwTLJF=G!eo zv_}>?wwT10L;N;b1m?xey*8p`p5IEH>4}wn5DP{KyIzh-0f)W^Qy+u#rgO>0T8;LX zV>(mu9j)|Wc<>1zDR+pg@tI5sRfMgo-BYb$L7D2a_Qu)ij7A%fG^qR~_ww)SRpRaq zd(B8Vf6ir8-vFTXmwm6rTjpa_hx9!SF{C?3>AmsFkbX zX1jmdlCy^%^<`;NPD{j3=% zGEyO&<=Z4Xmk@agpT*W|O4z`P4^3ng%)q;x<{ycMj&PJ`zqrZ+i=wn3VWMg)y^eBq z%joJxt&I3RI#Z)=&KcN{SyUci34a8q4**T=sI9cQn73ja5AXR~*R#lxr(sHmBySO- zT|epXXp~s9i72{1jZziQb-7yO@A~(o%>6y4&NgQlY{qVbYhUxQB|59Lg#*;=`6xd4 z=9eeQ4W4Nkrt~U%$9=2wLt;{FAK?-WVogo4s>g1x<)^ezd_9M8yK1V1E?Upu$F%B# z+9eYC1stlwHO)t`u@zRiw+-SeROb2CM;)9rMxXRHkOCdwEHpIlCR#UiOU{ijkS%#r z3iRulCxCn7BkkwhPzxl>nkXe@%;RId1zTX#DxjXcjEJ#>a2Le*n@OAm$&&ObHP4x% z$*6WXhr|juMxd;Rpzo2DqS^-cro!1W0Nc%qcblcMTEp*hE7oAvbI~p=1&IX@iL#M0 zU)ha^Kn;JfO)3-7`6h$hvw|mcUoG`H2!T`&a3icbANuH?>B6~aUO$D>d<^m7=?U+ z2c%E7GJo1%#GU`GBl$BnT9&CPXdTp`UkXJ!SI1FLY7N(zZ9Xm)V1^`p4Wv?$?zdS8 zb#d2!w>f!N`Gm?3n5op4rJ5%#tJ+M#A>crz@`zZ#%$%>CVM?5lbyw~&j02XT+`o+X z&D9n8X03L}kVfbv$bV3y?cM#(_^obXlpF)9+Zs|20;e`<6Q<($1+%AXK6zo^gIYb7 zJA{3k6t)eY5uh z(NO;p?YW<5qp|`Ulq8ZWjXDtDc+dp8!4<;2s6!3kZT@1)iqvw4M#S4(PMDL&t3n1d zs4lG!CuNtiWl^J7JyoXx;xVYDIlJR+PB{|qQV$RQvK>=1oE_jqMOGIq5e7e{>^zaT zm9JUxLY~HW_`iq3j8nYg{@cOcgOjydb+>X0cm#&vjedBUNPcb3S*ai_vRI4PZPf)B z1KSi3-V>%xnyu*Jq&y2B+%hU=Z2J(d##hi7LrMa|RO2gE)h@@5iV>rkVAM5IbC;_5 zV30^FPLLH-NY?RGM*R8-p-g%sCjL_qTxC0iYzL8ieY+f8vc>s)M48+#&S`){BmrK6@zFH%xBaM$p)imt z^q1jmY#i43a7y1~Ln+>&dsy`{se95n1}{tSRiEc=;InCK1Ez*VD#mUO%92d>pISe! zA;rn2Xjo1KMY!c2ObP~-DJw0d&mE=(c2TVZns?ChOvfY24hZ(L3t zIzc~7Pa6L=)S$vNBtb-rteX0pk8sz$PYrp^X14;Nc+4*t#SRCTM7=Gma5S+%^V|>U zPbApb_goKp0(BezGs&30*@4JTMJSqy4U{7$dcnfRe-VKgN{2Wst=JPPv<6Vj#va}P%ELe z%Xu#VoqC7Nq-X><^ahB;xbN$LvFy#grKhk1~)oRN166T>c62k6GRnoc~nPRfrFIiWRj zeO`9%HMuBmJO+W3p7&wjP-1lN$!4+$Q>#n0yb0!(ZR%|Qph2gTe{eEvkV3*R8;N@8 zeCC%X$P;_|VU*CH82CacOGF4yVdoOxWkmFbJ!}u_^^&kW$kRnt*Xn!4x!r73VGDby z7M5=*od$`{ipnwC!)+Fj8j$g}_UO zCIyx+L$OxlW>E_mSC{_n4}%2vwx8dmSewz=pxd)x3xNvtVZkS`84kmLj?JbAoWRcD z8g1RQ+?L$d%2CL8D}}}7DNK=ug=_hG*c^^ttiE{fPfdcj@05M3B~d{;1F|usPE%( zp9OZd-@B&S96jr~@856r6aZr9r5h1lm-m4^Mry;0P_VniK}r8Ia&aHWcn~a388kT> z9Mbxv>8&=q0GxA}hlq7n!{9le0BYU+6&?xLP3E<2Z)C=ZJ8hoVb5XxIT;ioVPNkR7 zA}YM0((%sQS3Pqi$C-STF{wnpphUdZtEO#IEAxCmd`#u17wT5NYlmZ zLRl<4vmcJnmsGCZWA>`a`T{;^_J&K$tgs^pcUdA!k>&w< z8B4rXTta;?hTZTgA`JNiV%sxWWnLcdQZ+(^ZndL*ffd+`l;41qvo;h>kN7rno!Q(# zPP?mYijhu2#KP<-gR<~oeRTJXH~0LNzUozH@o(aXlk7FET*)HJnh!3-9}mS}wVGF8 z3G?G>ZdJOX)(nL;)BRC(^f|Gnbww^?8-$=l`>~W}H`k#bkJ?I@Vp&gaiiz1uaFB`9 z0@#yeg%9&?G|`D2hoMEfx&F?xbu9uUiIrt-&o{*FT-M+~{zvfP#w$S>e8FN`nR7@F8_9BoX zTq4}@Z8<|urbbDHM2v({P-ibGeN5AyM_!Aqo%Jdq4YX*vRllFj3IJz5Bmy^fqzrAz^qjqr^?b4o!1b!U9*&|{EZoa?~g>4h{4EWjO za184I8uUmwHVUusy*p=3h*0r2@QjD%8_~67qCLO29#B6|IDk;R|MEJGk7M&4q^a`L z^tW+GU<^gN5DFVwr%_!4WU7M~m$5=0=nPB|_&JGR-OK`n~YA3Bw=y88gU*sm}{Bm?S zo|bH-(PcSV0z<>_tcT#@Rn`mn2~NtQHY5_d;m4Y)3Be2D?d_#`pXbv(L?5C!+4_r- z)}O||df@tp)Jdk6pjF=8lJAg}=0aL8#zo|c8nnC!#=p9xl#}r^vrtg+X$<8h^yQYi ziwsQZ_$W~g96AD?WH%0V!Fa21hawbgH>-aVo~dI|uYQ{Sx^Lz8m}v_rPUjl>v`N~a zJv&C-`?_}S5u)(V-waUX1W^Mwy3?sExv0V+aznpUH-OE&!sjMplqVJaU2Vgt@v7L^ z!3HeT>YwuN@GISEzFnKD<-jMU0o1z_8h{>_hp?!tX-0AUNoy&ghfb}5;p>dSrWaozQ))%8!*w5FmAUA?Y>gjIGv{=YU!k%PQc7*<{q zG56BB#HJ~g+&Z2ACF;T8DmH>t8W@t3^a(lkFp?SaxI!Z&pw$MVJZ}FoV_r8=Y3*3N zqDW>r3X4LchMZ*6(vb@GL=<`$rvtjsjuO890XnXou9uf7bXMsJgq@`&*3xY$uw$dtmB5Aj{XNtDNlQs(?y`MB{dmi&M4E9HlLc#o$n8&FzGtFNj`HsM>{nQ zerbKnPyvni)uH}iNf$E|d8U$uFj+$n$20Uwqu1*(ix5fF>)&>SR0kgldiX$L1nG`D z-pQq=4*Ok0I@MZ#g0V>CCg~AGs^Ott?F*nTv)=BXDQHN!x4X^1S~)WHh2BHyvNJvf z)#Mu*xFYqDjD`-yAQ>zRUW4zn;`bAgWVD z;T0&ABnUDIpU39Yml_XA3JDN`Ec`TFIH zw3n#(9RJ8gE?HB2$IH0it&Q3G8Zg~y*BE-`rghB=vvKIW`gY@MxQ1C#@Ji5z5$7b| z#;h2RC&<~IifEKymFl>9Z^`!6n+hi;57Ic1;47=k`tb(@*lLzHJf3D#Yy~`p6UJh7^cIB< z<4*Scv$(+O4!4`(Y6l1)0@NgiET zm^%5lY2eF;B%q}n;w~pPQ4hc;g0Dz4SFevZmgTXgsC(%*oZHl(L0b;&;2y&L*NOsO z&a8Qtu{=zgxxs(h`ffKDOf0Wjce>#3-mk&3hZ(m62M{s(<{1AtYkS%co^Ikbrga*t z^Ctnk10ZsE9ryN4yByPR+rEAIqP!w0Q0d)%bc;X`*=AZ!7#==jH?5T9SjR?5$+_sUzQh+7 zo!@S9Cr+TE-Tr(M$%rGNT5+BHcs;-T%3!oa>pmtIm$PW2v-CG&Eb1Q4CN1;&7D+He ztk!*8d(SJ836_4)6x5TAhgWZKKe&?3Jet~5y_2i>qD00clDv`_AcLo{;?2aT+Z22% z)KGeofKpcE(%ELVff(&Y z`y+*)o`T$or1OyltQde}&u9)~m_v^|flzL?aK^tHPN(@t+CmJxz@@%&JNtGoBEUz}3u@$LMsZ zg{#Ir#l=RD8snIf>rzL-@x2qOLxkaQ$~MaJMBM@k9-m2mB*?ZvuNp#axI6~$${*(w zX^{sbC53uY!F72}pl!T;$~kH-rk!)RGDR#>x94YQKiYkl7v|nr6V{4F7M(9kC7ma# zVm&ob$o59{J6-Qe=Ygu>#jCqPb%(&9W1i)oLMyZs^;1{;d^te70e$>H-G^HkrZvpH zlnH2arkwMO(rCu%Q=TG3L3nE#@A+Pj1$D|38$+@QPZ2%nJ(XQvP#}RBQ2L0IsvbW2 zwSI1uP|S3olOA)65>30eX?zzP!OWiyahEXlV+*0d6Bby#*+t&k=e7K4WxQai^t4x3 z3H?)v)f2z0O&(2s$@N%LCNTvuK4?(=(lDru|2SAe-t-1NpPpL1^G0G)i@-N>8w!0g zmuWPemgD=>XptDAZq1k%wVII}hs#ePjLK2u(>2EIctX<=NU?EuGum5bvtP<^Yybx( zaY;&&{LAow(V7Hp+rM%*v#=7}ro_JXtecH8-jA6W=>^O`zZ@kbU;tswRZK!CZ4kJ& z=I+j4q<-}_Q%qXD-6+p3H)Igt@kn2T zS@bsny3qM-9FubgR~ou9v2@@Dffi0F^M|p=q%PvGefoj-Z=W*~7l3fj1iUd98zK_G z__7AZr`fX>EVf5|Pz)cQj`tp8{9H*-BnWzG^w2cX8>72m+twZKA?>To#ji~ZaL*x&tsdExiQ?cg;xtY}a z=HTpab$xL>9I~Sg#a>dL#!tP|e_{EYp3sfj+_Y;u{CDbcdwec%8%y$DHMz3&E@Yfw4Dv`!LQ`nhrc4_d8=mW8C3P! z_H~0TcHV5(UY%}vC}=%n=;O@~n}}ULoS$Q6R8#513fJiK)&AGRiQb?c__2q4M4aoy zo!HdN_C+UBgYheynCOm5ru$t=QOb&gwAEbH&ZGKRH4spyTZcJ%+|0g8H)tiHYdCms zbF85KGw@zen-XC%^x}O+n}hw%3+P}Gt65n}qTJlM#%7#ym!77E#h5JN2M@exuoQfF zfNJ)zb(YcXyqAI31v+{mXWUjPf?j{|GNNcjdX}NjS#JxRwraa--xMdo(oK zaD&1LDEXgO?U@p~#GC^EJ{ioso1;u`NDshFa{cHQLRFCn@w`@#E*~Hn8t;gOdE2x5 z!9$P^zC44@e-W4z9}wd~xD+<42R7F&M%!d@0vZXsGc+b)<_;T~ZFi{7Jh?iWm=hs$ z!WcK3ueG%S>k_O-ir7JPj;k*SPtGK<2!{0?j?%}pk~3ZNwn02Asts$rOsAnR8KyL{ zu#vyfjLu&mDwSuwKSnsp78Evjn}55UmIxdo&^q+?(U!AQ5e%lL8?{4oP>x_tRJ~?W zvdyT+TfLmeAE_u1kvLw_fC3RP?4%1MnZ>?oOS5{dOJFLv=D+hLjSY5j zyq3f$i_@ozN_)D?;Xz6BRVbc95>2=T;xr)D_s)avaEJD^x0cAwaaZ?1UyVyGZhif-^h4~YUpp1>Nm_-#I zI!1)qh*B3(k>Xmu=3et51T@sO+~Ek@N!2lVd0*9bXf2WN);%_A68KFTmN($0cK`RH z&y$k2K83Z;qs6y>L#`5Y51ylI=FcXaDDWWLzvPX7-XM;DM* zqt|6%;YkY8Ve?6gJGWx5xaG5WuO09e;Vkob{a(l=44rUU5Ydup+SFgV0jOPLC<>T9 zG|_;V8?iGO?#SHZaAF8U{b80zYTR)iJG=>9H22hb!BHZ>z0bPdqGZbBk`MGpdrxBU zArioROcNC5zFSL5JRt8n&Yk&>^0ki4`!>dOhbyMO!$Us>ru9OG&i(XY0l+|0Z=+4Y z_ASs@Jzicy{UjjN^Q@!=($j^KMpD<^b!C^17KZ;v=H4m8X(pxejOk>g+R~tYibmg` zGP~(O%%MIL>*#gD$ys2jxQ6mC=$Z7m&$-Sl@ z9td*Pz{NRtd}OWg^Xnu>mil)l{~T6#gfRFQ>VNX)o2JiIO^?Wp?=+v96xWkPAm~uo>YlxR@tn*kFQ<6= zxHz==>q?&pG%iE9Z`|g_-XU&ab!woB#_~Hcs#yLKJt5esRfdDL^#<7zCM{GFOE$_ zzFdw%YUn$KkJqt94M*Ww82%POpSEqd--%(AZnGMJ*GOiZwoNCKqBSe+CimLcAstqT z5)QTS69u#CD|}1ecg zN)seY!4^$-TCR-Vzk-K4F5i`(BaYZ~HP5^Q%BVLWsh)>Cz=`sZlUw*h&@F*&VT;}E z5@hsBZQFtt)pECKR!1QdZJ0Lmigt}5O#EGy7%GFWHB6=L?!-pXbO=k!_JBFIJD)BT zSnvN{$O0N6kZ8>`4r6xrg1N1;pU+m5it+XGY_~rBYm~eZ zvUvDY4>8*a@n1?PJhubWdX&-JOyyZ+kV}+71{NhrNKXu<@_24S)J>NNBdE2|0#l*G zXnPfHbj>-@u97oxOy3mS@ES|XkqH%B7*zM2Fq z0EB72jM-VtFE7Ul83ga?d>XDvxXn(4#{6u|r`ZVOlC>OGCdH!S_>WXK{ru&N3;4rW zE6Yi-bTQsWM5vMs&3ga4=V%0@IhTMisSogHAwEUJ4Rf1r(9%xh zw`guqNX%-`Wi5)6bHsSU=7LHdcuagY>IXbmuJ*dBCW`PO^tVnvu~ESx;edBQ`W5Kd zCW)T>()mczOWqf9Xdrr=M8B{dw;_}I7)0in>XEHWza);wvh{^MaTk#`>WXa)hqj^K zi#Cg^#kVX?w^PA0xM zyKn&XEDPsrFdioLa|rgVWNuopV$KQ_N$G0?tSZSs_F6_+Gct5LNr$Js4ykPk$)9a& z6I-N|zLjiL8Z#S=%j~s%H@j)lAj3zfkiP#fVehisNRnjC}97x(WU|SX%`A8QQoA8O^`yBwZdLD zGHcoFHi4ytb$F|ldIHw)uq-w2}_!~P5kJ~QIpwT*-{npt(*WayC6Dbaj0c8BIuQ7XyU zxb>Js-jC7V>kxlhMqm(yM6 zsPjG>j0P5d4?<})D5B@jUYipePz#ORBLs+qXCSYvE{y|2^UX1b>l7d7nU+m2S}7Eh zw;IMWSFT|&e#doPFE8q?J@`;k|Ah$cy35kObJ)%A=at4ZZ|WqX1F%?2aLk~h!Dx@h z%kAP!vBfjr?>FJ{sM2rQ!0>lAsMBK8Va*W$Y!+HfF+5W-847sT$SxqaB;Vlf$B>r|cAJnPv72O!hYDMWiNd`?3 zkgkkKxXDcm0{_qi;{!+fSEg?`Wk!~1OU`4KQQ|M*{9!RcWK8mk^OKb~LR&Rr>bPGy zoLdutt*}##?xPKzv`Fg>{o9CIv%TlgLBYqr&N~u)@V~(Tmh9@VG{c3Tj84o9R~ z(xlhA>c{%hqXoN-W1sz8YB$tviXyA4aF{M|h&jKfy~W$*M9FD4wT0N7O<>!W-mn^| zAn@KIhEGCK{3?Ka99KFtv5L3-T9&%-H_r)#!RWD({=gsR&?e!q>+m5{8MXwF>Py?b zZ|+z$VV_Q}A!|OIs7{M_1xv;Dx>tQz+&#IWO1`&Uhe$+nKH3h!s!iP{s$i-c=@yVw z_P;Ei+H`>HE2Wpj%vF9lN|>H|ZH!W5!V=@AV}he7uZ8I-0|vUQiCGILwsVjVoxv+1(+hWl3EY#X)LatZ6|FRqZ55# zh(}L07&;}_;4tn+D+yrZq&+d~Bi>KHFoZp6wnhUqs86rgx}9`*q;pEj)H4g=%BOa; zANa`{06ulHcyJ zUsaDgbUxCdD#Idclrh?<3B1r7lhkvQy0D^MJf99VlQp{m`Ri0x_x0_T zl%0@M6huf<@S!@?+*1sofdHS#1B#s!+{V($k=%3Z;zB-9O?N8Lk~X02hK3iSk4AyP zdhGGONVk8RRsxV>OEM*%Pt5{B+KwkzsnSg9mXg}jL1Msz+}qaaW19C$Ns1_Y7wrH~ zx`>e4=sRN^H3N;Q`KWZAtYXfRhH!_v5_CIjQKvPJ8&`57U8vqQ6xCd=1F5O0UouAGpY6r8aYQGGGL!OOCs+9}S{8I zwYfuddpfwL8Xz%knVs$iij2@w?I1`Da%)%;*p1IE+1um3m;ZG?ajcp5v!qpmA&PPw zl~}ee&g=;y_OZiU%fi#v!lf_g65OU6_xi)NAMGB;_qVa87(ekhP-UJgfbrHT-2j81 zqz00O&A{H3^pqJ0-Cizi3uC6LdW>6iPV`G)38evI(!#}!_m8JPaRuNQwhzc~Sin?? zW}6(x1cbt;z#=Pq8rk2GTa<$PLqu#IUnIk7dB(G`<8dDBU~|34HYR9|D``vcs1XW_duv3PXV3nXNicEbBSCqvy8SoI`erQzi zH+$!b$-%;|#v0BU1VMGyOHwEG8ug$g<-3^rO2-8INeh$9`V|vbcJ_CP7U%r>hdu!p zQt`DTpkPAic^f94f5=ms5M&Hwv06Uo90xlM#~UemZ%J)9_OB*;OnvU1h9j>hBa+yW zYjUhP3P^iSi)dKiLlAAgsqBTVkT=)kFB<1qc)G5W;L z)nH~kC0C*Q4pLj0(;CI%u6?8{bpBi4qxHMC4Om?x%;|hBH(7k#nkQeBg=_xnS*|s` zp!$?v)fqX2pwJ<5;MkmrPKY!Ce>C1UE{hbR=`=$fII4UlO=;4ys9_Nuvm-1^sneE1 zOzPRx1R)4Ty~JG_u!f)cLv$+4pyQY*v&SL@V}o#CY7{n~L}I`a3EwhPnAj(J!;Zy6 zX5-$J`(z=mcopBmZ~4~RdD|-xvW-yBQGRzlk9O{*n#HZ`p307@7T8C^-@AB4eYXa}SbL;x0 z5?9;Ku34|9_0gR?Wb2|$UBW@V!|4SiH1X_+T8Dvf9#8$4I>cFn_fRA%DZ7BvChAeC z=zma+Or_dtGSfXCItp9LD~3wUC-=&6RvkEJGiPen%}3G}e&Xh8`%uP!Kw~v17E&Mz zoXoP{K-8`g@LBBVA|#(gww6tE^#h!AX>9Q#`tKOh;1JgEZ zB<<*!oIjb2xuoOBNgbONWkLREJqHw>P;I+ZmI`DiXD)u)tbxPt>q(T`^5h{a^4r+>h)vH2Zj#h{NT7$!ie3Z?lcI6Bh0)?>NlIXI z56<3Mwh)mNbffmC6tZFHdno414?E~!?4?7nn0wD;b9>W|3$5gy3*`x3oY{kn=F=#I z0yUj|f#C>oS0R5>_su^pJL+e<=e!DDhF1|qu<#aNEg#y5UF8Q!ksC6QX#RTMR=7@G z0D}bFgJ5FBrU!+dZO4$zAQAoH<&lX?SOjih};! zd7M%%C7z+Q%M%gVG*^D($SuU!){c)Q?3ENbLK0v&t^j&k7CgSYxlSX?(S2@&j=IrD zXny|$tA2i#Kv^K@XGcNdYQ)}*5&vS@elX(rbUvOG|z&%4@h{J=`sG8=@ zbk9wpt0(pA#jCdod@Wih9t@TorVw0RK(qZbl`P-gy>($r?NGpAbap?hkLkWqXc>0a zFw#`#7a@Hrzph2iIznFORDr`74VF11S!K)kH~US?0}ZeY6-5(F=*|=95!)1c7Dg5` z2<|sp+D{Fk&o%$0@Za+Mwq|eqj%l&=0x;H&92auZ>H(&@3bbEdH4)15!9`%4birVV zAn#a_x<;|vm?*wu@U6mkOKJvX&!vV3aT21*n4hhj8Y9XYQpxGa?N2#VvEB_U2gF;s zxXJvpuCU7qXxLPqh8$j^ovjgdZV9O-V#kvDolBEXp3Um%%)KCf_sgaYE@jQwWd&uW z(9Y|7a4IPcrA~~cb*`K57cZcAP9Us=QkW2B>$DBhn$Wp1wnEm($jQ+D8PxvqSFR9> zGL0V78N@Gg?J9gP*Yg%FeumCkBa=LF#*?UDf$3i917TdPw`jIMT0<$tlFq#XO;7!6 zgkPR2!bgg7uDD5S*)L3_=9ti2$A*7!KfF>fKh0qBS9n$wtiM<-{ zshRW{5a1eFaqA5#u~TbzHA&*4d8td0iaZ22z=y*0(9E@L6-)0^ydt}-2vS%&=`jyO z*)i_&a$OVzz;~DT{q)SVx`|!AV04Z22 zVvD+PE=kv$AgaF`1*tnX&u#&Bv+&~jW=uSgqY!-E;vs0-dHhABvyIG1jaF5~6cyzC zM!LfJGI28E6?eAmZjSRbWzw*}w$@favRA5PD?(x~qt|&Kolov{D|Rn)BSme(_mb_5 zl|)^Kh@=ZhPHF0?KOE|d+!`s?vf08ReAifJA&smrj%tni{8>CU!~B;gXFZetehVqY zdI^>~W~`VSJ$G0TlWTXeefI?VmYc4myOpbNhF zBj$9>Og5Cnbk1r@ZoojK?=^a2bZMncZ>PKZ|D>{Tz>1rOZSx86M+N~#OTZ(6PAb1a z>D-xNmvf9(L8lt9PcXu^Sf$`Js@Nut%5q!%2!@zp$|a!YmP!<~sQ~Uww0F+&eV80K zHv$rd#tC>;A7?aV350YOx<;;<-_leLk{q7(nWPWfLsw%x4Si(OrorH};!$s<+vee$ zyPJH{oe8E9v~wrp_s)!PY598HvL91Tn?@OAhcbMVeGC-R68@#c8Cwn*KiX+HYm(7%Zx&mnfit4^*wa{VLDt*839)H;tO&e^*+zY#=;cv$VC0={ar zD5R^f?mT-09)JUwrNfldQzLR%pU0FbQ7(eTEfN2wdCTQHh?WJ8;IMqLBp%8`U0XPG zM;1^t;-xkvH8!k!m?{%o>&^}B9-BnYCS#qTte;HEEw+F17{_4Ykh9f0bnWnxCwGP%Cr1!y;I2NzWjMOUgL_JQ-pa6nvwjZsA(dwkM*E^1is+%{Jj+c7|#dsKNznlWG_d* zFHzJtB(H|4ZJWN?V@;@ikvl`_D6Bj*hDXx;EI@Ni7IMTI`m4jzckfz|n<#`%&%x(> z1P!mflr;Qx?iuadd|N$Z^J$h?Tw_xmXA04lMhnQ*c6XNuSgk&tqxD?07j5eFYFE^i z`uNr+Lh81Ys6#DdS;E9#!$`_yZ%;OdG*n6Z4)%qit#*4;TViQK5+;;9B0-Bj}pY>I~! zX_j+LMkSK<7xMkmy$Qw6bo{=04@L-Nrq@K~`6Ruc-vZDd%({r*!3g4k9CW141t9G^ zO(Ls1|2JZLO^*Q}XzlJuaAq(7Z%8mPoBu*==~`HeXWjoKo>gKYpQ%3|R8VGC<0sDY z{b;#DkVFKwJ3z4tj)1L`gBpE)4mCadDF7W2g&{N}#DET8pq0>lh=%6mz0Eq#P-_{M zcVo$A991+-2!$;Hk|cPXX6la2{O-EIoJfd8@{8Y_$M1pX^;u{O0_HU#mL6aaJNepT zFRT-Yp!}d-BrUF>h<}n~R$~KnCUZ+a`HGV-92$npIUE<>2*Li>sIy-sqaHq9F(H!z zB>GREfOAlapFlT+LOFV*d6d@Xcx{El#31psa6pMmlEWG8EEeB*yXiSCQ>jQ0i?Yb>NQE6 zAP(D9 zP8przJR1RCA_|7j&>Aye(rXvpho;vWLQsc#iO7<@f(UmKKPr9nuBP4WT%kGe0UBD=6*TQ zgP4712_BTtx4C}V{O2ffJ7E%r_UrTU3ytW%t*nc-J(Hny(tzadpGFEjvTq5$ZFV6( zENCp69*I1mAPX6qnU zK{$mA5V|21RKK=sUGW4H#?C#k&F&;cPyj(dzQ5I#^pjD_=e12tb!I~Mo4-$icWbV5 zs^Q2p>z%6qyOE-SYSX#K4UYebf68M}8J`D@qKMIDcxuys5YO^JvE@zQ2wErjqLVk! zO0m1V0x%Oki4a^@$`21|ZNeUX4omCK-T{8uh}K3`Z$^e-5u2sM6T?QW*A@!W)Vz3x z?e1p-neqIk&-pQE_-hXmt}=2biOa;PKW2EPJVA&Xk9p+?-0D##uO!B`~%o7 zb~^`{r32!?g5qWb`V)(r1m;Bza>|J?X$_$RuN)25xmoA8IrmmD4dfJ3))w1H`-|YB z(g0eRa*(Jc%`!jwxF8Qq+-C@g4`y^gz_LqR-&#GYTK%%3Of{METef#yeHS9lYTD$= z-5nQCx^9c;Z47kXMvM_9vv%}bez1o8nPX_vk6N)Zj&_n(o)LPli$_sh1yk_d6M~@5 zPbl&Q{s>NJd--J4XjSh)lujsFKlf-j)Prf4=c-Y*)i>NoFqsPbk99Ymq$B5pA~?xy zovg;*(e`!Eq~tEqlR|Si{Lr|dpy9NQFKwDrw9Ap=Vrhztt}DWn5T`9v%T5#H3U5dO z&bot*Wl|5Nl-s@OSfvit65D$>1`^0(&6&h66tOW`Sc4~btD46Y8e3W@y|0NL`R7c_ z#d;^N=A=p;NTP)w4b$l(iS2AZ(dII`O?5Xe;T#=LmX{%usMaF;4!yGdKfqVmo2y$6 z^aCg-Tz0!r=k+p|6lj57<9T|$g!|?XCSz{|`rxjg77dLOpoiYLI<+C$R?qJi+UYJf z?wUzvOI>2Brc)EuSLj;fCaApI4>FiF`+8IG^;q4Sp>7g&(tum7U4nma{z9CTY3R{M) zP59G+MQ-3_681Wt2~%bSqQp$Gctor`?H%>^ebvi(NL85$$k=N)tciz z8(TW=9Awuok4W5jdAII`59R{$QudIP)d^A}ss9`%K5>p*xa;TLtKVO!gddtfJksc= zvBpFtTXjtae@tWlw>f+!_jYtbGiZ+l?D*VTBW-Vlab)2{PzJbR<)?}~dT4M51(Ja0 zn%`Y8;kK7$=NSak%1xS9 z=CQUTayD9Cb1;{&E=y$KPs6jzT8lRJQh-~E6)a1b$?{Y2Whmdbj%TYfc;&|qi=~S~ zWV|(efRv7!!gwSP+E*w`01!PkJKB{SW;=l~7rwdkD9>9w^~JFdN3g*&hH^*ZE0;v4 z^DssgirHIBvAgz2l030l?OJl2Mg*PX+^4`dje?r395}3P?yjd2k1$m_UyaDWVXU$A z|Gjyht9-D<^j93%<>m+WcBd6vSX^gYa_Oy?$c{WfEG`M1+{HQj>haUU_c4ZcpcOAo z$lYQSUe&2^_diTB2*aDsJz{6e{swS>>(}?7zYmV=aekV2R~ln44go?(x`mW})|6-8 z&B4y9v8>NiZ5ykXC?b*3HsP!F-Ch4^KIZ3ke|R9^I5fp^Gs_}>K03LuF+YD1QDqi) z#I@28iTMsQt%$r(fScS1jtCMl<*MJt zHtJ7-0uUP=oeW27jT_w zRd!`Vg;lT)>wD3C;|y>hFV|UWa>_*OYT`F`X}Z_*U*ry-Z3$gW1gmBTkC!L` z9hGU=&NV~CdTxQ5o7wnrGe$+56EVk9NAJo%#?MI zzU6-NYa>EaE=FtEclyYu|F{Wqwh-N3!Z#}hiF*)Eu^~vE=BX@=3?a6;l=TMG5fT8m ze%Als&{9PKo|VQD1Hew7Xq)cl(Grz9`V8=V8cy^`xqF)b*hZIWwaIFZSm4{u&u$k; zs>#-L^Q}Sv*oc<;3MP%I8MF>vUFi)A3LB|Z5 zD*dLju_~2MY*vb@FNi@V|_pU1*HK6txL!mn2Ia?~SmA+s(QK{$M<=z08{<0q^@x3W)9lOUK9TyZ z$Halck3y;3^?hEJMq>;{oOS??$5*g4w^F^^|1rOHWFD=edUCP_)URWGPvNOL(N6Vp zI;FrF|F4vlU^C>T;?q80wWGxK;=G}3I75P;*WpS(?`R$WX+*m?&-SOW#15#p|I-LW z%t&)3Y&8!RU%vP5l-f1_nBhi!6_?swywr0)E}tsfRgq@HIIu%9G^cUnaJqswikhlb zsR?JX1IOk0`!!la0CR^ILOF=;&z1B(jk~@oy%A*l^n+#-4RSNlgG*(qD*B;gKDvTCOmC9|-=%%~Qb`R`B)SbB-8H&*l_ zeafyK@XHPDuFV3_suv{23I|yImbCYYae03_4U@&MG&mw)RP}sk#J+T(!azs1>B87) z7Fi;fLrNtjrq|1hOgOZzw6u7FKmxK%CiK#%`rcidd-In->UZMK90v*ba@%Q99%tuC zP=}9acV1sEgd5W>ZqzChFJ`0>PAbA^@DQF1;r~IEPv%meM@+O}$!cuf_NOz(y4)_d zW&0TaXlC4^!vEr~A+dg8aMamhwaJg%1LU>arDb`lwlnpKx}fc%LpTf3y}%0Y zrr)xYc#;iA)Z1js2DvQc5B5!2=40CTt#B%cq$gVG$F_td*no0HX#prcA=Z~PS;`ZH z=fJ;TcheAb*JNc1*X2?|14| z)MKDbr;{>-tqIgBk2vrLe}`AfuYSEuJN3PDUZHFf?k}hLxVq$F2a=9n6hERK02oZ- zn04fUXuJe+gwIXhJ5dTc6B$v%Cd&^fRG+pa1#OL>Bj^H;vUFSLXb&h3%Kr>(nc1#E zCwSRUr*rDZKZF`W{S^PUlusq2J!qIxpKUDzUqQ-Qf7sKwDhHuAibanh{8O4YMPv^)sI)A0L8K>23)C>_p&mnDU(q}Y;prga^s(1h zr7F^BjvYt&Y|j!kQ>fI3Ha{We(MnEPW_IQUxyc#^Qv(bKm>mbZtN4i6n)XT=H>FTb zb$lU8@-Qtqhl6;eirsj6#o*@mlck!kX*Yg#px)Kg<8(PhY?i7!;`0{+K(dd#N>a39 zcEKRn)$88R@8MvwR@O`0d{Z#88*57(Qwomt8F{{<|J{nb+{79Vg znz~Lcf}J&nQ0rWf@1bMlFmb|>udvcio@`gDDJu#o@QEA%^K|iQ?uqsFT2@uTR+?7~ zcCxoa;YO)R=T_si*(FaP+XlROYfxQo}ixDxs;ry!n-)Co~r zyN1~zH%zkU)T7-R<&wi@itFy8++`Qy`;8EoLIJKp~{SngqpU5 z%K-YHNqPt^xm>Nyx#tLgi|ow;tF+H74Wu*p6XqHhv1el1;vv=jp@flZLz6slA5U0E>R%Zp`xY-W~jqecbAy zH9fbSx4n&2)2Sr|&$U&j8|c@Xa+ap%lwh+o7fg*3a_SE5LS0hT(`|$j&W%~Xb83<# ztp-f-u-5t$<_n?CfJ1un?l+?&F2Ylc4z7y;hsiSs8=O8;(hdfdlwd@;R9CPvJt)2n>=>tXj=lNhX6`&?HDx>%Cyfie8 zzZ^cBV`bPN**&#pOlw@S*jjt{guVJJ-I%qo-nO*Dz5#_p-41q6tuk zQtP&DN%&+4n$jw+L;M5F8ls!_bR|&>Uxw94;~F^)V|?C<_`L@=ZD%pfA71T#*S~Ky zVQhD)`c(Fk%S{g&|}rEX6!f?%T~V3voL9+AO`UpZv?ALazWxxoc)9Z z2?HQn9EBwMZOmRfA?&Rab1m9v)75np*Sn;1o8L!0yWkr1I^N8#^ky7kF;a3oA-ktF zkxFYKZTq@=P-FQtN>WyOzNVFRFewI^-yu~Jl!MN@C!$_0X+KsZ%V=xm8I6{1H~bbr z?8rL)J*{1!=kwT%=)o$rRdxAIFQ?VvKb|Yih!3|~MZa9w9P6NOePnEiB$Vj9z z#`Hm+&vdHbm*g>&b~Ybl`ucJzAkg0Eo;C+p-=pQC944w`ozu3kshPuY`tCKejCFrn zAEr~HU^-qeW7#wlB_0l#6;@qTcuh(d8B)mp|nHp zm`23pVX0@R{skVQ`w_d|UI8S6u4XQ3x*F>0ZbW2bNE@+imZrc!5FyZ8on>xxPlSP% zBA)WKgFM7em-i07pK4Gk+E+IY7D=JT2&35Y4ylIw&0pcSnXmmzJe&>}$`N2{ElVXQ z&QN2^qSqfN#+FDyS9Sw~$d#M!_(9<)D$b)D-Qb#pV&WW}!(5YyrtNAF7V7~MG^FHR zx1Y^s!Jk}uGp)y@(axMuvw_BG{=$2Ghfq4u$V~^!1pbL^z|YZLTec|@Lf2j_YyB!k zeh@orjcL@4X_68vdE-?sF5j6&XCXBAO#s^i6@DJ z1EJqDRrKF7zYbqdch8np2huYKJY`w@SZ-`_PF~{Z%&PAuFUEnA9oM9sbC+bpu{5OM z5>6IrQ1Nf@+&Omo5i*A6+ET~xjRKxUPB>e4Lz;F`B=9?IEot>x=F9j6<^5Dv+@Uc~ zbeL%w5|EjWyQDv-+^vWwMeU>H=&Uc?`MdwW0LuW>xW}DQlH3OG}q(<%p_@%e+gX0uRg~3 zn@;b$)=G3H+zgXO`t5mVpY!*=A)0=b((RAlO#uIr<|6||sMkZS zdgXTd!ljEf-S`&RG`P>K%(%%;qj1*<77-)ZIp+zp&Fmp2Pn=gChD^2VYsVgA&)?QR zpJVV3*Hgr>L0o$+q;VtgPj0<$AwsazNwfjC3}cPH0j|33R?3%y_X`XF896(EhCpvP z)p?;d-VID~oa>>_Ra&I=AT50X$B8+t9Y_d4Y*x+ChdyVzlUR)-2+#GFx2&@*D3^(q zk}Ys>q9u(&MeZ9s=;+kvgcfspZi7N=?M&TY68vIV`cGGcItz~l(}ur%ag+s;eihbN}c6qSW2yYS2ycv!NaQ@){2tS~%BKI^bJ^P(u0 z+G$Hs(@m1>ISRrQ*i~@bMRqOiuy`=6WlKM6NY-QjR!@x4j_!Xu*FS1iKd|JuT4=Ox ze^52$gkhUW)(&zZNjPf&nCTecE9#CA$1QcbILBDS%#>{P`O6RdugmoO9TNM{h*D-s zd^?-~gPsju_d(A0<-O*c=L=&l;YtZCKA}#usq0RKbp{)jNWZFYk3ZP2fEL zU4A*><4vdi(v8)g@OA=gp`N*4gTBCj zLE`UrDt6AP9($%GJ;yAh7r|s)2Xx4e)ukcX#z${~gmPadV>a5CeHs3~0}i zlwsSlum>+8y$((B^ED<9F`WG7AP`NLr_0q~LQ)l+Yt%))-`kS%9q8nh5&O5zE7P&C z`9(vMX1Zv;cxVk9KOhvI#69J+|1~TfTGX+L>(1A30r9M(=4i24OR`*!E&MEHf)B0!#a&&SGv)D+F?pRp4A$$Hqga@~ zrYrl3YmmfKL?UL3jWL0j#cQJgypKEj)qm3e)V?SJdnYrs6R_q*len9p9G6(vf@Z8> zG|IDnV5t1+oGe=3Ut0p-TT2DVI;R>v<5aow-=q0!*bYmGn&S@#h*~m2|A~Y0F!cGR z#wFweAjk1O(4GLx<+4Ph;nWH;J%TTEl!+|L2Rd~znc6Ki1wD*mle}XudblPrr+%4E{w^#R?iL=Xu^X=L%%}U(}2A07HHKTF3{pSU#`geqrIF?Sog8 z70G|rw~wbBTX4<3dPJi2*jw8hYl~{ow-&))(D z6q+k|LHKMUaQF4ZK8dhRk@>g2s|b}R9OlEGZ50wtppQ7y3;J~m*i0&_r?w*4Le!_- z@1>Egg_@7#e~~}JZ+e}*^^Q<)G&!pyp9=mKr+;0B?yq3!aDxT=b~kIhh1c}e#1W*V zds;^0)6utoXR}cM#ixSVK^bD!mNwj;wPF=ub!h*v1X!0Ru3RwO1O{s*DmOAY4>T{` zl-1KK$I%-4z}4JG+5m)RG)!>wF>3rZa3=MRc)oDGs`C!@V>&31l}XY0S(bZ@=EDXW2`#pE*cWmq0QU1X1qatDNj`YL}he^G-VcJ2-3dZ3UF zVJ;C@^dPp$cRYVB*{g?lwE);Rb{!ZEu-5!DW`d9+g@9lEtMFCD>-a+Ky6-ENsns?> z3qdCiEugxA_7Bz*n7sr-?hmnlcxjvxoSFHZ@zXCYOIZgK{7~uk&<$|YPS_R{Aq|>e z@;AROw@zN__3@tcUR6)IHo?*(T7AWNG_v8`JxANVc1UwTxy66OiP*L{g{B(6RkQQ< zN&LgddMG}oJ))T(qVBN2sujdVTy*>c8T}bcg6yZ?YjAsHrT6H6_G&xUYV{gThI+Es za)2LGZ~p5~un}QpaW>s1vv(#P zG~kIw$T>h634R3ZX*oC7#uE_%rza&bl2$!FgZwKWmi?ESkF}Ie{UyY)4skK=k&09O z>K)-|-3jL;_C%_~gPeHmW9`;#IH7Diky9!VtY_BKsXWbpj07^DzCR7GOMu_l{BKut zIja0%ZaBq|n6;NQ)M{GS@YXGvHZYu?Wg2TYZAX+2ZTkLv8V*8mb!|<4WQZmxdVz^) zWsXD5aG8m7gD486s14@}mt!ZI9a!b3da)p?esN4g!v(X>3(1AhogQNpCT|~{2*-wnPrAFs_l4$ytGJMXzb^H>_~${Pwu62KydlH}$V?4$D$|LHAAcUEXgs8NT+eTXcdjrv}9K*p-Di;pTkER&T{IL1Io~}6c@1CyXcKu2IdPI>& z+|OeS7Qg)8AuPtKcKL4PIZD;v@NAE-X9G15+r+No4@Pz2rf7-RmGf74F$_y*-o#l_9lK>IEx(i}x5Wr1&xFM*A1a#t#H24%$)KZ25Gl z7^7cUP<$3glQ)t!K4U)X4aRKm0Eu(quh{>1i{Zl;5d;5aJRUv!?tKb3e?8Sq#^HC> zWtdOR#Tnwt1C5B(5-TyGC+JsZG8UjDUaW`b1RPcY+lZL& zT9Tu`xFG0`qIr^i;ceTvDye3QVqA{WV)-=OBaCOWd-Hqt?TEw_KnvWNu^Sa}#6Ais zYH;~?S?#%!gSz^(7@W}#?|Nb9B@BTkD<7_?VU$7Qs3!+_OMmnc>lf3Ho`$xc3@d2) zlZeE{a)4bEiES_$b40_mgE}4~>F6s15TcW6fuZ|*m}F~01n>wBobS{$?nOot1Y>R^ z4m7lL>zhEM)6OpAa5-Nen(?c3Gi!7*Uqb>rUN7ng>^f0#Kz0{_X|)%O9?qvHsA#k(D=i>d{lA| zCZuU_m*QBSek_(J4jwk!_`e7U$8T|(VM8)@9z2zPreZpxdGPhJvsUB43xzjimI&}m z1QBGw9e47fe}9a`yZHi?zj2$}@Tz>fu~Vgde{cF6s%7P+CL3JJDb=hl%rdww@=4|q z{0y?QFn34ew$y^(w6|WHEfo?*L*@JyR-{7q2&zD`47LBK`rU;gMkU%d7 zx?y;a_JH^;^cNN3f)#0E+#^JYnVM+bIqYThmKYD-knF_ z=n|kDEjW+ zPpQ_w&)HSjJlB+-R^`yKxWo?7EmioC_1MVto?18dGeii z5)U$jz~BdDmTAvL{$daYU(})nY>f`;+BDVx7V>`|PSi^}8=>0afi2fh+ilPUpP_!H zXO^2PXuNz>2s~%}gk zbvy1>PS#qdS~opRkI&xSecNr1itkHh|@@Htk0&f!rsa zSJ6z3E!AHw=cqR{>~QRI9II&JX1)}w8~Xl9LXn-kZYPqrbb32aRJCAI;ped%t{tuI zno|BXS6?nu-ji9}OJI^Iyy`}TAQPT?1gq3DWVVsulp{W3Zr}&bu_#>1h(SSvun}5V2NuX&p7nLUePl zHU0?m9UY+05!VPPncp+ur#fsvL$nHKv`}6z0!&z>)hD_&lUD(r;Ip?F0Bcg(;c=90 z=L?T9O^I<2Q6ornynBXl?+IQ%?Ivv~cs%NnP;sC%Hm_X7C`Gb!VZ=zo8^f5mV`)$KJ zhb)ERrW`^*kBM;pk*fU7j20juIJ69tiSo3GCJZTLIeTSV$w1Wlp;jp&foDsPXefoB zHLZD~Hx}D4l$X&Fs$c({vfj;`aU~!PJqj$%A3QM2%6y>Hwom(yOi12`)X+9g(};$h zGZZ5=!!>AO*T78lWlCh0ttAx19IaHC#OfW6h zE5R1=l*rM@PN|VByrzRY*hG$ph^0xC-TJ5c242UHIW_B(l($q@!JghLT{qoLh#ECa zDkKpu(&?cF?Z(bPo!voGEo~rNXG1;IAgWOA?kS~uc@|B6nhdW?S-)C8G^>u#OPqKR zn9LDWf!Oig7+YJR6KtTexU|ZVYWT#Nw#qA%*|VNW7_FE`O(WQ18=n)Vc-(&DA8JXh z4Ecapv`@CE$LcOfR#v6G{zM)^yXj-vU4}gxpNixSUi*O`KPOFrhB*!KKd&Usw!Oic zwBsZCQ!oyfo)%p9<k=sj0A&g8R?g|uHFNX#A)pS=Z!(i# zq4AhK1DS-$CeLme9~{Syf%nu#z4fTCaa|8UZh##bs74#WD~d7Nafx`pys{7ott-N6 z{Vy~Jkddo(4wkp+g+{#^e+Y%g6~}1-rE-VU+49cPISU4ch_FEjx39in#7V{gadiN#>XX#~S|0zcCeLz~l< zjoPsLVnmL6gHUYK4Zo;)z_Bb{Uum8}7U=JeMG%Q`aM#1$_^{PM^dI<+dubms&yEkzZnWD0j_$;)HkfHYYaKE;Q=wS-d=MKi6)C+nFj zt==})ZU&2`>;te0-pJ5+t@W9at?AYFrGZAmkh0ZPZ~P_ z6Ab#Kbbk{(Q!SZ}=AOjqGVU(T#@2GTDT;i6pTu9-L0&WkcmFCb zzTqJ(#%~cJC2YZRox1GH(KlvCUGj@wQ!wy-agXhD9>d{NtnrKIq`P^AO+x+kjace) zy22tI)}GxP5fiG|{K{tvUJfC~Tbz%$!kYr?#aY4?z5<-e?^N=*~RJsm%=n!!%LW&N@ z{2=;YN2_uYDMt`wz7=T$9q06` zj22PC%!9E0?WUV(jF91%cOH>8G-(qF`^7 z&UfT};7bdrM>h`dd-Z?2P-DhT!Eli%D;J6Sn&+q!8QQQfX*lnaW?J+F$Zt}D@wFWt zl3rNV-t6&`t`>4d3e|6B1`d}RNKVr~$DNeSi~O3VrDK&%L~GQ@@{9nNmWL#Vg zWR=&eV;#n?>=?%z&y^X<$lY^-ANNdGYiHo-4e2H#YTY~c zb*4F?zZJ=T{dew-E_Z@rHCoN!U3Kf+B1}dPhH67SkKU)9y`dxXx1v-SKK^=+!F51! z5tO4zWPIi(auxl)a##Tno{}PrzX`)0~TL3CuT(yLjyYdHua^yKjh28xyCAQx5Rh;7%(%oOz9?rH`f4Z)f zZ{yZlK{y=^Fma-yWC+o+E{kU0DKlS-Uo*zU4%lm;T657M8razKBFoD>)&YM z&>cAFkwU4-og2s)Xvc+UGU5sURn2l6C665qQ+N z*>d_0bi?VOLrn!jV_h^=KXOb%#}4#d&v>^c`JFtqKE@jU;jv`(i2rK$RTdwY>nQv?WJ z6>_;VI~>`Tp=JlP5LI9MywOdm$GWH=EiVQJ5DaFQl=%-)^shhZFNSII81vGoy^4$b zS4UE(8R@TfxJD`%7{@5WGs9ZzX64n0O?R}9y6g#uP@pZLvLwjE&o{hVOXJ#fL(D6{ljvZRQ(0}ZAa;z4 zUOab4*Js%Z95`OT`uRyZ=bOeKB;!r6Z3BIkP;+tbdzUkw%Kj8uPkrs=4vFF@jXtY3 zCSPX|MClH-X!{C~v``21-3GjIr^UacR`plZptj}Gvrb!4{|wX4f<78eFo?adXtgP`;$A1U5N^`ofpB%*?enTuH9A&#Jp!u zhp~HS53Mfmj4*I|zK-@_Bxp9!iY&}P&js#L-{J@Sy}WQp)IARYZ1dK;Hc+;OemV-KVws!uGdH!M&lQoGYU zlNK?&56@|r{EyDuM8iEe{jl!c>vu#WU7;H3YG$M^6X&QbMlfk9tfo%xvadVesUuL0 zO4X?j6Uuw}yd;Tp5WMYwqcz>!hmuM_L@qIf3B{pCtLixo$Ag*DD5X5V@m-%sNk3@R zj~xM7H5b&Y-yPS7Roq ze4??8?8L*#D7J@nb0TZ$+Q6vTXU%KT>c-X}iY2wr5rPVjv9BEoq*v4o<5|i-_hO;I zI=wvHx~zSMMKhswu5juTx>)B5N__RJ0YS(%Yh6hlvyzq*b@{x`H-zz@u?EE7eX+D4 z*GiRId)nFVGP(6pr3JR%K$yivCBZM<~t z2F#US!4geeDA~y2tKF9l!JNR6_2uqv`v3VDhEfWzOp7RHsXE^Y4g^VR8Hc;4!9yu& zyh)AuBA01)<@!%$LN6GL`hU6wK|IzQ zou*O#V|Z2f^T-iS%+-HrW0e%+G!I8dEXgZxQRL@P>tLu45#%CXo_4|>7wWfi_}=QB zj|mDYA)Kx*mr`9ex%Xgd!G5c80safgQVioJ3+D>X>4d%My77Eki@eyqUQzFnl$%Fel|%J(HMLrMCrKl} z#?Z$aogT#?+?3uI_6t}Hf3WL z>R(6|$a35mnf?M5Y5dLzdJKo-0xC$PL1j-})PazGAIFbrd5g2kof5D}KHsiu0(r*> zfsD7}q&3=Loz5%-F>-atMa7OI=du3nV_^I}-pgk9w@#hTW4v+_K|)hU64Q-TuC?;E zBb){`gx^A_3J>8KX`OLrM~>Xt0PKL=ZFZ)yO7cybV`9wu@e4)25a#5+3IN(E!8WZF z#fFGNw(<(w%0f$1YoG-pp8SJs6iSSu6{$TbGK5b{XXV=Lb~!z~t0g;36hkLW)6hg# z3jgB6qv=bZ&(SYJS8h4Sq6u`%d3^6YY~;+z8u?r(xV`OEU!~x*fA`#o;=v#C)O$;N z(I{81_X<#~JDIkk=h!2?7-8ool{(9rHnw09aNof~ZpB3W6D96MVHZL2p2lp1 zi~g#S0SAxbuXWAm4{|RXmZwweGSO_+24KOd37Th=#15<){T3kP6jYM;ghRBm=2Jj~ z@Pg&Z?Q?TCr!ryP2zn)8MadUTJ&Ge#EQ0jXwBdO9?hDxvb zL7#T75&b^<&4gsG*E!Rm^)Jc4ZQp7i$mz({;(2gg!%n5zQmh9*q)P=v>=uA#Oxw+I zNXgqbmZf5j@(Y}_Oy?WWRQdE24uil%Htv)6TkD=->Jr}7XMQocL9EzD!%ua(FT9XN zn;`5%kz!GoA(5ziO74A?3x#(UD=_8m*(8P!hlKEic`Zw=bL;CAO}Zu@c&j-?8-h4w zNw6`ku43~()u*tdC61e1pEX6mFK8kHzk`GIgAVi`@z!tMhW8qA>0d^vx23F=1O*?d z8R!dg?Dit*==lQ*Ewl*g{ZQ#9kFTTLHav1bPlWJK+Z$?P+Sn<)KuYgN0_Y}(nvpZQvaKJXIW@W!FHh4B$yLo{AMWl3{i%mM)(e7E;@N5g!;u0K*hYN1{B<5X z4=ccHJy%XrR}E4tH?oyGQ%}}X44$-PthB4MR~1gS)gwL-q9H6{4W7|#!{CAN0&01o zCM-?72V{?!ey3bHAUvxh4BAn?s{XRnc)0eIfm$n z{z&S0El0sw|KEUgmY=y-*WfkpFE-sK(hcMsqdS9NpMLA=a+Me&)>E@V+0s8G_XlET zJwsDN52zptM#V*zv_Nx;Hgv?^rjWh*L^b9_tjU@Kw3v@A;BqC+=i$nE^bWh1`%B*5b}@|J&@8v-hPE9(;-S)DJlK zGv3Heo2Lx>_n{x)T{?kG171i-(kWNadRMllM+T+A`F&i09CINvi8tq7;Gw~>C7>14 zQ7Fjid7!fKuVw~NqLM|1@*6N#3JLd$vX-jt_yh*cVi^g&v7c?M>{e!gQHv`K%FQ;> zt6tAyEd|;Mkd48649e;X_FGzyA*#`BzIJMP6LgroC2twdv17u+T;Mc2Ua56Rl-pl94l^bFfos*T2*ehm*n>r<)D| z;`bXbV`ZOPLw%>cjtYh=}UT;jbi&vR3CIzWdc61_A7i%(9%(!Q65gC5ZPFU0JT8(eS)=NQ7tfs;~a zBLG(Q@nZ%0VSykCT5R3tySSqqTb5`PybdnNcf&vbxzBbW`kA$C3Apu~^6A+!XD})K zL$OxuQ>__>vkYX|uqKud+OF0$brog9P9&KnV`T`kYh^Fy9UfZ%G`ecBLN}hbcU%oAIV}x8&eFpdTv6bwAOP&kbOxZtrlJMT@wAjA6QhD6u9HCG zkORX0nv9o)ts${?BTZ!76bP;J~M{hdOj(mj6oEG1Nsus+N#1BCs1?oo3q1g3l)X4^E7s|oo2`vPS z`CU2dQdRI^X@%<@GUJO2Dp+JF7}HAD-KuO!y0xV?F}i~@5674}5AyB6raS2vp-qiO zdk@*F6)9VnLtkhFMEH|}WiAJ-m3$=Zw(`25xM(Q$Q`2on@zA zPSXdxP?quvenTy z7|YGL*ziRIZ@bZFxQEX{YyKYIVMQp`Ysb^F_RUJMNQ|S5qPWDc2h@RT@K^m&7E9Cl zpX8^QqbK#>p5@^Ll%`tpP-1OYfQwptFv@n-Rs^=NP+Qvjs!tRY)o>K2(_oWO$`HWU z4L?My-lRRGNbsCRVm2&M&%{tU+Vb%oXwa85!gF$xI#GUzr{lm<8(^iJFNjCy{cDh2 z7f|b~AdeX(X7K~CE>8rx=Fq?)jlW@~!xHTOl^9d@XO;DAJ8xRDZ)zsYsnF3tsD)tgBhWP` zb30_v;v0c-KOy3~Q6}POE$nt1V!YWAo<3B$h$9Nuhk87c?tI;kUp*Y&MXQ%Zzddui zM2gq8r-@|DmP=jlW>g$wpgn|nj4zMJ14YJ=k}NBz`*HsA;{Zxt4gKF>(qmwK@C~GH zfyqe_2PIK6_zQk-ha1$7wwa@a-w%Cr0xXeK$sjb7Z%B1#>s+W!Qg@F!I)nvZ(l8rs z;*L@+kV}&UaLwQ8RrFeF*eas?1*Da-iRiCF#h~;&$UG>w-SMrp<1XOuQhEu4pMIh2 zZdW`J!&6{=2NgHP9xpsrh}k;jrVN3~p|jM~XCHFI@mwn0tMOEEhfgK|vZ>rU2YgoU zL$zWidN+*^&cH7H1uL8BI^M>d#;Gv;BXdwD#_&t2Z(KmAm8o&WI2s$*Yd>?=u)GD+ zUqq7e@C2y9vIz4;7U}8O#QHi7O+Ip`&R$W{WO`-!YoAUdoZ~j^ECU^3@7O0m_FCCRWb8~gi>*060wB7@F!wckydq0=&}5c^H+ z``b94UM_n{0zHMT>Jb`Hr*i;RjK0EYQ@`i}Kju!HEleX>C@6(z2_qQY<;?H#(QgR8 zMrC-w{`qcPCgBy;pMkbN$2Ev+_c%v8uJhH9>C1b-1C=|+&94A)5J+*Eq|@N@ZA=tp z+PuhWc#}P;mGWtjCF*y}{ka#yy_xD#!8lM;({j~B;>9zU)!f+(Kw(MX`CoyJBc_Z1s%T$+^>T`wCBDoA*$U~!P5 zjs20I0hGj1XyaxY-pF|9W9eqrw(8^YAM>?u7yLx_qaH?5b!o(((?~-EW#x9_?nLdH zzh#r`4!)p8O7=s2Z)h|$UDU+Eb}*@@X~)|n;HJ-UuyViDyXdLl=%me;Ozm*n>~ZmDR^$ z5i=Aa^>kE0Yw1SWfQxW%cR8hg_

K?7*9zGyG=24ysi|G4>P212qG8=jFDSD6(oI2PR9Poo$hOCprCTSr%Dpm-rIJGgT z;9=H+GkQQQP2gP5evqtj#Eo@|zEc@sR(~0$!_?jC<|9x~mtiUt+ zziCTmQi9YHNPe;eddxVNixZ7z##+awFsn1}5 zoAaa2>bD+fqE2{qys(A#Vt@^#qxP!w=M#+wQlJD#PAU=vfW+m5Ma)WgJ;Uu9I z+q0DAS(i#;zW8$!B;fi0Pq?00JTI|=S?@GUoeEWDDv#{jMgwOA^75GCPLhXH+4PkE9Kw21MM z#?$`3!{v?!q;*9etf!nP1R^=A8cZ|KOd&6vy;fP*H0O~Q!NH+TseKddA?yDQ{e2f` zBJp#0PN1y9CdiLh74Z)O&#soz!|76!>E39~BuB-+=<44PAIX2MLr&X`6)gtXZ?aI0 z7@D8!CORdR_`~AA+$L*8j&>t?c?jYLd^DUpQZj)NM{`MN!+A$VGH&LamNU)@j(-{Q zE1Lfm{Xp~AwhzwnOEWYbXH+z-oEgj8jegG|R;&!p!6onz?>utNYQ6g>l$HDKRr&4j zYBBb$(G=+xGG8Z~^rU&Z!dUy^W#>lfOZf)4#El%@cqa}Nmct$BXDImUttZx3Zn8;f*4a$U9WAnnFx5n_=4JqTK6{F)2!JHE? zM$z$X=o!q7D(M~`=MN^@8~C6xPPQk9s_XB6*?&PnswYutc@Kap|BVYrWM+8owDojj zSRp_0%E&N{tW2LB|HWl%tm8^CF#ztlvtln}R?>?;w$0U)I71}lJ;DQk5_MzY^~(&R^N zEPr>CH8LtfqPP6+a`SwqsemM*lxMj)D^DpkElfyzsxvm8s0y3{dW+-S;JUhfZm`b5 z_+2L^qf*^wd2BxkB>w3Na_!P>vCGUQ==?WbgY*iS_H?--pW${YH?l^$F2V>rdOioI z{#HzZL`q?`-Ht{m?0Q$uyZOGpx5|R|23%le*)~pB&25NJ9>7rN%;3Gw&W5#MokvduI!d=etNTpg;c zsDofZs!@ktD0c1qe?U{XYbgPu9yb(vu{y}I}8%T^YA zJd)>??8#6o%ur=YpLlX}>3#C4m_$Zjl zsM9j*zo0!OPWjpJts#s2)DPr238n#RClXpJGoxFF3`&ohyR$Eeo|uIrV$2{1>h% z_d8V?6Rp=rF1hT>1S!?5pELAV1ODSbg|4(rb@<^1LWo21oED-^d`Vuvt~)M_LYH#S zsLs>&XAJz`AWM7mKS7pmy**DutB|ZG(K;->)8rp9>MtBY-`-jZtGrPgJNCY^W^5GD z0uXQ;F(>2!HwZa$HAC9`C+d=H{NWYUO(4nk0lB|gv2SV^(cFKdMvN{0L5((>HtR9m zTG|pe+hh9Vy+_XwK)eet^edJZ#2Uie<3~JgWNwZm;;}{wPFGq5 ztr8u4?fk4)nWm|Xf95*pbuGsuE5!$u zKY$=4C#*{Q*zcXYj)Be^+<^M|Sc54<%zTEZ836MnhwP7l$k_7Z4rlp2m1Np+@FX^W zG5nIdB`eT8t!t{*nvL_=+1XFoGhFYP_I_5eFlcG~;gCpg|HC1PWjaEEY6a)^Wl-<% zlSi9x%$>$~*kBTy=?5I}m0VP5#TBA1gUX&q{4CnxjPY;82wSV(N<4& zU#*#e$@dU10Y@C`DEfDE7pcOIj_2T%85m$a{1Yh3Zv(I_FeyO0)_BW8-*8oCelc?E z^%a=!W07sdWAw9si}Vb3{mneEMu4YMWWp&UI3KlmPH+@Xv0(DDq2C)cG9pw%L698w zT^wX-3Hw=q(@-Cqn?C1YkTiNN>2@5)xN$g*^r>A>=NqTb0!cg zVV`xNK&aQ`&o0p0O}QvR12xSYo}XS_LJbP?qOd)5b*<_8C>a&VoQZaFuSl>wI?=)f z=sVW0v>cu?AvqXUeg#OVA!wO5QX(r~liu@C$QHpT8aEqMNDJA=XTJ%j*As$*Fa8R*_WM|oNOTnFM9kxVLisNl>t9k!RI9WJLKv1{XW7broqylsmfILK@JYG zJSbc3AYTsqdshxURfj;(QtAmVpu2tp9{kh1W+^lGVj+1?6oH8ba2#mY0I1S&Fzaqc zjGECL<7apU5thCRaK$V8)b!;axaNAbB8XrjF^gCjy`#d~5H0=C+YqEW^-;h?|AEc( zJ!hI!PKCW|BRlS)v#l4ShoZdKSCp34FHo$zAi^eIPlWOgd4+Cn z!?kAa@DyTTrHOfAsYzZfLUp`*8^m^y{Dh&d^Wsfs3R=d|Txf@2a)UCtS}c8x9nD1x z{dgKOB5;2BBVA|6FLyapUCv=4|3H1VDy80;Q5=H?a$3%p2``N@&XIQ%;=9vD{_gCe zn!Wmx?uf)`SPv`vR!JE2H_*`wbs%5qfR1GG{MdF*t^n=g6?rZ)uFI3otZMmku--WtW&(A)m7cQ)1H?PR@qBE__!?0%c? zfq7X;F!L+v$)H1jYGq44ZbC`GmL&*G*cmvt5SJ;RhBU;`k(^gzlHxV6DH~W)pvL8d{qbC|< zS>CMl$d^1sb-13p2NxX+tZg|S)Og;m;wK9Ph>nGMM4jAMVO*v-5@AS6@|vQhz_r+m z&w8w28U<+UKAQa^n|N`}Hn8C$2iD9&R;obWj~d_H-}o3)xK^N%I`yftUh7K?hn6;~ z7h_b@rcceMel8H#ESyhq8-CHD45EC7+Zm|@;%VQ|uK}AM*T?d5+stJ;tCm=^-;d_y z0$>CRLXcuBC6_VI^Bfjp8y*4oY-?_cB*#W9-^N_jLq+!W9DEA>0v0>@R`pyl!{A7u z&;p7_fqe8O+Hn5oK3*(`CG7pb7I6Z&a)}~gWL^BiRA5#+jDB>keGyY>3o3kZ<%#^e zMIOF(k)5CAyIwGE;g@?`Zgr)~fGJ=KH1YiYZ41^r{UulF8b!m9A0%i~8Qp=OgRlu> zK78t@P8f_%=x(Xgv1s``>Y34{1^doJlbs+?bSWAJhAg6$DBPz#%-r*z^;D)}OmGLabNhHmiUAb`!5@BdRaGG?dH3L?ls%kBiA1>`Qe$cq zF~n1;0O7#YPk3_;#(P4jf5Ra>IjRx}&j1uR!9tvh#|z?v>l(UQ#?1`)RN|lL38e*ZRrzMNe|kgm|HHhM)*~t=7+g5AKd5S643;<4#_Po!-#; z&YT`AS%AzFGX*j?rDilCl zO;U&b-_t?Nc0z2gj3hFw!X!(c!LSO|(3d!n@Oxn9mHd#3`{x)C91K7nAG%T9*N;XE zYkS_jwrm9?pWyK>B7`lke4k`ZnEPLIP~ML4VAShhC9eP)Aum0+`rEA7F8;su08#%= ziB)*IK3WYBVU6a!Q%X*OtJlGsz%da?Vv8hSJ2=cNOJ{f$BUg`qRo4|7^T?yWjW zC6rlHP~aQ>uITe3(RcG#rvw>dsExj3`C4BSh_`{bZhlINvoT{hnrAyl!}8Y(5e9>v zc1=CBKq<+9{Gn4+;3D$=&X2H5Q=eGoDHG}~m$-JC@6FdqUM*mEvs*4xiPC3gZze`) zgnl*j`RybACBlgr^431XU>jdsD{t<zkP5>uG*r(?;LNL{< z0;3xK4{dK5R#n&aZ)0HqqI9D)NOvP5AT8aEbT@25N$GCbAdPf`umR~#>F(~%{hwU- zbw76;&s!hfs>6rhuLx+;63DkrW-(;XEMi?%uMI~2D(e@ zPBlJ}lh$_V_XbTq_5mmx2EGEZ0Q@m49%lBf$IVWP%;4lSvA$K?%l~{OI@4Uwz4@Pz zbFqUKoZyuFp_pF+8F4L8mkpK}$17<_e~iG0LvB8t;xkKRmv9*0lPP zAsAERkNFupL#D^u+b-sRc}}Yz-*Z3lBe4;xnR3R@4?_AJ8RzR&APE}eGo%5eJJg2K zo(7Y-7co~s{N|#v+Q&U{ntKS8D}rPubhhphX!pH=YujWxn87)X2+E4r&gkpyRz42a z-p8@pQUvcm$E+U)JO@F2i{SJaTcr+2(UGAosLSwsHGI=mK{okJWdJ%lw+#g;AHdd$ zQE42bKJj2akS7n*&O=pONA7G?9=Crw!k>CDP0#s`RCMVHmc%CR_hhvN+wUK!aF> z4Dfw>;Qwe0-ZlsEuXg$JbWXtj>*MEGUt#j;uKP;mWn75H3I+D$RvB5olUy{_nRJQQW3h$nN4^LTJ~tsf zBg;C55b>d&F2S7lA^WxKP>9VQ*O(+L9*XFI*4kjg@txGnr$aS{u`$_bBYh7Srum7} zXB_HkOk1ZID&M5_5%%Uc5|xo*r-bX)bzB4o$ajVStO|{sCIwY z7pPaQoab}(VNo{y0L`e-c*BoMRf%_0kZ`v>zV#u>S7DsZdg0_rRgs35bjAGe{+FYoPhF1gLI9;ln@goD=)0~`)56r8Co!5_So^x-GLuX-LXLzSSlyct?(V=N zugIgc&-}|VEabbmpW&PdbCOK-ujA)WUbazg@)I1b3-2r+6>-{_K%?Px+@Q-Z!$ zU;-A21Lv2qkOZ_sDey9mD(cCV4KCv?}=Hq zFFk~TknhE^gc<9KN_k0UHmI2UXeK&NIEj!Pa1{B;FzOED9|FD)u1Je2<>zh$*^3nurC+n(T>>JO@-}KspVG>~$S{;LfjQhkqT=yS)_3t0MfmETP zE5CZItEN)t%#T{2=BHM;rhUGn=;ovGuMCq<9exbQuCEh1hZ`C)cr|Yi3z7xmWEiwo zdq)%N=(Dr`CvEiRSwIW6px}1wr~AJCVymgz?qU?Iw_Zp+3LEyhG?UUf$ zUFVR0_P7Jn3d}d~nfxwN*dCh#9`Qf^+;_ZE?fZ|s<>uA3=JPZorzm=ACPLK4g&7kK6Owlfpao2vOwFdBvrkya6~g@Iue<$(QMnDmWpAq^Rfpxru;wVQ;ZGO;lfKn zJk@}%YZ0aFKbTxK1HW8HR2qjS!Mqc99ZRRz6G*Nw&T%x=;g5cI%kn$|>{PHi1`vyk z=9RXU@;=v+XEu+hH@5e)$vgsm!s0F$A@f2xEC6< zIm%zfnI+G{UF?RplhS3{(pVnqFlUn8UXFZ|&dDGf@S8>fh9sws+v$mEp%a^#hute1y+=3U?6h>nWSMsBK?J zv7Ao93ba6PJ2)YySA=<~ z)=qD(SeRC(3lM;E#k_$7-P`)umNItC4GZn;S`Q)7uwcvl;gi;&ofXrKUVPr!=MJS-*{e z)`e+|c&T0IoKv@NZ1`-9yu6ZD^Y1@2@(lI1-yw-cob$Q5Ug0=|QtUv4Wj>1RWVPDn z?8YfXy_Qg;X?;5&-qB(oO!|+tlO)Du170wTcmLPEi5>d^3d@8-G^h8u0mC=7f?72Q zgfKd>qD`2DZgOKT(vOV_bdmW4v&s>*I0$Bh*<0XLkt1sJRP|bijlAEhfHIs|1v^sS z)S>s8?=)IVnu}1L@qPU38%+9I1tTLPGqJ9GJ|XY@yGMUS! zCVun|2cH;}h-b)}ePYXuOj7+$$Ksi;A(Km4#6}wS#coU<%HjV&R3PVP*ynM%zKwX6 z6|ER?VOLX%O={c|l+$633GeA$lAg$(M3*EghUAWk+^DT~35^X)(gIvtl}xqu#JYqaeCf9=DYn7*MD&#Zu``r)-f&2d83#1b>71yc!-IW1i@J-eFZSlh>$y3oY^H3WA+E}BFKdkM@5Y^%3pOjK!nY$D@QXQK zt!E@G(#07l%s^hJiAIqs8>&uzCc%4gBDYKiTY)rYS4mI(%D!6^+pNcxsQq|)4YoHp zvzkwqd%mop{nwt^74n$Jj9D%f+_KGD79f74zGrvsX`$O9Fr9%ztf!S z!;WaoN@|aRmia&zed`etX^??%P#*km#SZmt6oqJv^p`br(*m6`^LJlEp{?X^#2#}O4>_HR zz~;ZF9(fe{IY?lY=^nZb*LVD(IQHf^^E0>!t<>ZPL@pAI_=M9E@>71Ej>uX}-ZEV_ z!{d)=jp|_fh4sL-;M|dT>eGY4E5!osUtdS!@?G@dvRKh^W%?~NWh}#1*B%rZ-=uo5 z%MBMbf<;IdFSG?N6+7FNX>CzT%(f9@l@xt6;CBpKVN~k#Zd!T1|t@q{~Ans#l~9nS9am=KUg8ENZ-5Azd*vI_ma+TKkG(PeVK~02E@|A>4!! z!wAqI@?v};u6(FgDt(bIc7tZ4Z&P_v^lwfjiWhK*o?p@`aRr^b4C(b$9PBdiip9ag z8m}+S?zYXa`j$)Swl_C-*C7JemGEC}$=$@faN1O8UGE$zWmDbnN}Ef)J?(OmOehF} z>rdtyN*okOjMgxsw4eH#b<}3H?RZ$f1Ae^Sxanj$+<0&uYyf_}(c@CN4Lt0aLc6S{=hinMLZ>ZH9n53O0J@7Yx^s-muEj4)4iQudY(TxthhhlN;gnUGRj%1Vq9JQpd{Cg9DaDYuRG{MF+2C$ z(z5DLcxi0Vd%X3mH=b5^;>xTIs@t0^1SD2F?C5S0wX8$_mRyvEPZ&V5Ksz# z*P;yfBgf1~z`-M081Ou?Fik4D*q^N5(QP=rAoh0pAwS~$+~4lI@RxUkruMv*tNEGP z*@pwgaN!TzwWhz<(%{9(b-?rL*l>2%UoMr}R;jZW>^v2?4q@=qsBmS&`|{Wo-UF-a(N8u?ZqGlohGbun=_4Yizabnmv>l$-HdMucVH z2*vL9-(B}foM`OT=(OEW1VJzdn!Md;A%mf9$pw<;s<9dFlI*Hf6t zFS$9j-MtG=SObsDqp{XXO$9MiH5;(bkK^shCMY2<{Q>2*jYCXqG+EzWvL9#4eAry% z-__OK#KkV+ZijEd=h#buemO?VJlqHV)(sMFmMsb+Gq<>fw~-xNoAI0Z*pLw}=^Q>= zk&CNXN3cz(dj>+BQJc=#-gSNAusbug7(}!zTU9d=pei&fuq_-hEirWAeWOl7JLtCm zirytM(sips4eGh`7JNZu6coE%LnNu)l#L2C;P7Bfl*#6g3MM@~>`aJUrxx5kc1Avk zSIAQ^*Qmm2YJ7E8YGC=qbU~wXJ=geWSF5tX`I{$PllS)wsSQC-O-IR^@ctHtEA)0Z`)+RqMWNJ;xkD$dseK=lT<2zkX^H=>P9EY-N2B?eVrW^h{ za7f5hEPo2nN(^-j5ao{v{_Bu<41M+e#Dn}vt+`6s*Nk=EWz9|J&uUf3?!w0jvY>6sd$J6Km9d^Ctfes4}4G@RO4c42n$}N-@M0?%ng1`M=`;I zmB%IV?IQXZo!eJ|yJjcwq;^!UF+}Ozdwr+Sb+;F$acWFa1O{BDaa-|=gl`5vkB@QK!fqZ%x3g3#Bl zEzZjtR>m)`rgt4PIut!0hSQpjZg`C{~VU9Kni^ za}5qTUApAe?a8hzx9fglrfG)%4y*Sq10{Zn2>O|`wQOh%N)HwzD{C)Al+s@Q?~Rg! zR@>jz{~<$e(LQu&6yM_zmzuO{*p)s-Sxgp+;;--H>2a&MqYKR+*`&DDu1%(PfugxF z-H1G@_rp?*jWA&X;|N2w@`ZE8FQ+|J6`z1m5=@)`am>zKX^dC0IVcctInD4E#J2-M z%RzG#PgnmZBUu-daHu?u(PE_Dro9@}LkB%*Et-j!x3`Cw0Qu^*kIA4!RDjaJySGpm z`GC8V!^>TDmCv%xK!6Z%S#K#3$i-Nj@(L}YEQc>8bVT=8i@)M|=bDZyDt;F?d7&q2 zY)@R+Jr3t6|IC;k#Fd_(4gDDwRFx%a|B8YfBi@Eh?C~`kgY-Hy^*M9e1J|dBCfgg? zZzz+NG}JYMf)t_xW_Eh1_L9?J9ebXlpo|a|5~K z$03@~wuvr@aW5dfSRafRVSwmGTW*0}yWj4HmJr%>cB+M&+$H+i85LhIk^q{v&f_rVM|8};B4W2wJ=lqtQ9Y61%K1% z<|)w9zkpMVQn<^?UM;T_-7)+#Fs-c+jf+%Lj#lDVLh;UwG}ysa{X|+**Gw??-=*tO zg>jJ~!!Gi9caJBkEXk4HNo?ZdEOkK6L{3(c3Zfz>Jr8^J#ZMyT_6&K-wBu?k@avgV8Jke9 zA+u8hP}Z?6^%ej6Jx7MVbgW{***sX69#@D$=nveik4^(Z7V5d;<(YAC%KT@}RZk<* zLWa(uSp50?AF?OAYqwMV3ah1a)*C&K;TJI9x@+!>epHX7*rIl^bKKm{RN(Ye8hI_p z*Unl>Yw)z*3o+rTiXX|M|DdPkG)BpfzZAXUMJuJiu2Gu*s9O~CGxDJVjp87Qao5ZX zFuU2ra(}6{E&pztKf|hbb!ip$+bF^L;it$Hb2*f>-%&}GAg}Ss#^dF!_e&4hzRpBQzBSZee7fyilU`@w?sE(N zn|HJ5!skAm=P;wQ%q7OaD05_ZsBf-ik#E~7 z?uUxWaBvg1DEIJw#>iX`uqf2jBI3t(LhLqRgqD@g5;Le7ldl5y0{ za6&}rN9UF*!%+QaC#0*i#M#B~o#hdo!E&2gQFeYV2m0)ciLM=itmAk&SoE_!cAj^W zXL7uMuOvyISs~=JAUQpk#wzGwC61W5X?DdtP$+#MuSX4JW;TlkdjWvk2X|6ZV_#m@ zN9H25pHk1Ws_Rew5N5L$6gI7UZhnR3472_g9y0Q2JjfzTqZZSY^JWFsho zVdv&q8B8aB0e2T@YjGFyE=8-O|9b2`9^7s!O%wJXfb69Rb>tNxv1J}mjQ!p&T-1kg zyo?phS>zCat4!EdNrrKBhLgI@)EJkZqgt=*WZfGjCTXjb${@bE*^Ykg7XhRj8BKV} z@4Yyu+TwzjCMG448wgCDAFVitH9C;x61%y39=a0d&}$-5JDr(IefRH~5{5cXdTwb< zyn6Dj<1(?u081&6_0hf_6J?KGFE^f50m#XMB*zaj2Fi_Jz;TWp?Xl+WaJkkg5tU+I zo$`0Eq^8aJq?mGV4Krwy*xtHy4e?z*uYh=VRy;U4X;ah%h2-DC9(mJ( z+^o*v5&{0Q+G9-=og>RgpZ1mz&Hjf`dNFJ3Wj3p5z$60ey$EEr*W2OduxRt>{v`3! z^(C%V;L?bQBqxV0ZdjLH%WU#u1!;+|Q0&bb8<9_I{iJ!PR1gG`g|Y0wN&g&ch4kxg<@Wr? zh_UnjgqZ6UrJg8-dXanU)IScUM^TBwN3AGve`x2<%y)Ozt{dIF>E6YanaW~vhxn1( z^{6seCxa+f>w4wc?CWNtzgI>>t08Gd22hi7Y3Ucd^>aX}%Le{Ya6$zJde)QL%p~~< zrz2zpq;5W@Nh`L>OOCHC5qp|9XFOi8@A@$;6y$AX@$y5?A-Q>2TOjc65&;@f*qw%w z>dU6lUw+|zmwBHY;nYbTdLt52HWX|~E0z`IkroWpqY@*!o-`2yA1C;zO0=WXR&F24 znx?kNNt-kxfwSnGSxaW738D6!oAAM0K3=7fjZ6OQ@}^CB9-fkKV`JZ(xXT;?0`o}O zbJ1U17p`@DHbG^}%4g+zd#<#3ss`$|7sU)Wk36p!<_!j4*T31pPvk^fwXJi$tK+c_ zrD*rwlU}+O=8K(B1!BN<2pug{zP&z@grDCdk=G2%EVehtbMSY=a*lWEq7*$)iV32h zk%wp5@dkBlC83hAoQ-MLx~_3+uh-8n7kaEj?p7+!Dl zn%fT8CLq)wIcnof(RW)r<=j2@vtrDNSBcQ<+PL6UHnn=7!;Dk!5L_^A&rSD57>W`q zF=tB^egEpG>^9`VVFse%<4cRAi@U#`Hl8KPBsd_bQaF}@oQG1PB2)wP#^`>1;4PEX zOz#wImc&JGWf@GT2ClL^a72Bv6Q?43JU20Em2 zt{BQH^Y_bAbF41Mx+MKyHcDv_zz?P7a6&pBE!E|Xn!@MtLMY59Kgz0wcZ{b_yp~Tm z5A$e`cxE%44kVvLv<>`cjZ~`a#lysQ^n=SHz+J_fdh|%>wZ8S$-t(D>?>WS7UEb{) z^xVzP20kr`izab*&qPjd&0@<9BPj3eGgpYjSMi>R34*RF)}ggPNdqnboXpY#-%SOS0KAiRlLX3$^o4b6Az5YskT^ZbK*y)j{5sl zINXrYc9XFS6qD7A8uAohX9E7`>lq8jG5ZOiPs^3<%48iSJgf63qhJd<1)HCnNbi5% ze%x2<7c_^h_c@7W8J)F^hQ8$h^?6sq>SMF?+4@q+_a{o-ZDGl#cV?MzD>yUueDuBCR{sy-KS< zGp@8x5yaO^;<$X&wll#PvBJ!l*Og*CFjQ*uG8NXj78CRUCv+hJCk#Z^TC^8>OIM+9 ziMa$8U(fBI8YZc$`3&VA8`)4cNmO%=i*QPckfK7amlhI^1&=a6_QywKAf+{Ag@CDe z*6^QMy>xU2x5K_}uKW`jeE-_>AS9s$>M%L_)~jv}2c3-n;IY%q{w5YU*L1YH zHvF*7QEmKM;I15YbGzTzD{#AL=4P?JFRIwTdh~2Nd8s*kpa6F>KDfPP6%oj?4G%f-%&;&t1MZ2_9K%xdW;3D_GL5+OnSXI zzkB=17iK(X^Bz~C1}lfp%t;?R*T)olz-Tr)!{T76wZ&7S;`+He!L3B0C{?tKWb!4K z(zJ8erR?m)8U?$kZ(sXt=KVgY{e14VB;dk0< zPnh{R#O@lnu0WT$neSmeNgZ~P%+nHo?39$kMtBwpViogi`C7Mjt8=PKZ9T!w>nG{< zysiJ^T+ly&)>uBzdG+2O7IU+shp_WV6ZtXO`A@T=AB=BkKje&~kAr0tR_qxk;JLW} zNv6v{_EOyA0Nx;Fr~Jqy+uj`4D zuL4NJ!wNiOQ#~(aK=ljZ_^zG>p|A`4S{yb|FT$+LvoJHXWWKQlrF&`IEj<+oQ>n^s z2;j{((sexeq=HC+?d*2!%1*0#AD7ne;KM5;|H%SKG7s}59mZMxIqrUXjq6nV zp}Wy#0S9RIz&E(?W)Kjv&}X63oHfYGiinC46qIJyr^hCCcct+gOTEU8nARs#3TFXufS0*PByXn^H8#zw_u3&WUR`(h*f}<$b_2 z>h4d{jukB2k9JtEGWe9>0YB#^&aqBY4VUMwQutnBp;dZmrFS|nejRZ`DsTcT9H+aw zDj2J45WEFtz}_w1<-s_TB)7$i$9Pww-JwaZb-VGV5q52JqDD8sUl9X8-ck<^l?u)` z0ImT5M-Js4+z zZ81dc+{tr?&1x~EtWIk?GcStu0TJ>o)sV!<5dB|{RG8c~Io47ssXG-R>QF2gKl@i8 zj$cU03FxGQp*fDlfayUZH-a6JZ8mm#$g{5=q5`22&q-Ged)V%F=RE@ioX;A|GWFO* zU;%+QH1KtIA$K?UINTSgFY|{^6ObE4=HAI2j|C2diy6Q54Z@1!tvlj})<;`!8_u^p zmpF*8Nr&Z`#n>|CA_NAs=eW3=4||?E-ww*4s})Od43y}fM%}@cIW+2b5$T(T?BYI2 zNa@ZFKLM#5IkkACCQ$fI*{`i@5U39$bR3{?Gc!W^`NQ?o7Rr<87 zj3j;RBI}}GSWb_Hx|w);i}|_l%^lf@o}r-db0ivGEu5Df&vNJ$?W)$=3s_r!ZsT!U zUp-V;rWec^`W$bTRh|y5T?9s}qWyd<%JcZ<1 zIU&uFKhWr|=xygnPU^h%-4)kTj)YFIP{C9vgZ~>M^6@?q`6;g_>}8>vkeJ}hFTyT& zKAg^;81=Rp(A%7TsY%1qPF0foNWK{ROJ8AjculdpH+qNcBO&UAS-0yUD2%t#f+dJX zLUrTTpX1$CooJX(-%85&Ov9+niu0qnh^U>ZU*D1okbyZzu_8`B_$ZmJB~Ee4zItp} ze|3xv5iq>|6;q(eN(|GIkcEbjnAd!roAWSDQu9W8LjDiodn@bX8%L!Zu;#ifsLKUk^yG|hCvp7d;J+D-GH@QA*854*eS0w2K+rtA6xm_TZtSVzz zgCLKQnT%g+XUcPM?$W%#E~#z#!P*D7hPWXElWNR%G102dmU>8Sg$3 zaOr)EgMtX$vD-)9Ol>3zd7~ih*2S2!gkz((PYl|?yy<+FPet>|wJhYL4pUB~RZq5) z4x_RBqC}Xv5#Vu&Q+a#to{1i3n8baI>|Ix7p#>=s`$@>=hAh@Pt5U5rd%18?%n5|7 zcCJ*HElG)X&FvHgYxZ;`QQRgbsTs`#zg``aY z>?rLu(2FQHPUS$q(##o&wCZb58yOlKmC`1dT-_(@8uRXI)fs%3$gw<{>Y5Tdvm-ey zCpqyHadV?ol#rA|2}ryVC&LDNtrvCpI|T{vD_l+3t0YKvpn37nj{{M&&L(%e$$$RC z4)|9FVIf!oW|L%!Y7bk_U}AG&DP*9=dAWSEs5?5iu07AB=984r5&JWSInJ&&d`iq1 zLPFx4yh876sy)R#1>Vyp3gFebS??&c`aX$Tx(W&A%sJsqcv0Q6ny0NU+6oz#mt?ZK z`cqjU4BlUslTP@YSXc53I}3#vES5D}-&5D)*HU$4kp}$5BXjspkv~;dT)E@vYzs5z zTe{e)#W){=$%kuOeOD!^vjMK$BFUWGzm|@=fkjStpOf4;9~xrXGWr@?Pk8@WJ_41Y z_|85w^s1$G#^nFYb3lsW`0C|^uG~!h%n4Nl1~np@`Jkx!TG@(y<_x5iEGEbz%SS8U zih3K7VA;`xS*msTr$S$t6~tk!9QB^j<&Ku@kCe;Oar)N9O_Gcc$?2qUZw4$ZT5dZD z#!E@AUnF%oj+uX$B9DgXYDIWp1DY~=LB@P7HrcRQvkAm_BLXXFj66YHbU)_()}@MPWc)B;Gp)(+3$SB~t8J{66(ci~Qlc^pCTkdhb_EJp*a+53OQYdC z`(BJmU4JP|d!LTp+=HBf6RH8iQ>$|yukKyw&t5c5W5XgZ_!*v+2UwWJUc-rqh`!7p zj)2p4v#K=TtxMI<-`^YU$INzx!@2cZw*yvRVOr*!xJa*=w1MrUX-?-el2Heg5hYQ< z8gvX7K7+lF|Lb)E2SCrl31n$9DwfKPymV-2)%H1dz+-%Rh{(5|lG;$)NJ54pU-Uy3vaqyTsZx9~szbk`OD+XI!dLPnt2D^=u%gv(xXt(QHN~WaXn|Hfnq-~KlK zV|sUSIeQ1McACCigu-<9VOEnQRRt;^9NbBZ-Q73?D(iVvstq0-_XKjepK@cHT2j%U zTR92{x{8Z;x(m2&KbrdW3teM-CFA7wBfnLcY_SoDbX`<&<0VCaz?)}gvZ`b^9ldH> z#!S`S^MjScf9R0OlD*P4BM-CV;2@X=JBVLCaN=#(x@ENx+NH+`mjIdm&GY8jen}`C z+^^4bvQpLPXv~#}NWIMbYO7!Q%;akr=zteSus@ps6sFC;)3JXx?W0#Ft?L#=^zE7( zoT^v|chkpA0Zw}b*LE+WUV>#^)q#pm}e!{chRgn56N(1CQiMp2=>M!&(} zQ@X)XQxuVmP^P$`y~m+LnM)1;-BvC*s zcCZ&4ji&!h+cv`;}b0V)UWztCB5d-9Q1$Rfd-K)H0s12LK|1C5w5G%mJRm}4S6k`)%4Z>O=<~@|ElYS6 zK(Y@xIXPk4B9NLm$8yGwD~GwG_1RYhu{UkhP5!|!MSGjtMWJeI!IV>GH*&AndmQ!qS2((cDpcFs z+f){P6S%#y%l)PhMQvPeHGX?8D;F-Iu8b<+cI*z9?GGj5p@|7JBA)sHA}AmW{9$L6 zQLlZUK58WXnuqLihMLD9+nREoVe;5J@{l8G{oJ@_dS2!ea=GpuFnq5*9Ii6UL)fj! zDfSFr+#*FL5FZaIkH_Nf<=OF2V0{a44{KIkV~V5A&d{`v>`BEL^I%^^gNjDGS2q49d=N=6^ntOr1;k+`rDQ(<6ZBXJ%h(5MofG zut-37S}mcfOBBn>0(NwT)A>w;rUK2dynIP!f=%5lr=KWQSEbY?k$8e}>I61j=}D?| zzHspf(;A+ULF;Ct)?e%|Q{b;1ScRj5|E?%WMNrGNX2ew}&J}~~ZaPNDc^qVY0!P_$ zCeS(6>V{1k&y%0a%yb(_)EHU&*b-b(f?;Zfzqz=(bDiYH^Pu-6mAlT@(4y(ERBLm< zWr1g#+@L`yUzTXxlczvqTdqH!JCr**QO4W}a7_ZjjJymMisEMDQ=QM6m>aVya}2MK z@+vezMa=&S3p=0cR2(J5SR%e@Gk$hyzPt1!_Zvv@xWY<(0O<_8m#0mKL23(*u+maTijggL;!I z`ORzUQp)Il)I(sjgfpHh(qK}HkkVK47<>14{u`IPhkVITzMEH}QMUU_WsMY6C^<&L zSQEg;`^?13>@~eA#~X^qc30D~jS%(wt6E_@7r&$WfOG0x(AwOm-t0>maJGF99xIf) zoVE3R?$*)l=C^m1g;eXgI-K@q&!|{y{q$DU#$Od7AsCUJPP;eJ*UlTQ?(tEh$5DQX zU;oUo9?^6~tfYk6phNYvl#`VJ(56?;V_9R7LX@LJEsRI0`0o{o6V3I4gw{Rf>CI8j|p=R_? zH|lwP7*DXqW(D3Dooc6F+p*X&ktkG>1jY$g*(WJB!Qdcf^4s(OD>&D?`*tA&_BNpo zgcR1Rdb_F5VHTOqFSB_4vPu*ZH;<^P0Dmt{7u+tl{dWduYV4Y?T_8&i8K z9hkPC*CPd3@tson)LjKPXZi3lCx z>R&9!_HM}MWboHSow0y80uHXt z5eLI9p7n87$}|;S@1q64uuXU#1vk3=V5Jlb76X$yCi-qKVIhMK@M!mO^@0BM{}zbrIve`F1xK)X6`C{3h|3rtKrBE zCCFYsURILwksm}`h<)e$Op;>QWPjDSgE+gMGqvH{+oC!1G**VEQC&?(mv*@(#xI#f z^q|*U{eCIN_p}vevzsQV zeSPLbWN2tRB>K{5 z&PPDs#t~YP|EBAnkfMQ_6#c?e#d!2cM@WFcjeT;`UrE z$~ouF4$OUL%9Z%5fRfZX--_A!8E)d5G>{w3(a4Sp6sFir1K zlJlXQazzzV&Y$~vh6Vlsf8~BgDxqjT_3^cllXmxlmC4}Cyt}GXC)@H!! z=fi$b`el`wna+@2t60ANTMupGU%vLiV^GM*Fw=Dr8Nk``7vBeXzBwK@FHkqxwrd-q zE4F^#P=W+%_LjQK(%Pc9*nH=mmvjpfu)qpm2zeyxtw&)Aj=9}!CBIXzi<>jL`?_6Y z)AISt{P6E;szsdSq z{w^H3DNo?HX{ci%^yJe}^$#o_Bn0iicL@52WW|w=RM?A|SHJ=eio6TT(L`%Y(~mD7 zmG9^e6z&Je=ZfTCYc>q}EED8Z;(L6R3bU(zBqXY%R9lD=e|z4g<>oYR5%#A;o_;Em z4P0iHr%En7O6p!W_ONF()?YX8L(y`!rJTgwq@`g%iFiux$_Q-o_BD0Pfb-Os&3bWw zkb^IKbG*H;x4(;&7bl&QbOLYv%PL!0e^(aZ?~N0vCIrGaN2=>%P&@>wPSJKRNjH)kwivf=uxzu7>E8UC`B=N%prjfBhAw-994Razpx2uX+@^GL)A zbK)V`VOM(Yp@`1d^gn4yfg)?~MgUU^k}f{3UM&0hVybTU&PjO+THaP9&rG~(VDv*a z4?s>EFYE+#n>(loMGu1oBI`lf%%IQou1J=gtfEF!Eu&*Pz@ge>f^czhmuC@V_Ei$3 z{*Da(37IvYOZ?Qz&wew|&MolNnRfSrc*eTPtl8C%ZSxw zCQ5<|9FZGdf?d9|a8C8oS5e7NXv?C{>V|DWUAqCd>(0$?P;sQ8bO<#rZqvv#J4ogH zo_-o8KR@g!+9RA)VK|+L30ln!fq;@>etw{Yg;PWa%;E{@+XXhhwBjd;@$0mYsO>D4 zG4-i{70uCH!&vhkCuN!ON)W?;+4^=oN|q#*RGP9ys3kKKN7luY>2Al?VGxo2ycWi8LPuEjAzZB_SiZEp(|LVe+tpI~*^r!$4PicR?A7Dx8b3ARen5}SQ0tYe3+5lXN zmYUk08`9O+Pr|_yg(|>@VpCzny!*>pmYMj8<&o&o8kB%vpz zP9mt;a*6+cxaHu1DyZ<^i|O2lvS=NYcq1;c*mr2qYt3DYsSJ?L+m!zPx`NoVrWlzL zAuS=w0YUYLSF&oXsdx(xw66vsT&(=5{f?Al{g?thkBVjiiKj}36|YtS5(F$5f!?PF zy$tW(lFmv3M@W>i?c>ppA!j{}Cln?Jl|e`oSU#@5>y!IyouyFg82URq%LQk6b8K1oege=d#de852 zY%gAKOVRo+OTCBz3p3>shfz5I2j(!0GZt5T|D%8on)tPWqQy-7;{u{$a*zd+l-|>-}CPf0@kvbgr#*Xk#+h+;ltbO-JG%+mUY*Z_CGD9=gkb`mpoRl*h$RMuxvA zdE3?f`A>-DSj&b7}wd z?=d13dPgYgtHnob#+9v0hyXmL0I*KL zuu3ojJgu}26ybpKCO`|?0O2*ww)8A@_)-akD)C~@d^V3Bt-K&|Yea0c+u$5^SPTnQ z>xC}6?LJ8QWQN`MR0p1Cr>pPvgy@m^%T6kovosfovLasR%)biuYce1KLX^2iEilz> z4a-J1Q8t`okYyuWdpwWh<1k-XN9^WdEY1tg_H~IhyPX|!Y3-MqoDTq9it6;Zsz*6BYq^B0!f+0JOFDV17F&>MD1Q2pBwl?e2?{7puU9`i*a9vkJIxNyI(%hVV*>b-S!HEJ}LK zRi)!U&_7+`_vJCpdOe6rbU*d}4_jXW6=l1%jVP#qAT1rzsUTf~bV~P-0+KRxi%2&} z!yq7`bayD-Eg;?9F%0?N_@48f^Zn~vi?wFK@H{imU3>3qU)SD!tHy)$QhXkj0U!zG zef(YO<+BrzR50VO_qc{hzh{IY@<;a$NAyY!*+>%DLFvX=9=MWgVm~9>gRgdmLGcl6 zYjJTjl{1c%&Rsi|Z#+{3_sNA9DSG*)wDZQ_I^+i@>xSdP*R-x*ax4$y%FeSxJU%@; zS$CYo6*eCB`#<=vO!uVaUIKVki@*9)K#qocBT0(vG-r}?TBJF}ObBZ)$EuKsr-NM= zT~Q1okJ5Zg;pUEX8yoYSiA)O2uc}(ad##5^V`DsJHvJqxXDLz3O{_EhlZ4P~-3xS3 zm~C)c(CWhC{Lhl%%WF{erQuEyH{^(xKFng-e7|C9{$kJhGIA4~$R@pI3yu-?LM^W! zwZZd`Rt(R3nR@Tl=uA8E2p=@#42`nRM-N_X2IHsSl6u$yCSxq7_@?^sYXUGh{hSS&ad z1*aGI=(%c2G@n@H_y>h+@PTm0VCR}QN8C)d2m8%PGRi7Up-fl7<_6^h??_{c()GRW z=r0>TT{{c0Xl44rkpa$=eX|3&4+2$JgYDgTCKF(+gK9D5rnY>i{Wc9}-Pm={Nv+fY zE9@O$G8O2k`Ge3>X&%u+k(1RpJ-~d4c^D^Eou$$Gd%X9m+lhq{rtlCTGubF^f=y2M zz3#ucvfl3m3HOs$PdiePc{8)JURsjUH-dc{0&NLu-`ObD!UrX5^fdRIb4Itj`lNF^ z=Q_KBR7+t1Ck-y44-&nzJ&Vr?gf`@?xq&DU#uW+<89S!u-c?A4e?%!A>Xx$c95 zN0M2-0B-^1e*%9O(XfuU9inTt2Dk=S-kSCP4JO6uC{sVl3fkX4pv5I3C&!eX!R{L2 zgS9K3_M_ew@8=Wrq3r*nj=>g~xB9BYZS0Ba_U*X}_BQbc!S{n1N{s!3w9jYz)&V6C zkbq-Fr8W+83SO1p?t?y1(YMn5yN|9;`C_owI9CLKLx$4z1L1HjV?!-u6n)tjrDNMZ&BztgU&RqvUf8pZjpnl^j}$W{qFnjvzq>w>MBJM*P~5Ydh*x#5O3IM zc-(#BPA1}QWiU3MUy&MV5&Qc*#t1_`8skmwsEWplb_wK1<7#u!U;pC1a?BVD!JUWU ziGjiA3O}$17NK*?|5&TpRT$)RK}3is%%xcL5b+S&IkW-GbN*+x+#8oDJ^rLp><=|SJ%)VE*hb4Fp5ve$fMXX2El8={SX1;oeSk+T|V z&)<38x2-+3|AfR)@ntD>ea!HoeUa;>=z9;k(IWJfRwbqOD^iNmKB-#mq-q6A4Rw_V zMls3l{1Hn;KHZRq=ioHZv$HljOyyXM!#g}Y90hc1t#VUgpfv%-G{~y-3a)u{*F->a zhl=B_j?-hhC2oK6QB(k3O|66nI|)(>M{~6$dMs4U!vpY(PCR>Lohce{LzLG3X(8%Fa zbO2rs{TRQ?JrFO}I9h39#kHe=sv2g2`&u7h;|}uYniQmuk{)swYwc)T+}`dq!X0!q zVGEbFUBM6TAyYo43l90hz+|o+L)#l;&6lgVXRG8gIFK@-%cXmqV>U-oQdwUqP+o55 z>@qi&0#n?pns%PHnVOZ2FECww{Vo_M?pYgY8xjtUFZ@0-xSucj(+A&u1&|dvlNU{S+L_QVVOQB6_wBV`LRA)M#&vD>c>Ar4Y7U?DA)}hCupLkBon-u`i6RZX0!0 z1g-=zS7^V-V!V2Z?Ylnm!Z#Lz(;kf_NSG!`c6hl}Tkh)B8(?^Pq_%EVajv<8A+G)S z70mR4J-Tpmxj13NA$r;;qwP|UCOGI9My&EgRO%?bOR9g2baSteVi7DTfN!GhqtyqM zS}Z)8dOeSX*o5UlW(If-QZU{%T7e1^Yl+k~qgIo zn!@B*K;cC`h(nxty!yQt&rDFV_1qpk9X!Xti`m-R4Xu6B&@goGwT<-0y3#_wcjp=+ zWc(hC#`#X#%J)Lo`hG35BG|?MGr6ETH)Ufx57lz@M6;~BqT6HswB+Qdt6^xmIsbLtcVlV~)8l5-jR+i4 z{pN2W>(?g93=;h~aapgGIr)wf_S{}GWW0B1I-QY}McbWmoBT3z@_fqp7EI;q7kp^l z%xEc~)b){?p5Z$CoO{v;^+TTh@#Dq0H%9YV=MHcCSshSA!=IhRQt70Lr6I3xBgD=W%N)LIiD_WpKpcDE)?yE)o6Wdb9=qc zXM3XPNq&|Fcjao*RFf^OM)R_S^jv+ghRy?T+&`c{gpVzApwPxj-Z)cL0i=UOm*?%} zSL|w^k@PLfqRwTK316D)jL9SD@OHPB#{xFjE z#GpdVB$(1AwY#?2G=e!|IH3Ea&2{ciRdDmoT*_5{y`6tI+!Yz6o4nHL2(gHZ3**G^ z^k3?I`Mqt~Xe85s@Vc&VvkS{s0K>5R<>4N>?n~m+nL2~P!%$FYC?V@%Y_u*PoZiK= z$BQi`p53hTLOszqEPU6>Gk@FN{jzo*&&$73&yCXekdJ@s-q)e%$k`ae!|Ql;IY!2c zWpbiLN9mslL+Y;^m#d2gL)j3|Qj~F4qK~^C3qs;ebB_VxVZI zsdtGFzb{Q$OhN|R2cBg&I`Uh_#Xy8EEkY~>1%P?;&9^cKi9^QJ8F4{ULgd1TEe4;% zGdT-2rn_16L9B$9d^=62;FEF7xSypw=ySf8GRF1lFfR+&N)Z#&id<1j_&GV2f&Ypw z6Q&|l7RlM&ts>y70`NSTuCLfV5$|gho#}ee!FR|)O|Q`*y>;F4e6t#&;HJ#^G2aOe zCW_O?j8Vk>9X*_!D{Iu^!=HjLyy)jjBA2^b0p3`y2j23K)Ju75L!xi?}@ z|5`aopIMKwU5O}vyE13c(CJS=Cab3OnZrerF?8+xNN3AYhg3suOs~@Slc}Gdt#|$Y z&}S4o75f=iWc6&{k{?rE_wQeA^&S+CIAM{zx3JCnHZ(9j&7v=DxxQ0RCfm{VVjRVdjiZ%);Y z!aei+xhM2Mt52O1x=J;7`1r%#nvI59|C{W$2eA{7m6TphkWZ!SG&G#Ln&Krxfame7 zxP3HB#fP1uGhOSxF05W|UgB^qdjypSOu3i!--3FQvO%dKOWWUvohG@hMG1S$Bf$r6 z%BHax&j{7s@nX=PCr1Lif+MpLY`Vv;?L&=y&6h@#`H+i>@kvC4-1-m&HBZ96U{I83 z?I0(~6@Ts7hLHoa)eq*X2EWpvaKgUdVI2yHzPt= z&jxn`!+)whlo~X^ERXw6N%GHXHC_Q4WO#+&kz*}I2_Ag?esR@3&w z_bN=gFD-pZ`8*8HE2=`($b}jb_P*gA@y?j3O)awlbVtADU{a|4B9k6Lps-}A<)SC} z0<`|lvL(y++}+*3TUlc=e^^R2lymcN!SZP0>t#;)y;QPR>NP9dB*=dzIl;x-C1pUi zI^Mqxo@%o+N)2Vt?sUicuz*!M_lQ1-x zxq4q^m3<&)E7J@K#J$fuEexHUXjj`&z7P_EqAk3ANx-y6kjQ{_W{esul<*GSGs0L7 zu;r?Li(j%Qt;Z;;D368F?6J=+BTEEEWzdV+2ja~FqY{$rO!YQOzEJ&en;o-_1^VU3 zawfKxQRdsQRh9cSP9jICS>#wE^Nk4;Rf4U*v{zXUZUdW>kJA-BTiZ#hO(4w5LWu=) z3M)4;MCs7Xaf*yp|2>~l@>C8G3u$vV@Bt1cPm zBK=^+T(j*T*+%m`uc}MoE44wN%#qT`_ycG=#le#+^b(+mP>_?o+vhRovuAWii*lq$ zVEarF)95X2p-k)YNg_^KOo_(9r~3jocrtL423siasJN~1`s+*T=FR1sQb&}G?|8^v zUa+Z!iS9}%&HWjZrrYeV?Wun4sc)>LcrKOFbI-hm0Gl%488C^_S*EM(v&SC~E1WJz zEbow*t6y<4KJ0<=)jq%)QYJp@zKu}RIsOfAgghS5lhZ#;qrX?E;Rh1?j4~%@r=4lb zUGe4}W-mvFXW!^*F1ZzgAO;0N0L5^xyS#Y6y%izAqn{~_hN9>p>_+#zh>B;n(aZ7l zOo7-l#&62>TNg}8$>8?0qM}4F=W09qm@F-33V!+UYf!YN--hfZ*`nzsyRxxR!v4-o z&s9=3DZg&(VmX&`q(ap#jvL!l(9C7ZPK^_2(P(lldv_3gxHZytr*26tGnSXUKe0E1 zmE!myQGLtQ`Ta{k)zGW;;@Lay5n_u2HZ{Z=VKHv0!_V)Q`a14V{M4{+m78cqkg!qX z4a63cPx3yROlGy+$ALLvJwLx#a*~E347WsU&xlULAyvpii{$|D+=gdI~krV5P5CVP*_qh{v zH(I$kt71sh#m+!h_1(AR%>()hohKq*m-u0lLbpeUgt0l!vhmg>f2ZP+`S>qcqyf(> z=5OP6KJ)0*LCjqk@qKCSfINiDzjpkb{=N14lrrw@lqF;V5w~3lLDzGN{afKR0<)hd z2Wt)_-(k8xOVKrXpPE=@-I=m7+{M604?a0k$>8i;`^2N*zuOUU6V8NuynFKjQa)kd z!-W(2W=VXj(C@Ebkd2M~mYB^v`)0^Z923}OIBUkg?+b}`o%09K2iBQcpdz4QV3l-CO z1q>#8eJaEhl_O(wGJx^JM|VFPlcZcW4JozOU9};6@U>VdCKruzi`d=Q1B>&(vD9r9 z$Nixm-nwp*zJ@%cD0mu+7%|&aY(l_*fA=qxPg`i;PRiqOzoC>yXHC=BYp@UWcVJ?0 zAb7jR^62u$NEKbmsz}A7+568Qh9HHn8$$~VhAQp9?(Tl~AxeY*K6!lO@!>izS~|8og=-xM=O#4#Va6i4%4 zL;jx^j>iVXdQmh!ivQLB)G5<4L|GD%*k{^4V*bCso%M|_hclqa& zIebfhnSDLDepAqf586rLqwOH#-`6)matz_LtWqzs-di-^>Nk`^Kx&(fPCSswWFfKgu~j+tg1k#2UreiD&}J=+quQj zH=WPDi`5v;Q}&z5uIekxQ+%cyd2Dv^A9487OYAF3R(03U8cTjx8<`e)-E8%u$b^ND zE(CptcNd}6U+XBemt9yNxOS!p>`1G()d1Ja`iexce1E+x-{@iGI*&_>ZSTIgKm5Sy zd|aG8YLN#M2ou2~Dyw zf^U_Um&I*`_z6RtENEVBVQIB~<`?idvR}=uWHm|o`5rfT>N^(->J(0Lw=4CYj2L>u~Ia}Lf)S{I1MOwimX+#5Jz*8X;ip;HN#5LyF zuwPlfH#9dcG}|u|60#%)PDqLJ#xF!a zH*z0W>L_SukQEc|alcYwi#YwxK~wL4nn z7-_aUZfvRQm@Rv)^vmvUj50WOg4NL|72V6%AK(MgAQ|-vfmJF=0*8%~ny#DK6l53otWR8*yoQ zub9B!yP}U1K*3t(II!*%-2|45u49;h`O_Ejx*CiWHg|P(9Wi;;Q(AosDNab9cFC=Y z`*hgH!nWeUZk>H;PjP#2Ll3rb;ms3j`+TGGHzu%i~;8-X^CnTs=WqLW87d#Ec?(^rQ*@US+YTuHI}cHSt+ra zb_XTj{Xco4v)k$U4d<a?O+E8G=#3%;v6SP~W_vkv$=ce7 zvp=kRJe5Pg$6Gf*WpCL~ZIIjnt`}g=z20pNNE8i_|MC#X=UfaZPW*jr`+G&RR9^D( zO1q>pFVzHKqz{_mKrg+1tos0`>=r>tfqDxOv?H*y_6n$q*n2$44CnRrTne@l?7r!B z>{I1bHpnF|rdez!kW>+DjEwsb^&Ek7zv5{f$TTTtHmGjw}|_Y+m3t-k-c ziA8NzHk4-nQR=9Y! z&#w-6jHXL;1?s^Rh_=}qkP5XQ)QsckH+sFQtTO55Qr1?M)XsRm#9Q9o74anh*{M5X zl>%#M2Mbfga(iLz+WlrPaQ`b(7|`I0&Ya5q&{EYWy7KV+5h(w9z*R=lcGPCR+}9Ll zPS;znhwylJ%-@rWQsL-$XqUc^p7R=X=97$}0ns>i?(=jG^LtLG3NjQtX>HR!l8u-S z5!f*$CeTjpx}B3Ll1fkeD@XCVq_e%wnVQRwx7!b!Y^v!%#~eNmWpY$hy{r95;mGqV zyE!j(g@_OnuB3F-7o*YB`{wsppF|KCs&+BP9*FeC8xg zUKCormGe#iQ!t|<6h)y<+C^a0sS((ujyH6@muv4rpOHB^to*{iIrLN7JZN_GdKJ9u z{eD>o3_GFS%o;Fkbp2UGhMq`_U)XLd0|TD>l$*|3e~D?UoQ8k8H-j`ctc8aL7$%4wm`7Sblgm^i_Cm81}+7{gk%4Du}@6bMU(LK?xUxGlJdNh z;tQesE4P)COUJ(3kauhhQ68z3-mot<0eU&D#k~Yxtc1m{G-l-eeYA@xQ!RAEzM!Gt z(@G>N&?Qd`SOl4%gnfG(WyKL?&hZkYr*0SC9@z9P3=Mt8m2xNw&h%skg0bHNY(RnW zEP{iNB}rU*%#w>Z4h^Y#n(UPIE8m28>1+TbH@h+`bnOgurlI!9RnAw=SHp-5TR1EJ zqsD~S$H3N>YLb~z8~%Hss5?EBCfe$i;lRtqQ+N9(n93T4=?B6%R41@ENjpioOE+fm zy!TwkMk1JfTk=#gT5S8XhRoW91jS5rJ#;jE>C0#}H+@D|0hZP=+cj?a`9##P?|Yo= z+Lwe6#ozhP^=M==D~5b^vQt0c%LziiX)pn4aFg?s`m6|ps|Ysf8Y#&oh#M(Z@JYHx zzG(K_C35lzKE@{%4u%#CTQsKfd-M};K-0nYn(g+i!I9YkHA=X9aF0RNrhcP_TtayXX!k?B$_+yVrB<#M5$d)rxsCeZpQ2FvUiAwVfR-t${HW_1Qd zqchdez?x9u@<3&qc_`S3DCC(|tyjSxCgRABzog%4jI; z)>^xlS6faO9h>z4E1zs=;6Hb10Zel}x~F!xD1T;N*urp2n1NM@B@l=KhI6r5zM}`F ziL_Ipa2L17qpIu8#11)hbTHa!iQ#tiBes1#Mr4+ zeH0%;FN9`~MPy zH)EBHo}5ax!NQ>Pt90RWmL-^&%-IbRQ>s7Gj1Qhcw!KV@769e@nG-UV8Gkk9`;7Hm zdSKG6V9?0Een^AUrhXZ>(qE6|Cz^fOaI@o1FaZ16>|OP+P^Udbm*?xmHW>e7to>W^ zgQK)NH~mbg_>DQc{rOINi31I_lultl^Thy(#Ve*gZowtOdbq@uv%)DdWU?e=&YSS| zVk&(@R`jW+;_dbMcpBb{?)iC(a*=rVgvpBy#56_I{T3etA`p8z-ys||OmhCL*%jh` z=PPwpuN*BlwdxhuQ&bDfxd_Bc44kf<7;tau1GeT_C9~EmJLk6Rdo{DqudXjC2EzH; z{>=qY$}?Gth964Dw52znA-AIl>s|~}_(UJsc(i6^VK0vm1&*H2{CI~%%IigC%t4jF znjddmoxHibyAJ)*V3u~XVC`m4#VHq3GjZ_f>gSYsJbTA3`6AYGhYV8S_%9b1+n>va z2RG?WVxTzv+m9Y?Z#>M`@WUkmWgbG3XOkh}K11wE>oOL}hRH zVDFR5*}c!mY-a6+nG<{YjGm^p8A zoTYf&k&WC{ilubqN$Vsn4Pl2A^OWGY)RBDA{!GASwZFI6m{7%8UJ=ykb$_NoQwc!k zfY%v=D6iF@$1~~+a~IlgEF%l#!h&}_!b$-tsI87~6RM`=F$q?g&GY!!E$aDS8W1yd zOw2vc4qaV`CdKqsIqw&V9`*C}v9Wg8a*Yy}?V4vU@u$RV9`9_ub8Up2k&5(o_b%!R zfVl9^V}U}$X7YX73Ip#VCk?hlIe=WiA%6yZu1`CSM14yiL+JrNcZU+5!0T)DJP569 zF#+h>6N;`4;p55dsnW?_nk9spNXk$>Cs#UPZPv*8X;f)govf-cW^Lihdu}%SbyUT4 zuydC@yYceyb7wawr{>}IkW&_|b)eGdJw8dihQF8d&M*8s5a((dh!XZf2NxYzM(6i4 z*Dk!0>pd9CR!UvxtYt$Gfe+;-mO#o?T0OGUbS<`_#6)kYm0IKJlCOcV9o1!n5fY<)<#@N_SM zrScT`7Gf9X*R<`wQ6BN&M7wU9F~PKmUh}3*V*iIaQ&Y1Q2^cKUsG`u2`*~5R7!B%+S-xz2z-*6xRH$_f{ZKDe>uAL?X)?BL z&u?8(;nH?mMF9-lVu{Wq2!P}8-+7y#zaCScAheq^(mxdgncn%?(l`U}T8dnu^!16t z7J;orV4o}^RT_CGEEXQJ3kS+(t@#NBT3FbaON@JVfnIEFe2^HBHaip2_MnlnE&nMj z{#NVoMj<`v+UD}0jrrzQs(%mxKyK(r~hMj*P3k-?(8@O(T-jac$u7v7B9u}@ zy=~026lR|WWl`+t9yYBM6oo%)#>w}N28VWy>Qb# zonG}Fey{aPfJ0_Q!Rd=`95F}t)2eLvhPsp4_tmzQ*H#nw8V z2~tY-haEOIOa1!1WZW>_+Aq=3*f)pO=_-8rsS_}LrP~&*1@R)%v^qyti!aUvx8H9|O!5?I}<-RD?9qP#}$%~f;yb?_d;b)H5( zJ!n7Q?eU7gj)HUP@T&?#9>_MI`CWXDNO%)HyTvX|T%V`?PJ$1~g_xKK z)2JL(lfJ^)`M7;JR?s3uPeE0A2@EcG=G&(;WDEB&)0_JMTg$i!;WH!BLLV4$JWE)e zIh=QXXeAI$6a0I?R``!k;%^3KODEzd`%N|Bsie>ABVn0mP5VUgs4HyuLzA)BLA0hs zV>usNDFt2O*4=sYibH!2HyZe4iJB0!mfb)Tc!6?0Vql;UtQ7BRxE1jDhpoKcE0SYj zbP2Pm2Ib!3lrp3IepyfLlA1m36D;vQ!_UQFGDI(SFN>#nAH^+L_kYF9PV=f_55{p7KQyv7DPeU{3G$n1U0aAM9eR&^%Gl ztp4yDgT)LN_y8b_J4WpE!I~AOD{5u#GQv!;8fF#qEFB8G!X?~Md!Mc#IojnwL7U^a zm4o%RCkj!_2iUHo8On?w2>u`p2Pcfk{>&aHc1YQP^piLt$5+7h6LD|P?Hhp9Cx;pu` zsz9%&Z%GUg1psj0ic#09`Rg}N^~lgzPgsoDk9Cc=s=9&7#krL8ILE2O7Q0&!UF!ip zy+1>5Qtjw1i^TOrh>-2v!X<@xisqJR$VFja7MOmFaF!`Woya;~Qe(do$n zeI>}>(-gbzYJO?PvPP<*$^NymK9Hx%*3{MYv%!tMkGL{bZP%tq2cJ@D*x9)w8&bO0 zwZg&tyw@V%T0g_$dA3!o24#HyzcvPJ=H|KF30up~67|78L-5+umjlC!v^M&)SIfmB zRGp^s(`|k4(Zi-DIUuW8OTWi?4*yBOh`wTHC~=~|@Wz(M@4nwN41O9P)=v%-R#~VY;?Pj${~WTQaHDs! z>8;L;mkOreyG{<4=wG5$cc&)5goO#kFjr3*Q>aMPtq5Tw9Qdt&vcjEi9%s?WG*sF= z2Nyn6GQe2tL&tOV5?qM!(&LVf(tliveFsroCs+U`LeKP|NT_T^&?B>N$&S;_;vvaX ztZRmfir_S#rx5JO){VYV*tlEXG5I3p!H)g6nJ>pB`d2DAwa}?)?2ZCx)nC1;&KS2S z(d|!sg8FAx2-qgZuIdj9WB#B2P(0yV*4aCkSJ!(!cP?JHkCkNq-SjFd*QO~3BW7Zv zIv5={itPP zoxQ#CoA7H3`?L@o`gwQn=lp3AR!&&wCqsvM_q}hzIF6tSCnskP#(G7?NV4x%2rgS3 z4w34yr1(9Ne(M=DeW{A7RtDiJBP(=hdjF0#Bk6>MGllwvLE`5}IZx3U@qP9`e02F? zMH)=n=1{0tz>~!6z}=ms@13*Fpx_@_fC!B1?`iy58+1=`z>a3zpekd6Yv4d(z+4__|sI zNn2;du(M8`w6K=C9<7!XgyU^#$&~o=8e{Ug!wX&T&6NILf(*`$^ObxPJy5mca{s}uZrQRBOD z6>ipIfO)a$HTO3h;e<&xvbXmRy-noAJopoT*PzFK^-Ksem6*9HM}x^;%PapSM`>OX zY0NP0;UwI2Cz`ajpYAeBc7$p_xXU1;pPrsAA+!iGFPpJGQDit5rouKc_oV1og>K)5 zV-u30i5!Vypu%taDRFA&Y}$^2Y)3q<*zM*pbDL9S=K_Q7p{NvtJrUAA8R58Zqc^lF zV+9N=)du83_(mS60W*4a>YZ^<1?V){yH7pJqQAp#!_9w$CU@K9$`ZkVxS=h0GL@dCoC_!B`q!qIz|a&&C?l**R}75$nqKbd8G zL6%M{SRkThx=n;!fNZ9Tm%O|i-ciogR;yh(l#si3fRIym==*GIfHsS&&E-J{P>>8N z7qJkQiW)^X<$K!+tYR@V00rk=B*@^p@ubzMj7e`OW!|#eA(*!u^o_E?_5FsRw<@w< zbUzuF#JsJ_AKKl@zd~FpLYfGo=^0&ZbyrAc)Se12rfEfYH8#IRj!MrJC^lk!?jRR?hrNNln`^b+s@lYN2>NFuQ@H*lK4KT)0oo`cpVbO4f}ACiD`k zWixTojpNzLisIW>%)_RK&{N=+C$R+UftA@y&=@MxIa%gb4w|MG=$3s{A3v^DhJWOo zu$5H6kJ^+K&AUbrNygq$wX*<_p)W1w$-1t!=f3ea$J)^Ol-Xg22 zP+RnL295j`_i^BxMN!kh{^VgmexM~1!Y=kw>LBlqp0+N-nvxYfN-#MP%0084FAB=+L`h?e9O188fvo+U*9{UfO#xlBwROjXp&#Gx&Mp0A5&0@`WLv_u@- zMnB@^16@|Rw=x;{_vG}gDYUF3iFyB-Za6ZIr!O_d@3{!57sFG-YG-DT?VL|k?eSRv zE0ew(Oa4VCA(yV86&(`K{sg^R15-r}5JzL6uG8T>bFjtaydF=Wz`{U1whYUy99;)B zj3R6vV^-YSnw;=VRta(fBwx8WL8IB|#d>n0?w*5V@ocbO+Jh*N^T}0RzDKOuAoPlN zQQ>QzHZ`2kE#mysa2%&s?B|E!tfFrS!}XdS{%k7|d!G&n70uQ1EY_N2s4((2PWiOEPCHfI2)cPI;92K6ArB&ur0|fHiJQu^*FxI9>TfcI^m|cpsRp z>96AI?RxoHlO6& zHD)9gi}It1{U&^8;cTC{P#wm@Y7tlZbGM2ytf|)=?$nS=wh8j?xM^#ET%eV%4DKYj z003fjKn(JyA|5~$S7!TE0a9Wi>4@Wb)K8xQMne~K>aq&*LFerE0L)ozGxZuQ@jDL8 zZd0+Uio~(P^O;}* z@G0?DB~VsYlU+@EQNc{4k5Wh+>N?2(^toM&G(f2iQIOo&>eP}HG<^0)pxzJ72XV#s zbBVRIsB-95Pepxpe6bl$=CRq7cO4w+kEw(A-NM<RI{UA0IG`5Qb-Hp!p`GdSW|QQ@VeWD+AM^$Xa6L>w+zE0fV13`rZVTL@SuELz zlH5?&kYs;Udv>J*j=sH#av}RO+>f2hhqpfRcG^(TnX3`+qX}1nA4sHB!nXmV*I46w ztX`HKer#(@XUppyd>PqFyY1wKiZmW z|L07z;5h>Z32x>X8qohUBI%33M-4JpRieu$6TgyH^WtPt9|qPQm-s@>sYE;xh{uBh zFfaOnLxnIwUaaKlM12j?oh&u%zC%viRKh8nFjv6}|AMV&>6{Eie7~CAR~i#wQn{l? ztHYz9#eG$g&ab`^YLJb|DOmcJdUO9%J`uwDM7A>|=o#_*;{gXmdhOg`dYF$9bzv)jPX!y==LT8jFr57;OKGQ4ni z<}f!t#zU_v3hi&G-0Jhw;EVZ9pUPh%-aZu|qvWl-;zX&ASvTyZJgRk^|Ke zl*kP(Al4OfAt<(+dRj1&_89mKfW)_+$nsr?%I>)w`i^q*#I~cgK|S)vEJAipLH-b$ zpV_)jkFPaBLJOLqb`JB<3{$s)f@0%bXQ^XMw3zv{#+L)GZgP?lfZub61q1Zwp`!jm z-#AHXUM=yf3W15j*Jv*-yD4*Tk*vL!=gDZL8@*-l2QMRyJ~hAyn#+ZsnuMw|{C`$$ zPaI?xp@J*z0}FWgxG&d}amviQIkc5IJ64^;;`W`#f4NyyGJ(jcO}`+o&B(+#2^aWZ zozOTGwEIq{XYDf)T96H-uiPEttY5@KlfREM5%hom+Gdc)h-Ien$^uWDw2eH;y z@!-QKBNj9ynYIkY;BV;I8T%c0mH#$LG4IXqgdJq9JPE~tsiQm;DXNf9in%#-C@B3= zd_}7fgKF(uD|lRToqz4n>mOH9^iCjVgvk72_QiX&gw_$X`U;P55)oPr$*Ji23D`KE zHSo8S@cTEZ6b`+qKWQ$3lcC8|Yz{0yYBRS6?^hGOz1WX9qn7HPF6+M+bPJGTXq5E~ zv0w@I?M`CT?km<$6mGhEsw#Uy@cKC|Kesr2`=$^bK++v~6B2^=bzw=M7>Dr_?F?2E<3ytH)<)m>zE*gF*aXvJI#}r9?lSgZ0X+0mC6(<-?kdeTUrVB8lXbA89+= z>e;Q^Eyf(Sy@fj2FObCnf8gq7q4e`js74fZ%MxGHgJaDsH5H|RuNak)LV>_+9d=lH zViVOuO#y_u*bB-fzk~unA8=AIZrUy$DjFcQn7u6TX%6AhUOAz-X-GS-=0rj6`7hDK z(!J7-q|W7(7pNgWJ{$M;N)(gCZZ17Dapv%f2dt-0;zjh;w*XVO)oDs3+txJVAj%>y z_X@X*d#wbzho`7$94#p9G?UKiixM9OXLsAsnYI7ozE4UUGjz2g|6AcNzIN%e{ZX*~!o z@9;(`NnWz-x9zUL+~-WZM{Msmb%PH)%Hn~1D z7D!1R{I`@OJ^+uDP?oOOfayvfVC7^taxH`P8gR;IVbb)QRX+c{BHwhI@}a#0teEA) z)dG#B!>G5)<>C>sHl0^5F@UrJkQz47AKyGgZsn5{0ugIn;IM-}eMqW3tBYq2by4&N zI>K>|f@4`FgC=u5y#pazCJmX_(eo(|azqPb4m%p*w9=~2i}9u6@kbCGLA`1visrq# zIxn%L8m1aOFUf&6!I7U##A}`FPkp`pBnwCoDURu_Pqz2681NEit|Zh#FWExZ)m{KP zEy$u=%|e7w>R`ZO~gXCKiguHow|ZRKkKS}njI7aakZMJ zyIGB8{}uAm(|d(x)gKc$B)5z8b}AD?ZCMeD1$=zG1#WkG2UkH)r!CKeMB~dg+?65f zM;X4Yy)JvoKq26d?0t2CUQy*@RK1choOegH3mhCj7`1EU&z9&%n)CL?^=-$Q*xIvS zxmqom?*S3%l7c1HTX;z{aHqknHh|)Jz(ah$x6@(tk~4QKSR+59s3}+pi(GC4w){@x z{Y!%K#|gc0N13GUH`E*%cHiy~Rtv8nBG{?@(U#&cmK zi4s-^4`=1jcKgz`dmFb%Tch)*hyyuAT`8>aV6z}ud$J_0@#)F%PxNf=)%+-YJ6dFUc zA0L~2S#XX#sJ|cRr%Mf>Kai&nby+619TWNr>Ia{}I#-@O>0@Fu{<%ZM@KH6v?!$-F zJwBIXZss3r87d6t-B#>J{j2!^bB{H7QDS{#of_dO*$4qRy6FgO;^%@63!DAKig^w8 zA8m#Qd_A%($pzQ@B&SBk$^rsccrsRGspphD8HcZyp{ z5thq^jLb*SnV+`PKc@RA0nJBqk5?}6mSfQ1Ly_ef#t*aG7i$g&NVkUu zKCvS@q?;u^%h9T11;W1LDcu{id>5QwEDC?hA8lBDo!?hS!&?CMXhG+&SU5V0|jeuXOJ zJQCZzb_eI-FJLl(j1*Wh(2sp_`*}hQ*!1c*nGaO8$1oo061iOHxb78<;c=<9r;U;O z46U!%!;k*Nn``mrr=r~4B3)eSUv0MWV8=PD${1$BeNyEdNw~U08iZKAtrFZ1V4pjvpXVQ zus@fl);f#m!U`*oU^R~6q*=b><8dHa_79!?cFXdqidDJl55$4gF1bCP>P4G^1o6N{mO z2)9LMw2O{a&jx$=y}&_zX{FMQ~Ly8ocL{C;v% zzR~&}odh&)$t-$mVQI4tL=jW9&a^-s3UrzQUh^QQHy683jRtF{j(Stb7}4!61ve3- znl7a4vEoVV8vlz;?qYMS#IuxQeK-EGDnx|1f4(#H@-G|P8Yq+)&JiFW^l0WI(b(a_ z??mZRL&*6z%i%0r|F}+gjl@r$V>}m)c?nP*7E8$(cbSMA3|iGyhA*3mwG(wBK?s(=G8u!bK_C8^>PC+ zuyPCYQx+LHbYjGOM`22^?jWl!P{fe}Wbu{#_`8j{i5Sb)M@J__GAK2^@T%%GFq42l z8gJ@o_{TV)I3CrAIMWV|=_&QkUr$wog?ZJxf14O>R~Z|Z)h)fs8#@X}ju9hb4`&;_ zaB~^}H(w>V)vVq6)zGNOYX@DaL@G$NE0w_S;uHL{Ngu6v;gl_BMgIn+Gd*hWbTfop z?%cQ)*UIWzl*y~FQQ%onH+FVY9DCoD^)WJcn7;b3J8=Dio-uaFs|0yV>aE))k*%kk*oDNbM0RSOOkz5zf% z0<7v3D)oxh7lQm-vGdR71J~dHiO8u-Q>EGmnz*3(TUg~IkI8#GtkUedBVQtug4|h_LDeBJMw%X@0y=vo8u2tZHu5zpHzWINQeRWh- zYuE2al$K8ERzO;$8|g;6Te?A{y95L&=>`#`OS&6Gy1N@TAbB6od5`b=#l7Q>;qVW3 zt+k$*^ZCV`bdtdhv_)*|SJ=Im6w_4R4xj#3$>B}w->*eS=t^}RzK>adK7W_5omZZk z#C({*#=rW}()dwtj~=Uiq3E7};`a`5vN*rV-Q)F+I8iBWp&&r(C6vF!_1ijoS_@<} z3NNr5j`!S4<~)J<0X?d_eLwi=FC_$udETe1R#|;RNo~JI&>poqWlUO3LkBE?xMSM& zWY6ifIGNj5^pgal>7{0bJQ8wod459@GIXjiJgOJbSZWNNI0$LD-~fHIz4Z=MVU(1M z`~PQ*AL`Z-Sgqj?({d;P}aioLptq&?$~R{OUh;r+*o6ko64MqN5c; zvP}Q}!+*RSt>H!uL1fa^lhelN`CrULLMOsxI3;2FG5?z@=#g)mDFkUzphN0^zS@KR zXc~)3kUI3T_Z(czS2k^@H!%=1bOmK|tB zMC8b?!z^!opXKFG+cxEbM=DtW2Gkq?9baT!mBDS06`@}#lcCGt2sa}<{uR6irI5s= z(}Sq3br46r9FCC6mDx&MxPt*RMtLHS7BN@2bsXSDP2 z<*+%~m3z1{)ZIoMJ86 zLH^*P&%Q%|kH01-f^}OW#H-6xh4##^xyW;>CpCx$Mf0g#@JGNQf~554-1H!D9Opqv z49W9Jfo|F)(4|4Qc6Nll^W|sx>PoiQpO&u-upS2rKUigfI?yo;bjZ5hEY)~00x`#A zGAX1vVeU7pOl@10(HGimrkr3t$UDLDq9lJLn4zZz^zxIUlL{ky!oQ(l|%?S3#d%x{KqmSh(5 zd43#l0OpgoLl5Q?L{%r?$AYj|Ip^eBD~qLZ;Q=Qsyt&2j!3QwrdC9;v031e6(6eI! zfvc3|<>i8A9y`#JcG3IV-xa(v^>Tx~irs$gtQ{yGnl9Q@>q#$;(GI;?|G9Ddr**&` zb;*2rvGTsc&1jbm%v^vfapz9T<=ulmIh<}+6yV_kmMf{81IaMf{64o$&MaI<(uAjY zLmOy%DS<78f9m2@Hi`V`J{!TAl~!W}^{H9#KJPn;EZe-S)TGFf^rYTj1+<1NI;WJ& z>C{mpOpodi?)SDk=|AGUR$kVNr^h$aZ|S_h+d2&yzTcRy8CfgfRCP=>!ef+|o=wA6 zA@5t#HDdGhyjAoz8{1{x#D_5-I3cqM)eCiT<};ll=8H~z349Mpg8De4WzuR!aCX^0 zcO4AaceeN5^9B}6qjKE>yC)g)+6%3jBw6h1^zCeR-e1O7!@I|JYd5(s%;U&(F}Isl zrvDmVi~Kjm8$3KU87f7WoeA(;gdy5Q(a(#%(!KW@L}GtN-FVArGd6Gb)|uMhU*xPu zJCF@$f8b5P-2h4&2OmcGb_yxtJp&X>{SYwZeDiM9h=_=B>Vbr$AY~zHMmN3=8<7zt zjV+oebCW^M}V#oc&#bZ6oZ21T8G1t_oiMp@v;)n^Rc@+vGVTq(`JubD7^Sw z8yswetH`?J?97PG%*$Fe%et0v*8I~xD#Fglk4$5! zF4zY4-zJ5!T)vXELx=S6|K^FX!`WXNQ5bj`81vuV3&E0I@)Y}@*@lFFT2=wKSK>!F zu+V{}@7>|fhW&j@&M@`D9br_C0yFS29yB#os=b2J9JN`zUnhcCkg|`&w%QV|pNUA0 zT+-gLlBXRP{;@0=MK1dAJ4Qu_>X4_(@Wi&JSY|z63B>QOj4) zyQblDg@r&R4%66BVBUD9N$VvCP{HWK4|!3T9(fLDW)!!0ZXByg65Wxs3tlbFhTs{aazl zS{qdJ&hGMY99uuosyLjSI<$LTR0!;?3n{Zf1iYMSfq9Id-;Uj)Qx8<%T)aCXhTQcJ z3q%w!TLCTVDA4qO#>qzb-H>0CuIE{oUR^Rkmy>%=?+-2*F3yR)%a%Z$J>M_YQca)r zBelQx)L7fF%VGGlsrYN{KRyK_CvKgw#lMUS2Iv3~|HnAcw|^}9p`R{4)uD^PM=sj{ zbXR2f@|y6Y6o$PnAeNfF?^iQcF+aLpGhs`{T%JzqX4JM!=HK5Ok4jLfHqsj^7iY#8TkX zqOE<`hhE(I=1m&xHmeTpPlr!8W|LgP)QmBYa-yIo{@_~ zqcq=p54SQCxZh4g8B+NCy>G41G^@gZ7hdrDBpdtM2I;~*-_kk{Pc=9jBeY~zByf71 zQ(PijZ)iFEwp%nPnVdHRz61`YB2;=-1; z4V0kL(77Zx^)?73r~^i;XUi>HIuH^)=krH5WOGstV3>kpXmg9!Y`QwyB? zyHvHF{#Or5?7G>Jj-xH}fcq^OiYZn_9!hWevrV^vyOd|QN#AaAk-7F|L?dIye{7ed z6$)-8NCbFc+$9|ir>Qo$HD(|Ig<^;-g=A)TwWw3MWjmx2?arRC$jBGXmnjH{qQr^ws{Z+1I8ygA$pc&9;nGuUisdaj}r(QSw zP;`HRe%4<7)GjLG!Dks?Vk6_u2gtwobu<7gQG8TOw*X{*rk}CFgMYke^X}C%B2{FU zA})&uic`iZ<;l$DtqSTX12m}npbwfn)Vz3b#ORb zNB0%3n?J3!UDLLj1BDG}%lye|ZDR^PY8H;?G4#e*NyopOEl;gXy&IqQHH?q`kI56bM??`VBLZw-VSL~ojhHOj2`b(e$g}Jr}QFt{-9$%zFLD( z$6=)~Sa3FRPrm8cp~J}eLhJZ^*~lP@dWK(oA2p@HojB^dUf{}{)n`1MrSsd+qH6kn zJpIWHDNZ8P2KyrfxLf;Yu`lx#QW85)XNwecQ_!L&fxpd$-NmW(z)tx4kl;IA)ClfZ zZ-}vK$X~2GG@OjNsLQxdS)r;dS_mgNBTQ>PrNfPnbTc(8^g3yT%tkBmRF7RsA&yAIg388j%T|doI zxk;*`-36y=S$3_wJ&cc9(Y|lFkD)UGhZ+6YRw#xL_Y4w(Ysl zF7Ta*b(BqwPQy0RUmW7O+|B5(Kms0Ef9 zo?=E1Q7m~ryA*sK&nEh7i`++GbWywUgR27B za>mEutR}rAWZrOE#S4ziavkBOT$kx`oFx%^Z_f_~(Chc{&w_m_+J5{7Xt)9X-lF1@ zaLK5y3Dk!B--V^fmg7$^j%L_?WK0?xN-Dy9-6bSXkji{}E!TIhYTY58&$u+R59e|5 zd%iRgWva=AnwJF4>*^fla{~6#bT>z<>4m3lxDXl!B%wQ;FX~%xr&Y%?TY}xPDY4sk zyl2idP4}a^b`0UU)PXGCR8%qSA`*?X%HF|pX=r!egQH?@GX>&mdW)aC)4{~_TMe5= z=PwyhxhIG+-qGpdq^W$EkF?25t6|X&hDv^=uLZbpp%X)SB{ts}m;$GB;JeEe0RsF5 zWlBL2^~Zc@`zB-OO@mc7Dup-ir6?$qsXPSP5ujAO=lXG*KeEa+j`_kYt6P!v)`qS- z*jVu%GxQ3NTOj+iBA9TPxhbyJ7g9%64L(OuwWGg>A3hzjNW$~+OEhEztBCemm?U|t z!N@txcQU|>zn$6b{icz^b7M}IV@-8&RES76htmaWi{U6GQ-7jrZ0#|^M-gqMX#Yh| zd(=sr;KhXnKIRYxe_SJS>FdA)yd+V-$%`wvIIwNE!Ik*z3G$ra4+Z?GSiwF-{QOpTp)b?!xGK)H%I$-+Z;^!oV=lne;_7rODm1?}GXl z|NQDIr^B6eL#-onttNcW33A?=eZ|^&lADv&;gNAX5`Rf^uGli?c{&Eh7cEBx z#I{5?>>GW4BZt+_`%>8Ji7f1f?cbwPLd?~Qb@S7`v+J8!YWsu>Kd+t- z^3=M_)-dJXO|?$L5#Jr(irCd_EA)ESNJ6jrF*)O@4KWx@DHphI#HK={oz3z0QyZTT zyb|H)M6_s-WDA(MmKoR6OEt5rDGR#1E|E45yy4UQ;og>JCPuM0Uyg{U^N2k@FU_>MWzyvV~ zZaB|Y#%rUQfVA*$2oI*d(Qf`AaH-Cl>RMTA8jf_D*{fn;-Vp@GBfBZe1nWl}Z|dWw z3cQw9tWU&fkS$iv<4^S>s%MDvOgPBWVC`j9xI)s?LYwNTHQ)4%HV}c0klE02sis>_ zEHxymI)*#x3Tt!-(y}8z8kb<*R`M}U;rW`PfNM3%>|VvpCniS5mK|#3P8 zm&3cxtRd6p8b?4ZG?m$Ey>h*ej)n5S&toW_8`x%py7jE-!J1>sK#}v;){56IoWPCG5 zL@2A@#eR9BI?T{uXrpWA+3x;U&hxw2Edg_tLEw|<{lbQN%r&_^8(fzXzWi8cnZoK*)zMn}ZP zNqZjhcsFuT=-MCsz^71C0=uH{48cRqZ^Q96rJi*qEA2A;xPH847b7;t@rlej%JZ<~ z-#%F3$8AF|^>1&J@LH0C`H}l62+`6ukB635FNcsn=#RcDbq1A6Te|a^g2t~sV$GRv zlD70v(;Fjf8%uc-OPA-{yMK-?;6ST{`(KQWj#7ZVxr_y`r2BpNh+iA8M6O{iFUj1y zFi)-MId7$BA*VQ;FRs@Ha+i!~8c``QfGsRCXd`s0pNmFBodGnwxI5ksFNiu$vWWYx zw-aiQ880y!VZ6(ibhWCEOOwhZGqCmpK^UNOl~Iq&%g?rk4A`gZwE_iG%*RO zi_3eD#l4Pwjn7|OK$vw7qaoH#i1;q3KRXa04bZGHn?uyD*f5%3-X|O@^3@m;Uw&dcSUN1> zH;s>BdPKz9jaaaVwrFTX43kw!BP3ATUrHNGT z7xf^zCK@_t$>W@__t=_;!rf&Gp%Z$n*QW*<%4REp`)}Az@O7{ewT4UZrp7&ho2wah5r6e1M9_V3L30t#S zcP0ndV8^9&eBrbZ4^l)S3D9Ca_PNP+kcjr=4t0Mj; zz=hUb?&e@b?Pb5!6A{No;EUBGDm>Ves|0B&;5#>F?+#T=+~xUMVr_*=Gq^>Jj}+dG zbNLC~9cA+}min>n`04$=6SsdT4~>@Y&UKlxcQcO2-E1#S`0ICFGGidg$J_dqFa3o) zha|eA785k1&L45d3UPnAy;t&YN;{~z z(-=v-?U#20|7ZcKSY|A_&Geh`*_(d>=2`zL%+SRnNj z=zYz+eIEPQh=q4;*sZL^J-b;`J6t_`t7k=BryZF!XVp3@Lp!L`gwN~C9VR{=W)j`M zZf?q&&r(u$0!sO+2ItuAuLm*=Br=QdJB$qkRP@=z< zi4&Y-c(Ex>!^zRv7fmhpWJw)YE}zgL@5Zh4{a;OhgSDd+N#h$lCZVTxmlX7883Dj~K`iL}*^YuE~mJdiU5MR(j(?9(ky!bI3h!Bf|!DiQ+aYv2$ z&t{io8F7z2vPh}G8K@pW^(y%wK12cT{&DQ9KB2~Uol1LCr8H}&v@|f+!iL`xpVrs% z(?5&EGSbjsarnjXPFcTM-kDIp5@%HSLVBq$Q-v66eUofi%&ELZXIWGM95owdEyu76Ls`PCMV$%(hj z{wcoV-fnQ{XEp-9yV#8KhPm3=^^kkf=S=iBmWv9bcZ zHrmKK5pf|7N!Z>u@4*FI)B3r0P(H zt)>I4kT`0)hyd@aOel*Lf%XBS4y6p|EbZU@>t!E>yHe70NYMu|M+#W$r8Isx0lwABw&S!p#4^(6U`L2?Mc- zf){7fg*M6;y~(||a4cV!c~d2z5u`%brItPgCr9L!)yj7#k3`}N%WwS1ICi6xUPHNf3}8}y2hwE)*JhkU zFL&OJi?hxK!)2)9%wPE>LUHp^4uSH8weSv%WCVTT!M{>V6$&V=llRc)I()Si3<>m! zX&#@&R8CrR!oj=C25e{nl(;L zO|(K1(iovzX{sH)(QNO`(~8TS-QUw5-ABCmY49x~C?gC9DqwM1eq_U9hEzI1^>0rc z%*W|&mivbd_1zx7*PxN0GSsg3B7|BKtE(sC#g8#8xzY}2w^#w}s+-gk3QG7nXHLXARot^+Z{|zF)k-iE6pYSzTeM`yi76#PgGs$W zns;iCy<=*GO7Bq+16_T}SLvm^4(VNI503sS7=$8uie93{kOT0C}3 zQ`<`)9Bw4pgl#P&T~Oq~bQ`AJEi=&I+B0HIgld|zdY1jPnYV2Hke=F7J4tx@K~o0H zH8#MMjihS!n3RYS_pXGSwi&=a%Oy`c{wy;o<69+^)fQ9B_~|)Rr{4Pm*aw zChiudR`TwR_|^L*+@6}m_pyOoKC&$-7bEA8c98%h<7TtT(qyTE^t z4wzwP^iWZBHeNK%kA$;UG_PQNl3fptJ~fwFjuVW0WgGcr+i8aPJ)@}*=dGm&?c&dp z7|s;An~hUtH`9I^Fn2%WZDu!2xsLZQG~htXT-DOtJgCZd5SC-4%-@BTp2@zvBc}au4@x>oz9mZ$#{an+eAC{VhDK%px zJ(Y%_>%IQKY?!H%e(q)!&jl6=*K)HMxi3Ep2tmC=<6{&eg)Q~#>(6_7LFa5-NXxd< z>L_#;KbbxAi^TP3Nvg}Mv~Gj3LPf`X4AS%_{3;Znxog`U=7p)#4*NPzf3#gEEt$YZ zaOmMqG&@l&ydEPKEz!~QitrOMWdkHA=*`H~0#1Cb{@iXS6RKC3Sq;A*HT4tqy?c0X zj%oR!)TjM|L|@q3shQuwL7ro?u3dYFxZm0{4lmYz{Brf?Ncl!ku;(kxCj>Aw;TuQ z&@d)fWK3bON+eq00#nAnr->lh=SOZN16O#u>o7|z#Zy8xy?e@iCOvG)`osjXmthE$ zn5~9mBH2o9EE|5##@POp*}qUpj+mP7<(Uqi^p{`WdpSd3MSdn64T}q(hYMu~2^GC* zc1CFQ7qRz_*US~IR*cG2)M|qjV}&L>uTyy>YYNBu87TxGjxyT?B_cwOkd8{j`$h?4$f!3Z(gRs>%bw8ctiLA0J*h4RuN34a|4DI^HV;q!arTMW| zjkGgE0blmr5pZn5V!loZ{VV;JdV5Pv8Mi9MD|N!~9Mql^w-vBqpG)3d>0tl$ugCvs zW9P=PDJ30|D>5K)|JZkExWF6HOYYe@fF-=_fLupT69Gam}EUN9Tt8^>erkCtg3CH-}>hJ^WOC>xg@aG@@qB`l@$Qq)NQ3-+0GG-Jw?f-P48MB%c zDR8?UHlEp(`FnPL-C-gqBvM=X2@vnDi8DQW*8lsRo03AAK5gAFH0I3yp7w9U6KZ{J zOAm+d?W28C`bj!>>b8e_YF`c*!QPjIi}Xa`SMeatm((gIP!GtHS1 zHvK2GriyiyL=duAiRj$PtZ&SC5WABd=US{<3yE&Uowt*(;jr|03-bDWq(`+__hWZb zqt|}givn80(Qjy2f;;<^?SxFf<+0()sMK7r>^i`xt(>2TQefOcm-77*&ah%^z|Zk> zIb-m={w4;p971|r_;h4#*;m`nQ+4*TpzIK8N@3y0_tI+YPDG!k$(A{{Dw{e)tr4l1 zsZI&UdB}USS3`V;hI<$?@79+^p>m_4le>Y5G* zTcJT8Mhe-YGowYw;~qiWZ)!#&xQQj!=-G>v zB4?)5`>E$f7%C@mvc*Y}amvOjUEuwmQ?W7DX(_n|DyE=6v426$@dKN>os2vHU05mH zNJD~(s6m$`vaGC*5mgf7tE+H~csB;QakawziCvaE?RJKPBo}^Kcjsc$dU&Wb0(n66 z*%Aq9@GQ$UL8PR6raLXwdnY0cVN+x%6B_MT*BDd}13S-qJVhloVen@3fBh%P)P$%F z$2)utp06n5zyD4ikb@Q!EI`8u%i<^GfcRft@E_?grC9Vfr~a?+4r(Wp}S$rZh^ zy+==*I32e}0H~!rFc90oOOmZlYHA&~uU{(OOqP2O~ zawc|0S3`&~ZB9EBq(E~PR$qKEuZ*{y-3)(ef}{x+;^n#a*dli7PA0yBj0YKzRJZMv zlGe0}MBU_zR*#2aFJ`z+jgu?hdT&cH#s$bOg^4m4v!arG$Ag{7)VSR<-WfI?Y0a60 zKzPxXL`^7el{}?+4yv66$>UlKS;?focYeu5{`pG3GdHMBSy#!5}aH3HUs z?^t$VAjFcNB}1}^*t`yVtxHj(R%mS-)6dRLv+qVzjRpdn-B;(A76IOVG*>M?GHV<7 z!NWsUfG6o{cWb}9EPPVmkyBB})peqeQw?)FAyW5}b?=bWYhUbnoCAKb#MKeIC6 zL`-4nJwK?dSAzWcu7f<5`eOD6Gq(j+B{5?6GQGUjlm5`-E^%=d3s)+g>b~6dsWLB+ zQR7l_z}vj$$aHwml`_PFf~XDU{LR&Fm|7f`y-r^v&JkrNsYPSJ7)ntq4eli?HnBJj8OmEE*2` za@kA!d$Z>VGTcv5(>SBf4;Bbs%*Ma%XXZa>>n(D%-`R2grcnujj7-z>%5BReu$3aY zI*p-z2&3h9G_`wjWHq+vfjyO9m>8Yjd-Y)iVLyR=M<($p_YrTD*2NdILZ?3qL6U@2 z!U|5x3KxE&d1Y|@c&WNYGV1Gie_1UjshaKhYQL7NUljd1D+EQ`25&B{&{lSn6BV-z zzr-GGj`1~TeCP71UeBn*dS!K=?k>9lGd?h;C)?MIZHOeS3!W=6yK_%4j$*AhI!V9c+=AO~(^LJ)`*DBz0 zBY#5bWWv(!gq)yI`PmJu!seEyBnH*l5Sj&dA8!*5THN;i;}-B$i=^h0);f(_1o$Z@ zr)x?t2H=Rtq@Hj_irkMdU%?gQS5N0N6}5Q1bAC?%$!My3D)Q)%j_jZ3-Lq%pc4@fc z2e8q=Q~CI@c}qmO395%1(4tq%DlWo%NMO(BUZ;Cvy35kY={5RhL+aL&|)dz#!_BH_A5?qrrR6$*yW=O=~3q zFwCJ(Ei;s4sroIpx4N2%c^~oj87BiEV*^LJ%*CF-m{p_W6msO+d?VY@p_ZovldFVE zRTtw}eU5w+n+3`_cE(rHlVgOpE&Q69_OC%DMV1?S)Y8<=VIcXA{0{2h9_?Vr+icCY z=D^j}rG43})%>JomHtO{7Fjn6U(czjs2*{Vu%yXZj#yH?xPdU%=1_(|!S z`na>}aciif;w}s%Lh}_^XJ9Xyzg3o3fHP-PnNqHT>HN)wa2o{hjhk-q^h{S~cZW*R zU0d66I0iihqT@?}hEt(|VFZav637yO-XBPaZbqHfMcV#+&O_$|YJIhFD3e zsXBwF@h=%ZUh+ch>UbXxlVTpG&&h8w+nLoXO0glJs}O(5#h(CUY;%MHn+D$jh-G67 zkRo#){eD-8P#ee}jnHr7x-w6xCragKvAyT38yniT_w7YZh4nt2IqTbk63dRp>d1oc zpX`X7?nK2xOFB|ZQegf+v?m5w!m(f878kZu_r;Y?6V>W^qty?ffP-#Njj=&{xkE}( z;nQp0T!24tZf6) zvHJY2YJ=zHRLVXc_^evm3~IO}TJoUpj+X3JIV!nUkx*xw5