diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9dd98123..f46bccaf 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,31 +17,28 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.9', '3.10', '3.11', '3.12'] + python-version: ['3.10', '3.11', '3.12'] os: [ubuntu-latest, windows-latest, macos-latest] experimental: [false,] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v3 + - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install flake8 pytest pytest-cov wheel - pip install -e . - - name: Lint with flake8 - run: | - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + run: uv sync --all-extras --dev + - name: Run unit and system tests run: | - pytest -v --cov=mokapot tests/ + uv run pytest -v --cov=mokapot tests/ + - name: Upload coverage to codecov uses: codecov/codecov-action@v3 with: diff --git a/.gitignore b/.gitignore index babb6b0c..82d2e580 100644 --- a/.gitignore +++ b/.gitignore @@ -111,4 +111,5 @@ venv.bak/ .idea/ tests/integration_tests/run* - +/temp/ +/.run/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 78830bf2..a15090e0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -40,6 +40,13 @@ publish: - python -m build --sdist --wheel . - TWINE_PASSWORD=${CI_JOB_TOKEN} TWINE_USERNAME=gitlab-ci-token python -m twine upload --repository-url https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/packages/pypi dist/* +check_formatting: + extends: .with_twine + stage: test + script: + - pip install .[dev] + - ruff check . --exclude docs/ + unit_test: extends: .with_twine stage: test @@ -47,4 +54,3 @@ unit_test: - pip install .[dev] - pip install pytest - pytest tests/ - diff --git a/data/percolator-noSplit-extended-201-bad.tab b/data/percolator-noSplit-extended-201-bad.tab index b6fb66ab..7319a44d 100644 --- a/data/percolator-noSplit-extended-201-bad.tab +++ b/data/percolator-noSplit-extended-201-bad.tab @@ -1,4 +1,4 @@ -SpecId Label ScanNr ExpMass Mass MS8_feature_5 missedCleavages MS8_feature_7 MS8_feature_13 MS8_feature_20 MS8_feature_21 MS8_feature_22 MS8_feature_24 MS8_feature_29 MS8_feature_30 MS8_feature_32 MS8_feature_33 MS8_feature_34 MS8_feature_35 MS8_feature_37 MS8_feature_38 MS8_feature_39 MS8_feature_40 MS8_feature_41 MS8_feature_42 MS8_feature_43 MS8_feature_44 MS8_feature_45 MS8_feature_47 MS8_feature_52 MS8_feature_53 MS8_feature_55 MS8_feature_56 MS8_feature_57 MS8_feature_58 MS8_feature_60 MS8_feature_61 MS8_feature_62 MS8_feature_63 MS8_feature_64 MS8_feature_65 MS8_feature_66 MS8_feature_67 MS8_feature_68 MS8_feature_70 MS8_feature_75 MS8_feature_76 MS8_feature_78 MS8_feature_79 MS8_feature_80 MS8_feature_81 MS8_feature_83 MS8_feature_84 MS8_feature_85 MS8_feature_86 MS8_feature_87 MS8_feature_88 MS8_feature_89 MS8_feature_90 MS8_feature_91 MS8_feature_93 MS8_feature_98 MS8_feature_99 MS8_feature_101 MS8_feature_102 MS8_feature_103 MS8_feature_104 MS8_feature_106 MS8_feature_107 MS8_feature_108 MS8_feature_109 MS8_feature_110 MS8_feature_111 MS8_feature_116 MS8_feature_118 MS8_feature_119 MS8_feature_124 MS8_feature_126 MS8_feature_127 MS8_feature_132 MS8_feature_134 MS8_feature_135 MS8_feature_140 MS8_feature_142 MS8_feature_143 MS8_feature_144 MS8_feature_146 MS8_feature_147 MS8_feature_148 MS8_feature_149 MS8_feature_150 MS8_feature_151 MS8_feature_152 MS8_feature_153 MS8_feature_154 MS8_feature_155 MS8_feature_156 MS8_feature_157 MS8_feature_158 Peptide Proteins ModifiedPeptide PCM PeptideGroup +SpecId Label ScanNr ExpMass Mass MS8_feature_5 missedCleavages MS8_feature_7 MS8_feature_13 MS8_feature_20 MS8_feature_21 MS8_feature_22 MS8_feature_24 MS8_feature_29 MS8_feature_30 MS8_feature_32 MS8_feature_33 MS8_feature_34 MS8_feature_35 MS8_feature_37 MS8_feature_38 MS8_feature_39 MS8_feature_40 MS8_feature_41 MS8_feature_42 MS8_feature_43 MS8_feature_44 MS8_feature_45 MS8_feature_47 MS8_feature_52 MS8_feature_53 MS8_feature_55 MS8_feature_56 MS8_feature_57 MS8_feature_58 MS8_feature_60 MS8_feature_61 MS8_feature_62 MS8_feature_63 MS8_feature_64 MS8_feature_65 MS8_feature_66 MS8_feature_67 MS8_feature_68 MS8_feature_70 MS8_feature_75 MS8_feature_76 MS8_feature_78 MS8_feature_79 MS8_feature_80 MS8_feature_81 MS8_feature_83 MS8_feature_84 MS8_feature_85 MS8_feature_86 MS8_feature_87 MS8_feature_88 MS8_feature_89 MS8_feature_90 MS8_feature_91 MS8_feature_93 MS8_feature_98 MS8_feature_99 MS8_feature_101 MS8_feature_102 MS8_feature_103 MS8_feature_104 MS8_feature_106 MS8_feature_107 MS8_feature_108 MS8_feature_109 MS8_feature_110 MS8_feature_111 MS8_feature_116 MS8_feature_118 MS8_feature_119 MS8_feature_124 MS8_feature_126 MS8_feature_127 MS8_feature_132 MS8_feature_134 MS8_feature_135 MS8_feature_140 MS8_feature_142 MS8_feature_143 MS8_feature_144 MS8_feature_146 MS8_feature_147 MS8_feature_148 MS8_feature_149 MS8_feature_150 MS8_feature_151 MS8_feature_152 MS8_feature_153 MS8_feature_154 MS8_feature_155 MS8_feature_156 MS8_feature_157 MS8_feature_158 Peptide Proteins ModifiedPeptide Precursor PeptideGroup 390120 1 52505 2984.374317675 2984.374317675 27 1 4.39773287346864 1.05600619360352 0.185185185185185 1.11111111111111 2.81481481481481 35 4.99716807635322 0.174402239166153 0.111111111111111 0.00529100552753166 0.0804232844599971 0.0317460320614002 -1.20775864924016 0.00317460326132951 0.600000023841858 0.000922763211442017 0.00055365791724004 0.104641347657766 0.0740740740740741 0.444444444444444 2.55555555555556 14 4.56519603562033 0.358209911483048 0.037037037037037 0.00529100552753166 0.182539674970839 0.0317460320614002 -1.51853827336966 0.00264550276376583 0.5 0.00189529062167343 0.000947645310836716 0.179104955741524 0.0740740740740741 0.592592592592593 2.55555555555556 18 4.56519603562033 0.355785448846456 0.037037037037037 0.00411522636810939 0.141975305698536 0.0329218109448751 -1.51853827336966 0.00205761318405469 0.5 0.00146413766048257 0.000732068830241284 0.177892724423228 0.185185185185185 1.11111111111111 2.55555555555556 35 4.92084022385512 0.15368946169618 0.111111111111111 0.00529100552753166 0.0730158708713673 0.0317460320614002 -1.1792680662246 0.00317460326132951 0.600000023841858 0.000813171791357855 0.000487903066332671 0.0922136806819505 0.0740740740740741 0.148148148148148 0.839147686958313 0 0.0740740740740741 0.596351504325867 0 0.0740740740740741 0.596351504325867 0.0740740740740741 0.148148148148148 0.839147686958313 4 14.0633640289307 14.0633640289307 7.51776170730591 7.18997812271118 0 0 0 0 0 0 0 0 -1.05600619360352 262211 _.dummy._ 5133041 513304104 3746459 167618 -1 35142 1598.79490536 1598.79490536 13 1 5.2123265552766 0.0782731870117175 0.615384615384615 1.38461538461538 29.6153846153846 26 6.23419631596735 0.68169286154319 0.461538461538462 0.02366863993498 1.139053271367 0.0532544392805833 -1.23618708000169 0.0177514793781134 0.75 0.0161347428861119 0.012101056773891 0.511269646157393 0.153846153846154 1.07692307692308 26.9230769230769 16 4.21731303051421 0.509927000412204 0.0769230769230769 0.00961538461538462 1.68269230769231 0.0673076923076923 -3.04056367951856 0.00480769230769231 0.5 0.00490314423473273 0.00245157211736636 0.254963500206102 0.230769230769231 1.46153846153846 26.9230769230769 22 4.39931826509783 0.475203076933652 0.0769230769230769 0.0104895108021223 1.22377623044527 0.0664335672671978 -2.85876431935613 0.00349650360070742 0.333333343267441 0.00498464780869727 0.00166154926956576 0.158401030365269 0.384615384615385 1.61538461538462 26.9230769230769 26 5.167312164545 0.786869767626696 0.230769230769231 0.0147928996728017 1.0355029472938 0.0621301761040321 -2.0936832299775 0.00887573968905669 0.600000023841858 0.0116400855280625 0.00698405122664308 0.472121879336455 0.384615384615385 0.538461538461538 3.06590270996094 0.0769230769230769 0.0769230769230769 4.209068775177 0.0769230769230769 0.0769230769230769 7.7958083152771 0.230769230769231 0.230769230769231 3.89074325561523 3 14.6776256561279 8.77441215515137 4.29312372207642 5.5558910369873 0 0 0 0 0 0 0 0 0.0782731870117175 2286595 _.dummy._ 2178398 217839803 1788327 47826 1 38693 1483.782350015 1483.782350015 12 1 5.33775257770524 0.744991249755863 0.666666666666667 1.91666666666667 25.5 31 5.87355472778032 0.534035432491101 0.416666666666667 0.0215053757031759 0.822580655415853 0.0618279576301575 -1.43610371273436 0.0134408598144849 0.625 0.0114846326145291 0.00717789538408071 0.333772145306938 0.166666666666667 0.916666666666667 22.8333333333333 13 4.66878509363779 0.754139716754149 0.166666666666667 0.0128205132981141 1.75641028086344 0.0705128212769826 -2.33735102703985 0.0128205132981141 1 0.0096684582672826 0.0096684582672826 0.754139716754149 0.25 1.58333333333333 22.8333333333333 22 4.77529467454582 0.658024571737302 0.166666666666667 0.0113636367022991 1.03787875175476 0.0719696978727977 -2.23139621492402 0.00757575780153275 0.666666686534882 0.00747755217440866 0.00498503478293911 0.438683060898641 0.583333333333333 2 22.8333333333333 31 5.52196035496777 0.335201948098924 0.416666666666667 0.0188172037402789 0.736559152603149 0.0645161271095276 -1.49625222801692 0.0134408598144849 0.714285731315613 0.00630756335151585 0.00450540239393989 0.239429968636258 0.416666666666667 0.416666666666667 2.52091455459595 0.166666666666667 0.166666666666667 3.12026619911194 0.166666666666667 0.166666666666667 3.1865119934082 0.416666666666667 0.416666666666667 3.1865119934082 3 16.0624752044678 13.8793077468872 5.27713108062744 6.50826787948608 0 0 0 0 0 0 0 0 -0.744991249755863 1213451 _.dummy._ 1880410 188041003 1559331 diff --git a/mokapot/__init__.py b/mokapot/__init__.py index 69029074..7ed40520 100644 --- a/mokapot/__init__.py +++ b/mokapot/__init__.py @@ -22,5 +22,4 @@ from .parsers.pin import read_pin, read_percolator from .parsers.pepxml import read_pepxml from .parsers.fasta import read_fasta, make_decoys, digest -from .writers import to_flashlfq, to_txt -from .confidence import LinearConfidence, plot_qvalues, assign_confidence +from .confidence import Confidence, assign_confidence diff --git a/mokapot/brew.py b/mokapot/brew.py index 8e370509..1183a567 100644 --- a/mokapot/brew.py +++ b/mokapot/brew.py @@ -1,29 +1,29 @@ """ Defines a function to run the Percolator algorithm. """ - -from __future__ import annotations - -import logging import copy +import logging from operator import itemgetter +from typing import Iterable import numpy as np +import pandas as pd from joblib import Parallel, delayed from typeguard import typechecked -from .model import PercolatorModel -from . import utils -from .dataset import ( +from mokapot import utils +from mokapot.constants import ( + CHUNK_SIZE_ROWS_PREDICTION, + CHUNK_SIZE_READ_ALL_DATA, +) +from mokapot.dataset import ( LinearPsmDataset, calibrate_scores, update_labels, + OnDiskPsmDataset, ) -from .parsers.pin import parse_in_chunks -from .constants import ( - CHUNK_SIZE_ROWS_PREDICTION, - CHUNK_SIZE_READ_ALL_DATA, -) +from mokapot.model import PercolatorModel, Model +from mokapot.parsers.pin import parse_in_chunks LOGGER = logging.getLogger(__name__) @@ -31,7 +31,7 @@ # Functions ------------------------------------------------------------------- @typechecked def brew( - psms, + datasets: list[OnDiskPsmDataset], model=None, test_fdr: float = 0.01, folds: int = 3, @@ -60,7 +60,7 @@ def brew( Parameters ---------- - psms : PsmDataset object or list of PsmDataset objects + datasets : PsmDataset object or list of PsmDataset objects One or more :doc:`collections of PSMs ` objects. PSMs are aggregated across all of the collections for model training, but the confidence estimates are calculated and @@ -91,29 +91,16 @@ def brew( Returns ------- - confidence : Confidence - An object or a list of objects containing the - :doc:`confidence estimates ` at various levels - (i.e. PSMs, peptides) when assessed using the learned score. - If a list, they will be in the same order as provided in the - `psms` parameter. models : list[Model] The learned :py:class:`~mokapot.model.Model` objects, one for each fold. - scores : list[float] + scores : list[np.array[float]] The scores - descs : list[bool] - Whether the order is descending or ascending """ rng = np.random.default_rng(rng) if model is None: model = PercolatorModel() - try: - iter(psms) - except TypeError: - psms = [psms] - try: model.estimator model.rng = rng @@ -121,52 +108,46 @@ def brew( pass # Check that all of the datasets have the same features: - feat_set = set(psms[0].feature_columns) - if not all([set(_psms.feature_columns) == feat_set for _psms in psms]): + feat_set = set(datasets[0].feature_columns) + if not all([ + set(dataset.feature_columns) == feat_set for dataset in datasets + ]): raise ValueError("All collections of PSMs must use the same features.") - data_size = [len(_psms.spectra_dataframe) for _psms in psms] + data_size = [len(datasets.spectra_dataframe) for datasets in datasets] if sum(data_size) > 1: LOGGER.info("Found %i total PSMs.", sum(data_size)) - num_targets = sum( - [ - (_psms.spectra_dataframe[_psms.target_column]).sum() - for _psms in psms - ] - ) - num_decoys = sum( - [ - (~_psms.spectra_dataframe[_psms.target_column]).sum() - for _psms in psms - ] - ) + num_targets = sum([ + (dataset.spectra_dataframe[dataset.target_column]).sum() + for dataset in datasets + ]) + num_decoys = sum([ + (~dataset.spectra_dataframe[dataset.target_column]).sum() + for dataset in datasets + ]) LOGGER.info( " - %i target PSMs and %i decoy PSMs detected.", num_targets, num_decoys, ) LOGGER.info("Splitting PSMs into %i folds...", folds) - test_folds_idx = [_psms._split(folds, rng) for _psms in psms] + test_folds_idx = [dataset._split(folds, rng) for dataset in datasets] - # If trained models are provided, use the them as-is. + # If trained models are provided, use them as-is. try: fitted = [[m, False] for m in model if m.is_trained] - # todo: assertions with exceptions for control flow and checking user - # input is atrocious - assert len(fitted) == len(model) # Test that all models are fitted. - assert len(model) == folds - except AssertionError as orig_err: + if len(model) != folds: - err = ValueError( + raise ValueError( f"The number of trained models ({len(model)}) " f"must match the number of folds ({folds})." ) - else: - err = RuntimeError( + + if len(fitted) != len(model): # Test that all models are fitted. + raise RuntimeError( "One or more of the provided models was not previously trained" ) - raise err from orig_err except TypeError: train_sets = list( make_train_sets( @@ -177,14 +158,14 @@ def brew( ) ) train_psms = parse_in_chunks( - psms=psms, + datasets=datasets, train_idx=train_sets, chunk_size=CHUNK_SIZE_READ_ALL_DATA, max_workers=max_workers, ) del train_sets fitted = Parallel(n_jobs=max_workers, require="sharedmem")( - delayed(_fit_model)(d, psms, copy.deepcopy(model), f) + delayed(_fit_model)(d, datasets, copy.deepcopy(model), f) for f, d in enumerate(train_psms) ) @@ -198,15 +179,15 @@ def brew( # If we reset, just use the original model on all the folds: if reset: scores = [ - _psms.calibrate_scores( + dataset.calibrate_scores( _predict_with_ensemble( - psms=_psms, + dataset=dataset, models=[model], max_workers=max_workers, ), test_fdr, ) - for _psms in psms + for dataset in datasets ] # If we don't reset, assign scores to each fold: @@ -214,11 +195,11 @@ def brew( if ensemble: scores = [ _predict_with_ensemble( - psms=_psms, + dataset=dataset, models=models, max_workers=max_workers, ) - for _psms in psms + for dataset in datasets ] else: # generate model index for each psm in all folds @@ -241,14 +222,14 @@ def brew( scores = list( _predict( models_idx=model_to_psm_idx, - psms=psms, + datasets=datasets, models=models, test_fdr=test_fdr, max_workers=max_workers, ) ) - # If model training has failed else: + logging.info("Model training failed. Setting scores to zero.") scores = [np.zeros(x) for x in data_size] # Find which is best: the learned model, the best feature, or # a pretrained model. @@ -262,12 +243,12 @@ def brew( preds = [ update_labels( - _psms.filename, - s, - _psms.target_column, + dataset.reader, + score, + dataset.target_column, test_fdr, ) - for _psms, s in zip(psms, scores) + for dataset, score in zip(datasets, scores) ] pred_total = sum([(pred == 1).sum() for pred in preds]) @@ -276,17 +257,16 @@ def brew( if feat_total > pred_total: using_best_feat = True feat, _, desc = best_feats[best_feat_idx] - descs = [desc] * len(psms) + descs = [desc] * len(datasets) scores = [ - _psms.read_data( + dataset.read_data( columns=[feat], - ).values - for _psms in psms + ).values[:, 0] + for dataset in datasets ] - else: using_best_feat = False - descs = [True] * len(psms) + descs = [True] * len(datasets) if using_best_feat: LOGGER.warning( @@ -301,7 +281,14 @@ def brew( "using the original model." ) - return psms, models, scores, descs + # Reverse all scores for which desc is False (this way, we don't have to + # return `descs` from this function + for idx, desc in enumerate(descs): + if not desc: + scores[idx] = -scores[idx] + descs[idx] = not descs[idx] + + return models, scores # Utility Functions ----------------------------------------------------------- @@ -361,21 +348,26 @@ def make_train_sets(test_idx, subset_max_train, data_size, rng): yield train_idx -def _create_psms(psms, data, enforce_checks=True): - utils.convert_targets_column(data=data, target_column=psms.target_column) +@typechecked +def _create_linear_dataset( + dataset: OnDiskPsmDataset, psms: pd.DataFrame, enforce_checks: bool = True +): + utils.convert_targets_column( + data=psms, target_column=dataset.target_column + ) return LinearPsmDataset( - psms=data, - target_column=psms.target_column, - spectrum_columns=psms.spectrum_columns, - peptide_column=psms.peptide_column, - protein_column=psms.protein_column, - feature_columns=psms.feature_columns, - filename_column=psms.filename_column, - scan_column=psms.scan_column, - calcmass_column=psms.calcmass_column, - expmass_column=psms.expmass_column, - rt_column=psms.rt_column, - charge_column=psms.charge_column, + psms=psms, + target_column=dataset.target_column, + spectrum_columns=dataset.spectrum_columns, + peptide_column=dataset.peptide_column, + protein_column=dataset.protein_column, + feature_columns=dataset.feature_columns, + filename_column=dataset.filename_column, + scan_column=dataset.scan_column, + calcmass_column=dataset.calcmass_column, + expmass_column=dataset.expmass_column, + rt_column=dataset.rt_column, + charge_column=dataset.charge_column, copy_data=False, enforce_checks=enforce_checks, ) @@ -387,17 +379,27 @@ def get_index_values(df, col_name, val, orig_idx): return df -def predict_fold(model, fold, psms, scores): - scores[fold].append(model.predict(psms)) +@typechecked +def predict_fold( + model: Model, fold: int, dataset: LinearPsmDataset, scores: list +): + scores[fold].append(model.predict(dataset)) -def _predict(models_idx, psms, models, test_fdr, max_workers): +@typechecked +def _predict( + models_idx: list, + datasets: Iterable[OnDiskPsmDataset], + models: Iterable[Model], + test_fdr: float, + max_workers: int, +): """ Return the new scores for the dataset Parameters ---------- - psms : Dict + datasets : Dict Contains all required info about the dataset to rescore models_idx : list of numpy.ndarray The indicies of the models to predict with @@ -412,44 +414,44 @@ def _predict(models_idx, psms, models, test_fdr, max_workers): numpy.ndarray A :py:class:`numpy.ndarray` containing the new scores. """ - for _psms, mod_idx in zip(psms, models_idx): + for dataset, mod_idx in zip(datasets, models_idx): scores = [] n_folds = len(models) fold_scores = [[] for _ in range(n_folds)] targets = [[] for _ in range(n_folds)] orig_idx = [[] for _ in range(n_folds)] - file_iterator = _psms.read_data( - columns=_psms.columns, chunk_size=CHUNK_SIZE_ROWS_PREDICTION + file_iterator = dataset.read_data( + columns=dataset.columns, chunk_size=CHUNK_SIZE_ROWS_PREDICTION ) model_test_idx = utils.create_chunks( data=mod_idx, chunk_size=CHUNK_SIZE_ROWS_PREDICTION ) - for i, psms_slice in enumerate(file_iterator): - psms_slice["fold"] = model_test_idx.pop(0) - psms_slice = [ - get_index_values(psms_slice, "fold", i, orig_idx) + for i, psms_chunk in enumerate(file_iterator): + psms_chunk["fold"] = model_test_idx.pop(0) + psms_slices = [ + get_index_values(psms_chunk, "fold", i, orig_idx) for i in range(n_folds) ] - psms_slice = [ - _create_psms(_psms, psm_slice, enforce_checks=False) - for psm_slice in psms_slice - ] - [ - targets[i].append(psm_slice.targets) - for i, psm_slice in enumerate(psms_slice) + dataset_slices = [ + _create_linear_dataset( + dataset, psm_slice, enforce_checks=False + ) + for psm_slice in psms_slices ] + for i, dataset_slice in enumerate(dataset_slices): + targets[i].append(dataset_slice.targets) Parallel(n_jobs=max_workers, require="sharedmem")( delayed(predict_fold)( model=models[mod_idx], fold=mod_idx, - psms=_psms, + dataset=dataset_slice, scores=fold_scores, ) - for mod_idx, _psms in enumerate(psms_slice) + for mod_idx, dataset_slice in enumerate(dataset_slices) ) - del psms_slice + del psms_slices, dataset_slices del file_iterator del model_test_idx for mod in models: @@ -476,27 +478,32 @@ def _predict(models_idx, psms, models, test_fdr, max_workers): yield np.concatenate(scores)[orig_idx] -def _predict_with_ensemble(psms, models, max_workers): +@typechecked +def _predict_with_ensemble( + dataset: OnDiskPsmDataset, models: Iterable[Model], max_workers +): """ Return the new scores for the dataset using ensemble of all trained models Parameters ---------- max_workers - psms : Dict + dataset : Dict Contains all required info about the dataset to rescore models : list of Model The models for each dataset and whether it was reset or not. """ scores = [[] for _ in range(len(models))] - file_iterator = psms.read_data( - columns=psms.columns, chunk_size=CHUNK_SIZE_ROWS_PREDICTION + file_iterator = dataset.read_data( + columns=dataset.columns, chunk_size=CHUNK_SIZE_ROWS_PREDICTION ) - for data in file_iterator: - data = _create_psms(psms, data, enforce_checks=False) + for psms_chunk in file_iterator: + linear_dataset = _create_linear_dataset( + dataset, psms_chunk, enforce_checks=False + ) fold_scores = Parallel(n_jobs=max_workers, require="sharedmem")( - delayed(mod.predict)(psms=data) for mod in models + delayed(model.predict)(dataset=linear_dataset) for model in models ) [score.append(fs) for score, fs in zip(scores, fold_scores)] del fold_scores @@ -528,11 +535,12 @@ def _fit_model(train_set, psms, model, fold): LOGGER.debug("") LOGGER.debug("=== Analyzing Fold %i ===", fold + 1) reset = False - train_set = _create_psms(psms[0], train_set) + train_set = _create_linear_dataset(psms[0], train_set) try: model.fit(train_set) except RuntimeError as msg: if str(msg) != "Model performs worse after training.": + LOGGER.info(f"Fold {fold + 1}: {msg}") raise if model.is_trained: diff --git a/mokapot/brew_rollup.py b/mokapot/brew_rollup.py index e4bc0b4f..e59f7ebe 100644 --- a/mokapot/brew_rollup.py +++ b/mokapot/brew_rollup.py @@ -2,43 +2,33 @@ This is the command line interface for mokapot """ -from __future__ import annotations - -import datetime +import argparse import logging import sys -import time -import argparse from argparse import _ArgumentGroup as ArgumentGroup from pathlib import Path import numpy as np -import pyarrow as pa -from typeguard import typechecked -from mokapot.streaming import ( - MergedTabularDataReader, - ComputedTabularDataReader, -) -from mokapot import __version__, qvalues -from mokapot.tabular_data import ( - TabularDataWriter, - TabularDataReader, - auto_finalize, - remove_columns, - TableType, +from mokapot import __version__ +from mokapot.cli_helper import ( + setup_logging, + output_start_message, + output_end_message, ) -from mokapot.peps import peps_from_scores +from mokapot.rollup import do_rollup def parse_arguments(main_args): # Get command line arguments """The parser""" - # Todo: we should update this + # todo: we should update this copyright notice asap desc = ( f"mokapot version {__version__}.\n" - "Written by William E. Fondrie (wfondrie@talus.bio) while in the \n" + "Originally written by William E. Fondrie (wfondrie@talus.bio) while in the \n" "Department of Genome Sciences at the University of Washington.\n\n" + "Extended by Samia Ben Fredj, Elmar Zander, Vishal Sukumar and \n" + "Siegfried Gessulat while at MSAID. \n\n" "Official code website: https://github.com/wfondrie/mokapot\n\n" "More documentation and examples: https://mokapot.readthedocs.io" ) @@ -84,7 +74,7 @@ def add_main_options(parser: ArgumentGroup) -> None: "-s", "--src_dir", type=Path, - default="./", + default=Path("."), help=("The directory in which to look for the files to rollup."), ) @@ -94,7 +84,7 @@ def add_output_options(parser: ArgumentGroup) -> None: "-d", "--dest_dir", type=Path, - default="./", + default=Path("."), help=( "The directory in which to write the result files. Defaults to " "the current working directory" @@ -128,6 +118,12 @@ def add_confidence_options(parser: ArgumentGroup) -> None: "the default mokapot algorithm." ), ) + parser.add_argument( + "--stream_confidence", + default=False, + action="store_true", + help=("Specify whether confidence assignment shall be streamed."), + ) def add_misc_options(parser: ArgumentGroup) -> None: @@ -162,298 +158,6 @@ def add_misc_options(parser: ArgumentGroup) -> None: ) -def setup_logging(config): - # Setup logging - verbosity_dict = { - 0: logging.ERROR, - 1: logging.WARNING, - 2: logging.INFO, - 3: logging.DEBUG, - } - logging.basicConfig( - format=("[{levelname}] {message}"), - style="{", - level=verbosity_dict[config.verbosity], - ) - logging.captureWarnings(True) - - -def output_start_message(prog_name, config): - # todo: need to update that too - start_time = time.time() - logging.info(f"{prog_name} version {__version__}") - logging.info("Written by William E. Fondrie (wfondrie@uw.edu) in the") - logging.info( - "Department of Genome Sciences at the University of Washington." - ) - logging.info("Command issued:") - logging.info(" %s", " ".join(sys.argv)) - logging.info("") - logging.info("Starting Analysis") - logging.info("=================") - return start_time - - -def output_end_message(prog_name, config, start_time): - total_time = round(time.time() - start_time) - total_time = str(datetime.timedelta(seconds=total_time)) - - logging.info("") - logging.info("=== DONE! ===") - logging.info(f"{prog_name} analysis completed in {total_time}") - - -DEFAULT_PARENT_LEVELS = { - "precursor": "psm", - "modified_peptide": "precursor", - "peptide": "modified_peptide", - "peptide_group": "precursor", # due to "unknown nature" of peptide groups -} - - -@typechecked -def compute_rollup_levels( - base_level: str, parent_levels: dict[str, str] | None = None -) -> list[str]: - if parent_levels is None: - parent_levels = DEFAULT_PARENT_LEVELS - levels = [base_level] - changed = True - while changed: - changed = False - for child, parent in parent_levels.items(): - if (parent in levels) and (child not in levels): - levels.append(child) - changed = True - return levels - - -STANDARD_COLUMN_NAME_MAP = { - "SpecId": "psm_id", - "PSMId": "psm_id", - "Precursor": "precursor", - "pcm": "precursor", - "PCM": "precursor", - "Peptide": "peptide", - "PeptideGroup": "peptide_group", - "peptidegroup": "peptide_group", - "ModifiedPeptide": "modified_peptide", - "modifiedpeptide": "modified_peptide", - "q-value": "q_value", -} - - -def make_timer(): - t0 = time.time() - - def elapsed(): - nonlocal t0 - t1 = time.time() - dt, t0 = t1 - t0, t1 - return dt - - return elapsed - - -@typechecked -def do_rollup(config): - base_level: str = config.level - src_dir: Path = config.src_dir - dest_dir: Path = config.dest_dir - file_root: str = config.file_root + "." - - # Determine input files - if len(list(src_dir.glob(f"*.{base_level}s.parquet"))) > 0: - if len(list(src_dir.glob(f"*.{base_level}s"))) > 0: - raise RuntimeError( - "Only input files of either type CSV or type Parquet should" - f" exist in '{src_dir}', but both types were found." - ) - suffix = ".parquet" - dtype = pa.bool_() - else: - suffix = "" - dtype = np.dtype("bool") - - target_files: list[Path] = sorted( - src_dir.glob(f"*.targets.{base_level}s{suffix}") - ) - decoy_files: list[Path] = sorted( - src_dir.glob(f"*.decoys.{base_level}s{suffix}") - ) - target_files = [ - file for file in target_files if not file.name.startswith(file_root) - ] - decoy_files = [ - file for file in decoy_files if not file.name.startswith(file_root) - ] - in_files: list[Path] = sorted(target_files + decoy_files) - logging.info(f"Reading files: {[str(file) for file in in_files]}") - # todo: message if no input files found - - # Configure readers (read targets/decoys and adjoin is_decoy column) - target_readers = [ - ComputedTabularDataReader( - reader=TabularDataReader.from_path( - path, column_map=STANDARD_COLUMN_NAME_MAP - ), - column="is_decoy", - dtype=dtype, - func=lambda df: np.full(len(df), False), - ) - for path in target_files - ] - decoy_readers = [ - ComputedTabularDataReader( - reader=TabularDataReader.from_path( - path, column_map=STANDARD_COLUMN_NAME_MAP - ), - column="is_decoy", - dtype=dtype, - func=lambda df: np.full(len(df), True), - ) - for path in decoy_files - ] - reader = MergedTabularDataReader( - target_readers + decoy_readers, - priority_column="score", - reader_chunk_size=10000, - ) - - # Determine out levels - levels = compute_rollup_levels(base_level, DEFAULT_PARENT_LEVELS) - levels_not_found = [ - level for level in levels if level not in reader.get_column_names() - ] - levels = [level for level in levels if level in reader.get_column_names()] - logging.info(f"Rolling up to levels: {levels}") - if len(levels_not_found) > 0: - logging.info( - f" (Rollup levels not found in input: {levels_not_found})" - ) - - # Determine temporary files - temp_files = { - level: dest_dir / f"{file_root}temp.{level}s{suffix}" - for level in levels - } - logging.debug( - "Using temp files: " - f"{ {level: str(file) for level, file in temp_files.items()} }" - ) - - # Determine output files - out_files = { - level: [ - dest_dir / f"{file_root}targets.{level}s{suffix}", - dest_dir / f"{file_root}decoys.{level}s{suffix}", - ] - for level in levels - } - - lvls = {level: list(map(str, files)) for level, files in out_files.items()} - logging.debug("Writing to files: " f"{lvls}") - - # Determine columns for output files and intermediate files - column_names = reader.get_column_names() - column_types = reader.get_column_types() - - temp_column_names, temp_column_types = remove_columns( - column_names, column_types, ["q_value", "posterior_error_prob"] - ) - - # Configure temp writers - merge_row_type = TableType.Dicts - - temp_buffer_size = 1000 - - temp_writers = { - level: TabularDataWriter.from_suffix( - temp_files[level], - columns=temp_column_names, - column_types=temp_column_types, - buffer_size=temp_buffer_size, - buffer_type=merge_row_type, - ) - for level in levels - } - - # todo: We need an option to write parquet or sql for example (also, the - # output file type could depend on the input file type) - - # Write temporary files which contain only the best scoring entity - # of a given level - logging.debug( - "Writing temp files: %s", [str(file) for file in temp_files.values()] - ) - - timer = make_timer() - with auto_finalize(temp_writers.values()): - count = 0 - seen_entities: dict[str, set] = {level: set() for level in levels} - for line in reader.get_row_iterator( - temp_column_names, row_type=merge_row_type - ): - count += 1 - if count % 10000 == 0: - logging.debug( - f" Processed {count} lines ({timer():.2f} seconds)" - ) - - for level in levels: - seen = seen_entities[level] - id_col = level - if merge_row_type == TableType.DataFrame: - id = line.loc[0, id_col] - else: - id = line[id_col] - if id not in seen: - seen.add(id) - temp_writers[level].append_data(line) - - logging.info(f"Read {count} PSMs") - for level in levels: - seen = seen_entities[level] - logging.info( - f"Rollup level {level}: found {len(seen)} unique entities" - ) - - # Configure temp readers and output writers - buffer_size = 1000 - output_columns, output_types = remove_columns( - column_names, column_types, ["is_decoy"] - ) - output_options = dict( - columns=output_columns, - column_types=output_types, - buffer_size=buffer_size, - ) - - def create_writer(path): - return TabularDataWriter.from_suffix(path, **output_options) - - for level in levels: - reader = temp_writers[level].get_associated_reader() - output_writers = list(map(create_writer, out_files[level])) - - # data = reader.read(columns=["is_decoy", "score"]) - data = reader.read() - - scores = data["score"].values - targets = ~data["is_decoy"].values - - qvals = qvalues.qvalues_from_scores( - scores, targets, config.qvalue_algorithm - ) - peps = peps_from_scores(scores, targets, config.peps_algorithm) - - data["q_value"] = qvals - data["posterior_error_prob"] = peps - - output_writers[0].write(data.loc[targets, output_columns]) - output_writers[1].write(data.loc[~targets, output_columns]) - - def main(main_args=None): """The CLI entry point""" diff --git a/mokapot/cli_helper.py b/mokapot/cli_helper.py new file mode 100644 index 00000000..fcd25e05 --- /dev/null +++ b/mokapot/cli_helper.py @@ -0,0 +1,57 @@ +import datetime +import logging +import sys +import time + +from mokapot import __version__ + + +def make_timer(): + t0 = time.time() + + def elapsed(): + nonlocal t0 + t1 = time.time() + dt, t0 = t1 - t0, t1 + return dt + + return elapsed + + +def setup_logging(config): + # Setup logging + verbosity_dict = { + 0: logging.ERROR, + 1: logging.WARNING, + 2: logging.INFO, + 3: logging.DEBUG, + } + logging.basicConfig( + format=("[{levelname}] {message}"), + style="{", + level=verbosity_dict[config.verbosity], + ) + logging.captureWarnings(True) + + +def output_start_message(prog_name, config): + timer = make_timer() + logging.info(f"{prog_name} version {__version__}") + logging.info("Written by William E. Fondrie (wfondrie@uw.edu) in the") + logging.info( + "Department of Genome Sciences at the University of Washington." + ) + logging.info("Command issued:") + logging.info(" %s", " ".join(sys.argv)) + logging.info("") + logging.info("Starting Analysis") + logging.info("=================") + return timer + + +def output_end_message(prog_name, config, timer): + total_time = str(datetime.timedelta(seconds=timer())) + + logging.info("") + logging.info("=== DONE! ===") + logging.info(f"{prog_name} analysis completed in {total_time}") diff --git a/mokapot/column_defs.py b/mokapot/column_defs.py new file mode 100644 index 00000000..de057f64 --- /dev/null +++ b/mokapot/column_defs.py @@ -0,0 +1,18 @@ +STANDARD_COLUMN_NAME_MAP = { + "SpecId": "psm_id", + "PSMId": "psm_id", + "Precursor": "precursor", + "pcm": "precursor", + "PCM": "precursor", + "Peptide": "peptide", + "PeptideGroup": "peptide_group", + "peptidegroup": "peptide_group", + "ModifiedPeptide": "modified_peptide", + "modifiedpeptide": "modified_peptide", + # "q-value": "q_value", + "q-value": "q-value", +} + + +def get_standard_column_name(name): + return STANDARD_COLUMN_NAME_MAP.get(name, name) diff --git a/mokapot/confidence.py b/mokapot/confidence.py index 7fc9f848..b3af4229 100644 --- a/mokapot/confidence.py +++ b/mokapot/confidence.py @@ -8,521 +8,251 @@ The following classes store the confidence estimates for a dataset based on the provided score. They provide utilities to access, save, and plot these estimates for the various relevant levels (i.e. PSMs, peptides, and proteins). -The :py:func:`LinearConfidence` class is appropriate for most data-dependent +The :py:func:`Confidence` class is appropriate for most data-dependent acquisition proteomics datasets. We recommend using the :py:func:`~mokapot.brew()` function or the -:py:meth:`~mokapot.LinearPsmDataset.assign_confidence()` method to obtain these +:py:meth:`~mokapot.PsmDataset.assign_confidence()` method to obtain these confidence estimates, rather than initializing the classes below directly. """ - -from __future__ import annotations - import logging -from pathlib import Path from contextlib import contextmanager +from pathlib import Path +from typing import Sequence, Iterator import numpy as np import pandas as pd -import matplotlib.pyplot as plt from joblib import Parallel, delayed from typeguard import typechecked -import os -from . import qvalues -from .peps import peps_from_scores -from .utils import ( - create_chunks, - groupby_max, - convert_targets_column, - merge_sort, - get_dataframe_from_records, +from mokapot.column_defs import get_standard_column_name +from mokapot.constants import CONFIDENCE_CHUNK_SIZE +from mokapot.dataset import OnDiskPsmDataset +from mokapot.peps import ( + peps_from_scores, + TDHistData, + peps_func_from_hist_nnls, + PepsConvergenceError, ) -from .dataset import OnDiskPsmDataset -from .picked_protein import picked_protein -from .writers import to_flashlfq -from .tabular_data import ( - TabularDataWriter, - TabularDataReader, - get_score_column_type, +from mokapot.picked_protein import picked_protein +from mokapot.qvalues import qvalues_from_scores, qvalues_func_from_hist +from mokapot.statistics import OnlineStatistics, HistData +from mokapot.tabular_data import ( + BufferType, + ColumnMappedReader, + ComputedTabularDataReader, + ConfidenceSqliteWriter, + join_readers, + MergedTabularDataReader, +) +from mokapot.tabular_data import TabularDataReader, TabularDataWriter +from mokapot.tabular_data.target_decoy_writer import TargetDecoyWriter +from mokapot.utils import ( + convert_targets_column, ) -from .confidence_writer import write_confidences -from .constants import CONFIDENCE_CHUNK_SIZE LOGGER = logging.getLogger(__name__) # Classes --------------------------------------------------------------------- +@typechecked class Confidence(object): - """Estimate and store the statistical confidence for a collection of PSMs. - - :meta private: - """ - - _level_labs = { - "psms": "PSMs", - "peptides": "Peptides", - "proteins": "Proteins", - "peptide_pairs": "Peptide Pairs", - } - - def __init__(self, psms, proteins=None, rng=0): - """Initialize a PsmConfidence object.""" - self._score_column = "score" - self._target_column = psms.target_column - self._protein_column = "proteinIds" - self._rng = rng - self._metadata_column = psms.metadata_columns - - self.scores = None - self.targets = None - self.qvals = None - self.peps = None - - self._proteins = proteins - - # This attribute holds the results as DataFrames: - self.confidence_estimates = {} - self.decoy_confidence_estimates = {} - - def __getattr__(self, attr): - if attr.startswith("__"): - return super().__getattr__(attr) - - try: - return self.confidence_estimates[attr] - except KeyError: - raise AttributeError - - @property - def levels(self): - """ - The available levels for confidence estimates. - """ - return list(self.confidence_estimates.keys()) + """Estimate the statistical confidence for a collection of PSMs.""" - def write_to_disk( + def __init__( self, - data_path, - columns, - level, - decoys, - out_paths, - sqlite_path=None, + dataset: OnDiskPsmDataset, + levels: list[str], + level_paths: dict[str, Path], + out_writers: dict[str, Sequence[TabularDataWriter]], + eval_fdr: float = 0.01, + write_decoys: bool = False, + do_rollup: bool = True, + proteins=None, + peps_error: bool = False, + rng=0, + peps_algorithm: str = "qvality", + qvalue_algorithm: str = "tdc", + stream_confidence: bool = False, + score_stats=None, ): - """Save confidence estimates to delimited text files. - Parameters - ---------- - data_path : Path - File of unique psms or peptides. - columns : List - columns that will be used - level : str - the level at which confidence estimation was performed - decoys : bool, optional - Save decoys confidence estimates as well? - out_paths : List(Path) - The output files where the results will be written - - Returns - ------- - list of str - The paths to the saved files. + """Initialize a Confidence object. - """ - # The columns here are usually the metadata_columns from - # `confidence.assign_confidence` - # which are usually: - # ['PSMId', 'Label', 'peptide', 'proteinIds', 'score'] - # Since, those are exactly the columns that are written there to the - # csv files, it's not exactly clear, why they are passed along - # here anyway (but let's assert that here) - reader = TabularDataReader.from_path(data_path) - assert reader.get_column_names() == columns - - in_columns = [i for i in columns if i != self._target_column] - chunked_data_iterator = reader.get_chunked_data_iterator( - CONFIDENCE_CHUNK_SIZE, in_columns - ) - - # Note: the out_columns need to match those in assign_confidence - # (out_files) - qvalue_column = "q_value" - pep_column = "posterior_error_prob" - out_columns = in_columns + [qvalue_column, pep_column] - protein_column = self._protein_column - if level != "proteins" and protein_column is not None: - # Move the "proteinIds" column to the back for dubious reasons - # todo: rather than this fiddeling here, we should have a column - # mapping that does this - out_columns.remove(protein_column) - out_columns.append(protein_column) - - def chunked(list): - return create_chunks(list, chunk_size=CONFIDENCE_CHUNK_SIZE) - - # Replacing csv target and decoys results path with sqlite db path - if sqlite_path: - out_paths = [sqlite_path] - - write_confidences( - chunked_data_iterator, - chunked(self.qvals), - chunked(self.peps), - chunked(self.targets), - out_paths, - decoys, - level, - out_columns, - ) - return out_paths + Assign confidence estimates to a set of PSMs - def _perform_tdc(self, psms, psm_columns): - """Perform target-decoy competition. + Estimate q-values and posterior error probabilities (PEPs) for PSMs and + peptides when ranked by the provided scores. Parameters ---------- - psms : Dataframe - - Dataframe of percolator with metadata columns - [SpecId, Label, ScanNr, ExpMass, Peptide, score, Proteins]. - - psm_columns : str or list of str - The columns that define a PSM. - """ - psm_idx = groupby_max( - psms, psm_columns, self._score_column, rng=self._rng - ) - return psms.loc[psm_idx, :] - - def plot_qvalues(self, level="psms", threshold=0.1, ax=None, **kwargs): - """Plot the cumulative number of discoveries over range of q-values. - - The available levels can be found using - :py:meth:`~mokapot.confidence.Confidence.levels` attribute. - - Parameters - ---------- - level : str, optional - The level of q-values to report. - threshold : float, optional - Indicates the maximum q-value to plot. - ax : matplotlib.pyplot.Axes, optional - The matplotlib Axes on which to plot. If `None` the current - Axes instance is used. - **kwargs : dict, optional - Arguments passed to :py:func:`matplotlib.pyplot.plot`. - - Returns - ------- - matplotlib.pyplot.Axes - An :py:class:`matplotlib.axes.Axes` with the cumulative - number of accepted target PSMs or peptides. + dataset : OnDiskPsmDataset + An OnDiskPsmDataset. + rng : int or np.random.Generator, optional + A seed or generator used for cross-validation split creation and to + break ties, or ``None`` to use the default random number generator + state. + levels : list[str] + Levels at which confidence estimation was performed + level_paths : list[Path] + Files with unique psms and unique peptides. + out_paths : list[list[Path]] + The output files where the results will be written + eval_fdr : float + The FDR threshold at which to report performance. This parameter + has no affect on the analysis itself, only logging messages. + write_decoys : bool + Save decoys confidence estimates as well? """ - qvals = self.confidence_estimates[level]["mokapot q-value"] - if qvals is None: - raise ValueError(f"{level}-level estimates are unavailable.") - - ax = plot_qvalues(qvals, threshold=threshold, ax=ax, **kwargs) - ax.set_xlabel("q-value") - ax.set_ylabel(f"Accepted {self._level_labs[level]}") - - return ax - - -@typechecked -class LinearConfidence(Confidence): - """Assign confidence estimates to a set of PSMs - - Estimate q-values and posterior error probabilities (PEPs) for PSMs and - peptides when ranked by the provided scores. - Parameters - ---------- - psms : OnDiskPsmDataset - A collection of PSMs. - rng : int or np.random.Generator, optional - A seed or generator used for cross-validation split creation and to - break ties, or ``None`` to use the default random number generator - state. - levels : list[str] - Levels at which confidence estimation was performed - level_paths : list[Path] - Files with unique psms and unique peptides. - out_paths : list[list[Path]] - The output files where the results will be written - desc : bool - Are higher scores better? - eval_fdr : float - The FDR threshold at which to report performance. This parameter - has no affect on the analysis itself, only logging messages. - sep : str, optional - The delimiter to use. - decoys : bool, optional - Save decoys confidence estimates as well? - """ - - def __init__( - self, - psms, - level_paths: list[Path], - levels: list[str], - out_paths: list[list[Path]], - desc=True, - eval_fdr=0.01, - decoys=None, - deduplication=True, - do_rollup=True, - proteins=None, - peps_error=False, - sep="\t", - rng=0, - peps_algorithm="qvality", - qvalue_algorithm="tdc", - sqlite_path=None, - ): - """Initialize a a LinearPsmConfidence object""" - super().__init__(psms, proteins, rng) - self._target_column = psms.target_column - self._peptide_column = "peptide" + self._score_column = "score" + self._target_column = dataset.target_column self._protein_column = "proteinIds" + self._metadata_column = dataset.metadata_columns + self._peptide_column = "peptide" + self._eval_fdr = eval_fdr - self.deduplication = deduplication self.do_rollup = do_rollup + if proteins: + self.write_protein_level_data(level_paths, proteins, rng) + self._assign_confidence( - level_paths=level_paths, levels=levels, - out_paths=out_paths, - desc=desc, - decoys=decoys, - sep=sep, + level_path_map=level_paths, + out_writers_map=out_writers, + write_decoys=write_decoys, peps_error=peps_error, peps_algorithm=peps_algorithm, qvalue_algorithm=qvalue_algorithm, - sqlite_path=sqlite_path, - ) - - self.accepted = {} - for level in self.levels: - self.accepted[level] = self._num_accepted(level) - - def __repr__(self): - """How to print the class""" - base = ( - "A mokapot.confidence.LinearConfidence object:\n" - f"\t- PSMs at q<={self._eval_fdr:g}: {self.accepted['psms']}\n" - f"\t- Peptides at q<={self._eval_fdr:g}: " - f"{self.accepted['peptides']}\n" + stream_confidence=stream_confidence, + score_stats=score_stats, + eval_fdr=eval_fdr, ) - if self._proteins: - base += ( - f"\t- Protein groups at q<={self._eval_fdr:g}: " - f"{self.accepted['proteins']}\n" - ) - - return base - - def _num_accepted(self, level): - """Calculate the number of accepted discoveries""" - disc = self.confidence_estimates[level] - if disc is not None: - return (disc["q-value"] <= self._eval_fdr).sum() - else: - return None - def _assign_confidence( self, - level_paths, - levels, - out_paths, - desc=True, - decoys=False, - peps_error=False, - sep="\t", - peps_algorithm="qvality", - qvalue_algorithm="tdc", - sqlite_path=None, + levels: list[str], + level_path_map: dict[str, Path], + out_writers_map: dict[str, Sequence[TabularDataWriter]], + write_decoys: bool = False, + peps_error: bool = False, + peps_algorithm: str = "qvality", + qvalue_algorithm: str = "tdc", + stream_confidence: bool = False, + score_stats=None, + eval_fdr: float = 0.01, ): """ Assign confidence to PSMs and peptides. Parameters ---------- - level_paths : List(Path) + level_path_map : List(Path) Files with unique psms and unique peptides. levels : List(str) the levels at which confidence estimation was performed out_paths : List(Path) The output files where the results will be written - desc : bool - Are higher scores better? - sep : str, optional - The delimiter to use. - decoys : bool, optional + write_decoys : bool, optional Save decoys confidence estimates as well? """ - - if self._proteins: - data = TabularDataReader.from_path(level_paths[1]).read() - convert_targets_column( - data=data, target_column=self._target_column - ) - proteins = picked_protein( - data, - self._target_column, - self._peptide_column, - self._score_column, - self._proteins, - self._rng, - ) - proteins = proteins.sort_values( - by=self._score_column, ascending=False - ).reset_index(drop=True) - assert levels[-1] == "proteins" - assert len(levels) == len(level_paths) - proteins_path = level_paths[-1] - proteins.to_csv(proteins_path, index=False, sep=sep) - out_paths += [ - _psms.with_suffix(".proteins") for _psms in out_paths[0] - ] - LOGGER.info("\t- Found %i unique protein groups.", len(proteins)) - - for level, data_path, out_path in zip(levels, level_paths, out_paths): - data = TabularDataReader.from_path(data_path).read() - if self._target_column: - data = convert_targets_column(data, self._target_column) - data_columns = list(data.columns) - self.scores = data.loc[:, self._score_column].values - self.targets = data.loc[:, self._target_column].astype(bool).values - del data - if all(self.targets): - LOGGER.warning( - "No decoy PSMs remain for confidence estimation. " - "Confidence estimates may be unreliable." + if stream_confidence: + if score_stats is None: + raise ValueError( + "score stats must be provided for streamed confidence" ) - # Estimate q-values and assign to dataframe - LOGGER.info( - "Assigning q-values to %s (using %s algorithm) ...", - level, - qvalue_algorithm, - ) - self.qvals = qvalues.qvalues_from_scores( - self.scores, self.targets, qvalue_algorithm + for level in levels: + level_path = level_path_map[level] + out_writers = out_writers_map[level] + + reader = TabularDataReader.from_path(level_path) + reader = ComputedTabularDataReader( + reader, + "is_decoy", + np.dtype("bool"), + lambda df: ~df[self._target_column].values, ) - # Set scores to be the correct sign again: - self.scores = self.scores * (desc * 2 - 1) - # Logging update on q-values - LOGGER.info( - "\t- Found %i %s with q<=%g", - (self.qvals[self.targets] <= self._eval_fdr).sum(), - level, - self._eval_fdr, + writer = TargetDecoyWriter( + out_writers, write_decoys, decoy_column="is_decoy" ) - # Calculate PEPs - LOGGER.info( - "Assigning PEPs to %s (using %s algorithm) ...", - level, + compute_and_write_confidence( + reader, + writer, + qvalue_algorithm, peps_algorithm, + stream_confidence, + score_stats, + peps_error, + level, + eval_fdr, ) - try: - self.peps = peps_from_scores( - self.scores, self.targets, peps_algorithm - ) - except SystemExit as msg: - if "no decoy hits available for PEP calculation" in str(msg): - self.peps = 0 - else: - raise - if peps_error and all(self.peps == 1): - raise ValueError("PEP values are all equal to 1.") - - logging.info(f"Writing {level} results...") - self.write_to_disk( - data_path, - data_columns, - level.lower(), - decoys, - out_path, - sqlite_path, - ) - if sqlite_path: - [os.unlink(path) for path in out_path] - os.unlink(data_path) - - def to_flashlfq(self, out_file="mokapot.flashlfq.txt"): - """Save confidenct peptides for quantification with FlashLFQ. - - `FlashLFQ `_ is an - open-source tool for label-free quantification. For mokapot to save - results in a compatible format, a few extra columns are required to - be present, which specify the MS data file name, the theoretical - peptide monoisotopic mass, the retention time, and the charge for each - PSM. If these are not present, saving to the FlashLFQ format is - disabled. - - Note that protein grouping in the FlashLFQ results will be more - accurate if proteins were added for analysis with mokapot. - - Parameters - ---------- - out_file : str, optional - The output file to write. - - Returns - ------- - str - The path to the saved file. - - """ - return to_flashlfq(self, out_file) + # todo: discuss: This should probably not be done here, but rather + # in the calling code, that intializes the writers + for writer in out_writers: + writer.finalize() + + level_path.unlink(missing_ok=True) + + def write_protein_level_data(self, level_paths, proteins, rng): + psms = TabularDataReader.from_path(level_paths["psms"]).read() + proteins = picked_protein( + psms, + self._target_column, + self._peptide_column, + self._score_column, + proteins, + rng, + ) + proteins = proteins.sort_values( + by=self._score_column, ascending=False + ).reset_index(drop=True) + protein_writer = TabularDataWriter.from_suffix( + file_name=level_paths["proteins"], + columns=proteins.columns.tolist(), + column_types=proteins.dtypes.tolist(), + ) + protein_writer.write(proteins) + LOGGER.info("\t- Found %i unique protein groups.", len(proteins)) # Functions ------------------------------------------------------------------- @typechecked def assign_confidence( - psms: list[OnDiskPsmDataset], - max_workers, - scores=None, - descs: list[bool] | None=None, + datasets: list[OnDiskPsmDataset], + scores_list: list[np.ndarray[float]], + max_workers: int = 1, eval_fdr=0.01, dest_dir: Path | None = None, file_root: str = "", - sep="\t", prefixes: list[str | None] | None = None, - decoys=False, + write_decoys: bool = False, deduplication=True, do_rollup=True, proteins=None, - combine=False, append_to_output_file=False, rng=0, peps_error=False, peps_algorithm="qvality", qvalue_algorithm="tdc", sqlite_path=None, + stream_confidence=False, ): """Assign confidence to PSMs peptides, and optionally, proteins. Parameters ---------- max_workers - psms : list[OnDiskPsmDataset] + datasets : list[OnDiskPsmDataset] A collection of PSMs. + scores_list : list[numpy.ndarray] + The scores by which to rank the PSMs. rng : int or np.random.Generator, optional A seed or generator used for cross-validation split creation and to break ties, or ``None`` to use the default random number generator state. - scores : numpy.ndarray - The scores by which to rank the PSMs. The default, :code:`None`, - uses the feature that accepts the most PSMs at an FDR threshold of - `eval_fdr`. - descs : list[bool] | None - Are higher scores better? The default None, sets all entries to True. eval_fdr : float The FDR threshold at which to report and evaluate performance. If `scores` is not :code:`None`, this parameter has no affect on the @@ -531,11 +261,9 @@ def assign_confidence( dest_dir : Path or None, optional The directory in which to save the files. :code:`None` will use the current working directory. - sep : str, optional - The delimiter to use. prefixes : [str] The prefixes added to all output file names. - decoys : bool, optional + write_decoys : bool, optional Save decoys confidence estimates as well? deduplication: bool Are we performing deduplication on the psm level? @@ -543,8 +271,6 @@ def assign_confidence( do we apply rollup on peptides, modified peptides etc.? proteins: Proteins, optional collection of proteins - combine : bool, optional - Should groups be combined into a single file? append_to_output_file: bool do we append results to file ? sqlite_path: Path to the sqlite database to write mokapot results @@ -553,39 +279,27 @@ def assign_confidence( ------- None """ + is_sqlite = sqlite_path is not None - if scores is None: - scores = [] - for _psms in psms: - feat, _, _, desc = _psms.find_best_feature(eval_fdr) - LOGGER.info("Selected %s as the best feature.", feat) - scores.append(_psms.read_data(columns=[feat])[feat].values) - - if descs is None: - descs = [True] * len(psms) + if dest_dir is None: + dest_dir = Path() # just take the first one for info (and make sure the other are the same) - curr_psms = psms[0] - file_ext = os.path.splitext(curr_psms.filename)[-1] - for _psms in psms[1:]: - assert _psms.columns == curr_psms.columns - - # todo: maybe use a collections.namedtuple for all - # the level info instead of all the ? - # from collections import namedtuple - # LevelInfo = namedtuple('LevelInfo', - # ['name', 'data_path', 'deduplicate', 'colnames', 'colindices']) + curr_dataset = datasets[0] + file_ext = curr_dataset.reader.get_default_extension() + for dataset in datasets[1:]: + assert dataset.columns == curr_dataset.columns # Level data for psm level level = "psms" levels = [level] level_data_path = {level: dest_dir / f"{file_root}{level}{file_ext}"} - level_hash_columns = {level: curr_psms.spectrum_columns} + level_hash_columns = {level: curr_dataset.spectrum_columns} # Level data for higher rollup levels extra_output_columns = [] if do_rollup: - level_columns = curr_psms.level_columns + level_columns = curr_dataset.level_columns for level_column in level_columns: level = level_column.lower() + "s" # e.g. Peptide to peptides @@ -600,11 +314,9 @@ def assign_confidence( level = "proteins" levels_or_proteins = [*levels, level] level_data_path[level] = dest_dir / f"{file_root}{level}{file_ext}" - level_hash_columns[level] = curr_psms.protein_column + level_hash_columns[level] = curr_dataset.protein_column - # fixme: the output header and data do not fit, when the - # `extra_output_columns` are in a different place. Fix that. - out_columns_psms_peps = [ + output_column_names = [ "PSMId", "peptide", *extra_output_columns, @@ -614,7 +326,7 @@ def assign_confidence( "proteinIds", ] - out_columns_proteins = [ + output_column_names_proteins = [ "mokapot protein group", "best peptide", "stripped sequence", @@ -623,139 +335,155 @@ def assign_confidence( "posterior_error_prob", ] - for _psms, score, desc, prefix in zip(psms, scores, descs, prefixes): - out_metadata_columns = [ + @typechecked + def create_output_writer(path: Path, level: str, initialize: bool): + if level == "proteins": + output_columns = output_column_names_proteins + else: + output_columns = output_column_names + + # Create the writers + if is_sqlite: + writer = ConfidenceSqliteWriter( + sqlite_path, + columns=output_columns, + column_types=[], + level=level, + qvalue_column="q-value", + pep_column="posterior_error_prob", + ) + else: + writer = TabularDataWriter.from_suffix(path, output_columns, []) + + if initialize: + writer.initialize() + return writer + + for dataset, score, prefix in zip(datasets, scores_list, prefixes): + # todo: nice to have: move this column renaming stuff into the + # column defs module, and further, have standardized columns + # directly from the pin reader (applying the renaming itself) + level_column_names = [ "PSMId", - # fixme: Why is this the only column where we take - # the name from the input? - _psms.target_column, + dataset.target_column, "peptide", *extra_output_columns, "proteinIds", "score", ] - in_metadata_columns = [ - _psms.specId_column, - _psms.target_column, - _psms.peptide_column, + level_input_column_names = [ + dataset.specId_column, + dataset.target_column, + dataset.peptide_column, *extra_output_columns, - _psms.protein_column, + dataset.protein_column, "score", ] - input_output_column_mapping = { - i: j for i, j in zip(in_metadata_columns, out_metadata_columns) + level_input_output_column_mapping = { + in_col: out_col + for in_col, out_col in zip( + level_input_column_names, level_column_names + ) } file_prefix = file_root if prefix: file_prefix = f"{file_prefix}{prefix}." - out_files = {} + output_writers = {} for level in levels_or_proteins: - if level == "proteins": - output_columns = out_columns_proteins - else: - output_columns = out_columns_psms_peps - - outfile_targets = dest_dir / f"{file_prefix}targets.{level}" - writer = TabularDataWriter.from_suffix( - outfile_targets, output_columns + output_writers[level] = [] + + outfile_targets = ( + dest_dir / f"{file_prefix}targets.{level}{file_ext}" + ) + + output_writers[level].append( + create_output_writer( + outfile_targets, level, not append_to_output_file + ) ) - if not append_to_output_file: - writer.initialize() - out_files[level] = [outfile_targets] - if decoys: - outfile_decoys = dest_dir / f"{file_prefix}decoys.{level}" - writer = TabularDataWriter.from_suffix( - outfile_decoys, output_columns + if write_decoys and not is_sqlite: + outfile_decoys = ( + dest_dir / f"{file_prefix}decoys.{level}{file_ext}" + ) + output_writers[level].append( + create_output_writer( + outfile_decoys, level, not append_to_output_file + ) ) - if not append_to_output_file: - writer.initialize() - out_files[level].append(outfile_decoys) - with create_sorted_file_iterator( - _psms, + score_reader = TabularDataReader.from_array(score, "score") + with create_sorted_file_reader( + dataset, + score_reader, dest_dir, file_prefix, - do_rollup, + level_hash_columns["psms"] if deduplication else None, max_workers, - score, - ) as sorted_file_iterator: + level_input_output_column_mapping, + ) as sorted_file_reader: LOGGER.info("Assigning confidence...") LOGGER.info("Performing target-decoy competition...") LOGGER.info( "Keeping the best match per %s columns...", - "+".join(_psms.spectrum_columns), + "+".join(dataset.spectrum_columns), ) # The columns we get from the sorted file iterator - iterator_columns = _psms.metadata_columns + ["score"] - iterator_column_types = _psms.metadata_column_types + [ - get_score_column_type(file_ext) - ] - output_column_types = [ - iterator_column_types[iterator_columns.index(i)] - for i in in_metadata_columns + sorted_file_iterator = sorted_file_reader.get_row_iterator( + row_type=BufferType.Dicts + ) + type_map = sorted_file_reader.get_schema(as_dict=True) + level_column_types = [ + type_map[name] for name in level_column_names ] - handles = { + level_writers = { level: TabularDataWriter.from_suffix( level_data_path[level], - columns=out_metadata_columns, - column_types=output_column_types, - buffer_size=0, + columns=level_column_names, + column_types=level_column_types, + buffer_size=CONFIDENCE_CHUNK_SIZE, + buffer_type=BufferType.Dicts, ) for level in levels } - for writer in handles.values(): + for writer in level_writers.values(): writer.initialize() - seen_level_entities = {level: set() for level in levels} + def hash_data_row(data_row): + return str([ + data_row[level_input_output_column_mapping.get(col, col)] + for col in level_hash_columns[level] + ]) + seen_level_entities = {level: set() for level in levels} + score_stats = OnlineStatistics() psm_count = 0 - batches = {level: [] for level in levels} - batch_counts = {level: 0 for level in levels} for data_row in sorted_file_iterator: psm_count += 1 for level in levels: if level != "psms" or deduplication: - psm_hash = str( - [ - data_row.get(col) - for col in level_hash_columns[level] - ] - ) + psm_hash = hash_data_row(data_row) if psm_hash in seen_level_entities[level]: if level == "psms": + # If we are on the psms level, we can skip + # checking the other levels break continue seen_level_entities[level].add(psm_hash) - batches[level].append(data_row) - batch_counts[level] += 1 - if batch_counts[level] == CONFIDENCE_CHUNK_SIZE: - df = get_dataframe_from_records( - batches[level], - in_metadata_columns, - input_output_column_mapping, - target_column=_psms.target_column, - ) - handles[level].append_data(df) - batch_counts[level] = 0 - batches[level] = [] - for level, batch in batches.items(): - df = get_dataframe_from_records( - batch, - in_metadata_columns, - input_output_column_mapping, - target_column=_psms.target_column, - ) - handles[level].append_data(df) + out_row = { + col: data_row[col] for col in level_column_names + } + level_writers[level].append_data(out_row) + score_stats.update_single(data_row["score"]) for level in levels: count = len(seen_level_entities[level]) - handles[level].finalize() + level_writers[level].finalize() if level == "psms": if deduplication: LOGGER.info( @@ -763,28 +491,28 @@ def assign_confidence( ) else: LOGGER.info(f"\t- Found {psm_count} PSMs.") + LOGGER.info( + f"\t- The average score was {score_stats.mean:.3f} " + f"with standard deviation {score_stats.sd:.3f}." + ) else: LOGGER.info(f"\t- Found {count} unique {level}.") - LinearConfidence( - psms=_psms, + Confidence( + dataset=dataset, levels=levels_or_proteins, - level_paths=[ - level_data_path[level] for level in levels_or_proteins - ], - out_paths=[out_files[level] for level in levels_or_proteins], + level_paths=level_data_path, + out_writers=output_writers, eval_fdr=eval_fdr, - desc=desc, - sep=sep, - decoys=decoys, - deduplication=deduplication, + write_decoys=write_decoys, do_rollup=do_rollup, proteins=proteins, rng=rng, peps_error=peps_error, peps_algorithm=peps_algorithm, qvalue_algorithm=qvalue_algorithm, - sqlite_path=sqlite_path, + stream_confidence=stream_confidence, + score_stats=score_stats, ) if not prefix: append_to_output_file = True @@ -792,56 +520,60 @@ def assign_confidence( @contextmanager @typechecked -def create_sorted_file_iterator( - _psms, +def create_sorted_file_reader( + dataset: OnDiskPsmDataset, + score_reader: TabularDataReader, dest_dir: Path, file_prefix: str, - do_rollup: bool, + deduplication_columns: list[str] | None, max_workers: int, - score: np.ndarray[float], + input_output_column_mapping, ): - # Read from the input psms (PsmDataset) and write into smaller - # sorted files, by - - # a) Create a reader that only reads columns given in - # psms.metadata_columns in chunks of size CONFIDENCE_CHUNK_SIZE - reader = TabularDataReader.from_path(_psms.filename) + """Read from the input psms and write into smaller sorted files by score""" + + # Create a reader that only reads columns given in psms.metadata_columns + # in chunks of size CONFIDENCE_CHUNK_SIZE and joins the scores to it + reader = join_readers([ + ColumnMappedReader(dataset.reader, input_output_column_mapping), + score_reader, + ]) + input_columns = dataset.metadata_columns + ["score"] + output_columns = [ + input_output_column_mapping.get(name, name) for name in input_columns + ] file_iterator = reader.get_chunked_data_iterator( - CONFIDENCE_CHUNK_SIZE, _psms.metadata_columns + CONFIDENCE_CHUNK_SIZE, output_columns ) - outfile_ext = _psms.filename.suffix - # b) Split the scores in chunks of the same size - scores_slices = create_chunks(score, chunk_size=CONFIDENCE_CHUNK_SIZE) + # Write those chunks in parallel, where the columns are given + # by dataset.metadata plus the "scores" column - # c) Write those chunks in parallel, where the columns are given - # by psms.metadata plus the "scores" column - # (NB: after the last change the columns are now indeed in the - # order given by metadata_columns and not by file header order) - Parallel(n_jobs=max_workers, require="sharedmem")( + outfile_ext = dataset.reader.file_name.suffix + scores_metadata_paths = Parallel(n_jobs=max_workers, require="sharedmem")( delayed(_save_sorted_metadata_chunks)( chunk_metadata, - score_chunk, - _psms, - do_rollup, dest_dir / f"{file_prefix}scores_metadata_{i}{outfile_ext}", + output_columns, + dataset.target_column, + deduplication_columns, ) - for chunk_metadata, score_chunk, i in zip( - file_iterator, scores_slices, range(len(scores_slices)) - ) + for i, chunk_metadata in enumerate(file_iterator) ) - scores_metadata_paths = list( - dest_dir.glob(f"{file_prefix}scores_metadata_*") - ) - sorted_file_iterator = merge_sort( - scores_metadata_paths, score_column="score" + readers = [ + TabularDataReader.from_path(path) for path in scores_metadata_paths + ] + + sorted_file_reader = MergedTabularDataReader( + readers, + priority_column="score", + reader_chunk_size=CONFIDENCE_CHUNK_SIZE, ) # Return the sorted iterator and clean up afterwards, regardless of whether # an exception was thrown in the `with` block try: - yield sorted_file_iterator + yield sorted_file_reader finally: for sc_path in scores_metadata_paths: try: @@ -855,107 +587,130 @@ def create_sorted_file_iterator( @typechecked def _save_sorted_metadata_chunks( chunk_metadata: pd.DataFrame, - score_chunk: np.ndarray[float], - psms, - deduplication: bool, chunk_write_path: Path, + output_columns, + target_column, + deduplication_columns, ): chunk_metadata = convert_targets_column( data=chunk_metadata, - target_column=psms.target_column, + target_column=target_column, ) - - chunk_metadata = chunk_metadata.assign(score=score_chunk) chunk_metadata.sort_values(by="score", ascending=False, inplace=True) - if deduplication: - chunk_metadata = chunk_metadata.drop_duplicates(psms.spectrum_columns) + if deduplication_columns is not None: + # This is not strictly necessary, as we deduplicate also afterwards, + # but speeds up the process + chunk_metadata.drop_duplicates(deduplication_columns, inplace=True) + chunk_writer = TabularDataWriter.from_suffix( chunk_write_path, - columns=psms.metadata_columns + ["score"], + columns=output_columns, + column_types=[], ) chunk_writer.write(chunk_metadata) + return chunk_write_path @typechecked -def get_unique_peptides_from_psms( - iterable, peptide_col_name, out_peptides: Path, write_columns: list, sep +def compute_and_write_confidence( + temp_reader: TabularDataReader, + writer: TabularDataWriter, + qvalue_algorithm: str, + peps_algorithm: str, + stream_confidence: bool, + score_stats: OnlineStatistics, + peps_error: bool, + level: str, + eval_fdr: float, ): - f_peptide = open(out_peptides, "a") - seen_peptide = set() - for line_list in iterable: - line_hash_peptide = line_list[peptide_col_name] - if line_hash_peptide not in seen_peptide: - seen_peptide.add(line_hash_peptide) - f_peptide.write( - f"{sep.join([line_list[key] for key in write_columns])}\n" + qvals_column = get_standard_column_name("q-value") + peps_column = get_standard_column_name("posterior_error_prob") + + if not stream_confidence: + # Read all data at once, compute the peps and qvalues and write in one + # large chunk + data = temp_reader.read() + scores = data["score"].to_numpy() + targets = ~data["is_decoy"].to_numpy() + if all(targets): + LOGGER.warning( + "No decoy PSMs remain for confidence estimation. " + "Confidence estimates may be unreliable." ) - f_peptide.close() - return len(seen_peptide) - - -def plot_qvalues(qvalues, threshold=0.1, ax=None, **kwargs): - """ - Plot the cumulative number of discoveries over range of q-values. - - Parameters - ---------- - qvalues : numpy.ndarray - The q-values to plot. - threshold : float, optional - Indicates the maximum q-value to plot. - ax : matplotlib.pyplot.Axes, optional - The matplotlib Axes on which to plot. If `None` the current - Axes instance is used. - **kwargs : dict, optional - Arguments passed to :py:func:`matplotlib.axes.Axes.plot`. - - Returns - ------- - matplotlib.pyplot.Axes - An :py:class:`matplotlib.axes.Axes` with the cumulative - number of accepted target PSMs or peptides. - """ - if ax is None: - ax = plt.gca() + # Estimate q-values and assign + LOGGER.info( + f"Assigning q-values to {level} " + f"(using {qvalue_algorithm} algorithm) ..." + ) + qvals = qvalues_from_scores(scores, targets, qvalue_algorithm) + data[qvals_column] = qvals - # Calculate cumulative targets at each q-value - qvals = pd.Series(qvalues, name="qvalue") - qvals = qvals.sort_values(ascending=True).to_frame() - qvals["target"] = 1 - qvals["num"] = qvals["target"].cumsum() - qvals = qvals.groupby(["qvalue"]).max().reset_index() - qvals = qvals[["qvalue", "num"]] + # Logging update on q-values + num_found = (qvals[targets] <= eval_fdr).sum() + LOGGER.info(f"\t- Found {num_found} {level} with q<={eval_fdr}") - zero = pd.DataFrame({"qvalue": qvals["qvalue"][0], "num": 0}, index=[-1]) - qvals = pd.concat([zero, qvals], sort=True).reset_index(drop=True) + # Calculate PEPs + LOGGER.info( + "Assigning PEPs to %s (using %s algorithm) ...", + level, + peps_algorithm, + ) + try: + peps = peps_from_scores( + scores, targets, is_tdc=True, pep_algorithm=peps_algorithm + ) + except PepsConvergenceError: + LOGGER.info( + f"\t- Encountered convergence problems in `{peps_algorithm}`. " + "Falling back to qvality ...", + ) + peps = peps_from_scores( + scores, targets, is_tdc=True, pep_algorithm="qvality" + ) - xmargin = threshold * 0.05 - ymax = qvals.num[qvals["qvalue"] <= (threshold + xmargin)].max() - ymargin = ymax * 0.05 + if peps_error and all(peps == 1): + raise ValueError("PEP values are all equal to 1.") + data[peps_column] = peps - # Set margins - curr_ylims = ax.get_ylim() - if curr_ylims[1] < ymax + ymargin: - ax.set_ylim(0 - ymargin, ymax + ymargin) + writer.append_data(data) - ax.set_xlim(0 - xmargin, threshold + xmargin) - ax.set_xlabel("q-value") - ax.set_ylabel("Discoveries") + else: # Here comes the streaming part + LOGGER.info("Computing statistics for q-value and PEP assignment...") + bin_edges = HistData.get_bin_edges(score_stats, clip=(50, 500)) + score_target_iterator = create_score_target_iterator( + temp_reader.get_chunked_data_iterator( + chunk_size=CONFIDENCE_CHUNK_SIZE, columns=["score", "is_decoy"] + ) + ) + hist_data = TDHistData.from_score_target_iterator( + bin_edges, score_target_iterator + ) + if hist_data.decoys.counts.sum() == 0: + LOGGER.warning( + "No decoy PSMs remain for confidence estimation. " + "Confidence estimates may be unreliable." + ) - ax.step(qvals["qvalue"].values, qvals.num.values, where="post", **kwargs) + LOGGER.info("Estimating q-value and PEP assignment functions...") + qvalues_func = qvalues_func_from_hist(hist_data, is_tdc=True) + peps_func = peps_func_from_hist_nnls(hist_data, is_tdc=True) - return ax + LOGGER.info("Streaming q-value and PEP assignments...") + for df_chunk in temp_reader.get_chunked_data_iterator( + chunk_size=CONFIDENCE_CHUNK_SIZE + ): + scores = df_chunk["score"].values + df_chunk[qvals_column] = qvalues_func(scores) + df_chunk[peps_column] = peps_func(scores) + writer.append_data(df_chunk) -def _new_column(name, df): - """Add a new column, ensuring a unique name""" - new_name = name - cols = set(df.columns) - i = 0 - while new_name in cols: - new_name = name + "_" + str(i) - i += 1 - return new_name +@typechecked +def create_score_target_iterator(chunked_iterator: Iterator): + for df_chunk in chunked_iterator: + scores = df_chunk["score"].values + targets = ~df_chunk["is_decoy"].values + yield scores, targets diff --git a/mokapot/confidence_writer.py b/mokapot/confidence_writer.py deleted file mode 100644 index 8faf8afd..00000000 --- a/mokapot/confidence_writer.py +++ /dev/null @@ -1,158 +0,0 @@ -from __future__ import annotations - -import sqlite3 -from pathlib import Path -from typing import Iterator, Iterable - -import numpy as np -import pandas as pd -from typeguard import typechecked - -from .tabular_data import TabularDataWriter, SqliteWriter - - -@typechecked -class ConfidenceSqliteWriter(SqliteWriter): - def __init__( - self, - database: str | Path | sqlite3.Connection, - columns: list[str], - column_types: list | None = None, - level: str = "psms", - qvalue_column: str = "q_value", - pep_column: str = "posterior_error_prob", - ) -> None: - super().__init__(database, columns, column_types) - self.level_cols = { - "precursors": ["PRECURSOR_VALIDATION", "PCM_ID", "Precursor"], - "modifiedpeptides": [ - "MODIFIED_PEPTIDE_VALIDATION", - "MODIFIED_PEPTIDE_ID", - "ModifiedPeptide", - ], - "peptides": ["PEPTIDE_VALIDATION", "PEPTIDE_ID", "peptide"], - "peptidegroups": [ - "PEPTIDE_GROUP_VALIDATION", - "PEPTIDE_GROUP_ID", - "PeptideGroup", - ], - } - self.level = level - self.qvalue_column = qvalue_column - self.pep_column = pep_column - - def get_query(self, level, qvalue_column, pep_column): - if level == "psms": - query = f"UPDATE CANDIDATE SET PSM_FDR = :{qvalue_column}, SVM_SCORE = :score, POSTERIOR_ERROR_PROBABILITY = :{pep_column} WHERE CANDIDATE_ID = :PSMId;" # noqa: E501 - else: - table_name, table_id_col, mokapot_id_col = self.level_cols[level] - query = f"INSERT INTO {table_name}({table_id_col},FDR,PEP,SVM_SCORE) VALUES(:{mokapot_id_col},:{qvalue_column},:{pep_column},:score)" # noqa: E501 - return query - - def append_data(self, data): - query = self.get_query(self.level, self.qvalue_column, self.pep_column) - # todo: what about using connection.executemany()? Should be faster... - data = data.to_dict("records") - for row in data: - self.connection.execute(query, row) - - -@typechecked -def write_confidences( - data_iterator: Iterator[pd.DataFrame], - q_value_iterator: Iterable[np.array], - pep_iterator: Iterable[np.array], - target_iterator: Iterable[np.array], - out_paths: list[Path], - decoys: bool, - level: str, - out_columns: list[str], - qvalue_column: str = "q_value", - pep_column: str = "posterior_error_prob", -) -> None: - """ - Write confidences for given rollup level to output files. - Note, that the iterators all need to yield the same number of chunks, each - one having the same size/length as the others. - - Parameters - ---------- - data_iterator : Iterator[pd.DataFrame] - An iterator that yields chunks of data as pandas DataFrames. - q_value_iterator : Iterable[np.array] - A iterator that yields numpy arrays containing the q-values for each - data chunk. - pep_iterator : Iterable[np.array] - A iterator that yields numpy arrays containing the posterior error - probabilities for each data chunk. - target_iterator : Iterable[np.array] - A iterator that yields numpy arrays indicating whether each data point - is a target or decoy for each data chunk. - out_paths : list[Path] - A list of output file paths where the confidence data will be written. - The first element contains the path for the targets and the second - those for the decoys. - decoys : bool - A boolean flag indicating whether to include decoy data in the output. - level : str - The rollup level (psms, percursors, peptides, etc.) - out_columns : list[str] - A list of column names to include in the output. - qvalue_column : str, optional - The name of the column to store the q-values. Default is 'q_value'. - pep_column : str, optional - The name of the column to store the posterior error probabilities. - Default is 'posterior_error_prob'. - - Returns - ------- - None - - """ - if not decoys and len(out_paths) > 1: - out_paths.pop(1) - is_sqlite = True if out_paths[0].suffix == ".db" else False - - # Create the writers - if is_sqlite: - - def create_writer(path): - return ConfidenceSqliteWriter( - path, - out_columns, - level=level, - qvalue_column=qvalue_column, - pep_column=pep_column, - ) - else: - - def create_writer(path): - return TabularDataWriter.from_suffix(path, out_columns) - - writers = [create_writer(path) for path in out_paths] - # for writer in writers: - # writer.initialize() - - # Now write the confidence data - for data_chunk, qvals_chunk, peps_chunk, targets_chunk in zip( - data_iterator, q_value_iterator, pep_iterator, target_iterator - ): - data_chunk[qvalue_column] = qvals_chunk - data_chunk[pep_column] = peps_chunk - data_out = [] - if not is_sqlite: - data_out.append(data_chunk.loc[targets_chunk, out_columns]) - if decoys: - data_out.append(data_chunk.loc[~targets_chunk, out_columns]) - else: - if decoys: - data_out.append(data_chunk) - else: - data_out.append(data_chunk.loc[targets_chunk, out_columns]) - - for writer, data in zip(writers, data_out): - writer.append_data(data) - - # Finalize writer (clear buffers etc.) - for writer in writers: - writer.finalize() diff --git a/mokapot/config.py b/mokapot/config.py index cecfe5b2..3b408cd5 100644 --- a/mokapot/config.py +++ b/mokapot/config.py @@ -72,18 +72,10 @@ def _parser(): ), ) - parser.add_argument( - "--verify_pin", - type=bool, - default=True, - help="Verify that PIN input files are valid TSVs. If not convert them.", - ) - parser.add_argument( "-d", "--dest_dir", type=Path, - default=Path("."), help=( "The directory in which to write the result files. Defaults to " "the current working directory" @@ -326,14 +318,14 @@ def _parser(): "--ensemble", default=False, action="store_true", - help=("activate ensemble prediction. "), + help="Activate ensemble prediction.", ) parser.add_argument( "--peps_error", default=False, action="store_true", - help=("raise error when all PEPs values are equal to 1."), + help="Raise error when all PEPs values are equal to 1.", ) parser.add_argument( @@ -351,7 +343,7 @@ def _parser(): default="tdc", choices=["tdc", "from_peps", "from_counts"], help=( - "Specify the algorithm for qvalue computation. `tdc is` " + "Specify the algorithm for qvalue computation. `tdc` is " "the default mokapot algorithm." ), ) @@ -399,8 +391,15 @@ def _parser(): default=None, type=Path, help="Optionally, sets a path to an MSAID sqlite result database " - "for writing outputs to. If not set (None), results are " - "written in the standard TSV format.", + "for writing outputs to. If not set (None), results are " + "written in the standard TSV format.", + ) + + parser.add_argument( + "--stream_confidence", + default=False, + action="store_true", + help=("Specify whether confidence assignment shall be streamed."), ) return parser diff --git a/mokapot/dataset.py b/mokapot/dataset.py index 0db62cb1..9f440067 100644 --- a/mokapot/dataset.py +++ b/mokapot/dataset.py @@ -17,152 +17,41 @@ """ -from __future__ import annotations - import logging -from abc import ABC, abstractmethod +from abc import ABC from pathlib import Path - from zlib import crc32 + import numpy as np import pandas as pd from typeguard import typechecked -from .tabular_data import TabularDataReader -from . import qvalues -from . import utils -from .parsers.fasta import read_fasta -from .proteins import Proteins +from mokapot import qvalues +from mokapot import utils +from mokapot.parsers.fasta import read_fasta +from mokapot.proteins import Proteins +from .tabular_data import TabularDataReader LOGGER = logging.getLogger(__name__) # Classes --------------------------------------------------------------------- class PsmDataset(ABC): - """ - Store a collection of PSMs and their features. + """Store a collection of PSMs and their features. - :meta private: + Note: Currently, the derived classes LinearPsmDataset and OnDiskPsmDataset + don't have anything in common, so maybe this class can be removed in the + future. """ - @property - @abstractmethod - def targets(self): - """An array indicating whether each PSM is a target.""" - return - - @abstractmethod - def _update_labels(self, scores, eval_fdr, desc): - """ - Return the label for each PSM, given it's score. - - This method is used during model training to define positive - examples. These are traditionally the target PSMs that fall - within a specified FDR threshold. - - Parameters - ---------- - scores : numpy.ndarray - The score used to rank the PSMs. - eval_fdr : float - The false discovery rate threshold to use. - desc : bool - Are higher scores better? - - Returns - ------- - numpy.ndarray - The label of each PSM, where 1 indicates a positive example, - -1 indicates a negative example, and 0 removes the PSM from - training. Typically, 0 is reserved for targets, below a - specified FDR threshold. - """ - return - def __init__( self, - psms, - spectrum_columns, - feature_columns, - other_columns, - copy_data, rng, ): - """Initialize an object""" - self._data = psms.copy(deep=copy_data).reset_index(drop=True) + """Initialize a PsmDataset""" self._proteins = None self.rng = rng - # Set columns - self._spectrum_columns = utils.tuplize(spectrum_columns) - - if other_columns is not None: - other_columns = utils.tuplize(other_columns) - else: - other_columns = () - - # Check that all of the columns exist: - used_columns = sum([other_columns, self._spectrum_columns], tuple()) - - missing_columns = [c not in self.data.columns for c in used_columns] - if not missing_columns: - raise ValueError( - "The following specified columns were not found: " - f"{missing_columns}" - ) - - # Get the feature columns - if feature_columns is None: - self._feature_columns = tuple( - c for c in self.data.columns if c not in used_columns - ) - else: - self._feature_columns = utils.tuplize(feature_columns) - - @property - def data(self): - """The full collection of PSMs as a :py:class:`pandas.DataFrame`.""" - return self._data - - def __len__(self): - """Return the number of PSMs""" - return len(self._data.index) - - @property - def _metadata_columns(self): - """A list of the metadata columns""" - return tuple( - c for c in self.data.columns if c not in self._feature_columns - ) - - @property - def metadata(self): - """A :py:class:`pandas.DataFrame` of the metadata.""" - return self.data.loc[:, self._metadata_columns] - - @property - def features(self): - """A :py:class:`pandas.DataFrame` of the features.""" - return self.data.loc[:, self._feature_columns] - - @property - def spectra(self): - """ - A :py:class:`pandas.DataFrame` of the columns that uniquely - identify a mass spectrum. - """ - return self.data.loc[:, self._spectrum_columns] - - @property - def columns(self): - """The columns of the dataset.""" - return self.data.columns.tolist() - - @property - def has_proteins(self): - """Has a FASTA file been added?""" - return self._proteins is not None - @property def rng(self): """The random number generator for model training.""" @@ -196,84 +85,6 @@ def add_proteins(self, proteins, **kwargs): self._proteins = proteins - def _targets_count_by_feature(self, desc, eval_fdr): - """ - iterate over features and count the number of positive examples - - :param desc: bool - Are high scores better for the best feature? - :param eval_fdr: float - The false discovery rate threshold to use. - :return: pd.Series - The number of positive examples for each feature. - """ - return pd.Series( - [ - ( - self._update_labels( - self.data.loc[:, col], - eval_fdr=eval_fdr, - desc=desc, - ) - == 1 - ).sum() - for col in self._feature_columns - ], - index=self._feature_columns, - ) - - def _find_best_feature(self, eval_fdr): - """ - Find the best feature to separate targets from decoys at the - specified false-discovery rate threshold. - - Parameters - ---------- - eval_fdr : float - The false-discovery rate threshold used to define the - best feature. - - Returns - ------- - A tuple of an str, int, and numpy.ndarray - best_feature : str - The name of the best feature. - num_passing : int - The number of accepted PSMs using the best feature. - labels : numpy.ndarray - The new labels defining positive and negative examples when - the best feature is used. - desc : bool - Are high scores better for the best feature? - """ - best_feat = None - best_positives = 0 - new_labels = None - for desc in (True, False): - num_passing = self._targets_count_by_feature(desc, eval_fdr) - feat_idx = num_passing.idxmax() - num_passing = num_passing[feat_idx] - - if num_passing > best_positives: - best_positives = num_passing - best_feat = feat_idx - new_labels = self._update_labels( - self.data.loc[:, feat_idx], eval_fdr=eval_fdr, desc=desc - ) - best_desc = desc - - if best_feat is None: - raise RuntimeError( - f"No PSMs found below the 'eval_fdr' {eval_fdr}." - ) - - return best_feat, best_positives, new_labels, best_desc - - def _calibrate_scores(self, scores, eval_fdr, desc=True): - calibrate_scores( - scores=scores, eval_fdr=eval_fdr, desc=desc, targets=self.targets - ) - class LinearPsmDataset(PsmDataset): """Store and analyze a collection of PSMs. @@ -374,7 +185,10 @@ def __init__( rng=None, enforce_checks=True, ): - """Initialize a PsmDataset object.""" + """Initialize a LinearPsmDataset object.""" + super().__init__(rng=rng) + self._data = psms.copy(deep=copy_data).reset_index(drop=True) + self._target_column = target_column self._peptide_column = peptide_column self._protein_column = protein_column @@ -397,14 +211,30 @@ def __init__( if opt_column is not None: other_columns.append(opt_column) - super().__init__( - psms=psms, - spectrum_columns=spectrum_columns, - feature_columns=feature_columns, - other_columns=other_columns, - copy_data=copy_data, - rng=rng, - ) + # Set columns + self._spectrum_columns = utils.tuplize(spectrum_columns) + + if other_columns is not None: + other_columns = utils.tuplize(other_columns) + else: + other_columns = () + + # Check that all of the columns exist: + used_columns = sum([other_columns, self._spectrum_columns], tuple()) + + missing_columns = [c not in self.data.columns for c in used_columns] + if not missing_columns: + raise ValueError( + "The following specified columns were not found: " f"{missing_columns}" + ) + + # Get the feature columns + if feature_columns is None: + self._feature_columns = tuple( + c for c in self.data.columns if c not in used_columns + ) + else: + self._feature_columns = utils.tuplize(feature_columns) self._data[target_column] = self._data[target_column].astype(bool) num_targets = (self.targets).sum() @@ -418,6 +248,15 @@ def __init__( if not num_decoys: raise ValueError("No decoy PSMs were detected.") + @property + def data(self): + """The full collection of PSMs as a :py:class:`pandas.DataFrame`.""" + return self._data + + def __len__(self): + """Return the number of PSMs""" + return len(self._data.index) + def __repr__(self): """How to print the class""" return ( @@ -444,16 +283,149 @@ def peptides(self): return self.data.loc[:, self._peptide_column] def _update_labels(self, scores, eval_fdr=0.01, desc=True): + """ + Return the label for each PSM, given it's score. + + This method is used during model training to define positive + examples. These are traditionally the target PSMs that fall + within a specified FDR threshold. + + Parameters + ---------- + scores : numpy.ndarray + The score used to rank the PSMs. + eval_fdr : float + The false discovery rate threshold to use. + desc : bool + Are higher scores better? + + Returns + ------- + numpy.ndarray + The label of each PSM, where 1 indicates a positive example, + -1 indicates a negative example, and 0 removes the PSM from + training. Typically, 0 is reserved for targets, below a + specified FDR threshold. + """ return _update_labels( scores=scores, targets=self.targets, eval_fdr=eval_fdr, desc=desc ) + @property + def _metadata_columns(self): + """A list of the metadata columns""" + return tuple(c for c in self.data.columns if c not in self._feature_columns) + + @property + def metadata(self): + """A :py:class:`pandas.DataFrame` of the metadata.""" + return self.data.loc[:, self._metadata_columns] + + @property + def features(self): + """A :py:class:`pandas.DataFrame` of the features.""" + return self.data.loc[:, self._feature_columns] + + @property + def spectra(self): + """ + A :py:class:`pandas.DataFrame` of the columns that uniquely + identify a mass spectrum. + """ + return self.data.loc[:, self._spectrum_columns] + + @property + def columns(self): + """The columns of the dataset.""" + return self.data.columns.tolist() -class OnDiskPsmDataset: - @typechecked + @property + def has_proteins(self): + """Has a FASTA file been added?""" + return self._proteins is not None + + def _targets_count_by_feature(self, desc, eval_fdr): + """ + iterate over features and count the number of positive examples + + :param desc: bool + Are high scores better for the best feature? + :param eval_fdr: float + The false discovery rate threshold to use. + :return: pd.Series + The number of positive examples for each feature. + """ + return pd.Series( + [ + ( + self._update_labels( + self.data.loc[:, col], + eval_fdr=eval_fdr, + desc=desc, + ) + == 1 + ).sum() + for col in self._feature_columns + ], + index=self._feature_columns, + ) + + def _find_best_feature(self, eval_fdr): + """ + Find the best feature to separate targets from decoys at the + specified false-discovery rate threshold. + + Parameters + ---------- + eval_fdr : float + The false-discovery rate threshold used to define the + best feature. + + Returns + ------- + A tuple of an str, int, and numpy.ndarray + best_feature : str + The name of the best feature. + num_passing : int + The number of accepted PSMs using the best feature. + labels : numpy.ndarray + The new labels defining positive and negative examples when + the best feature is used. + desc : bool + Are high scores better for the best feature? + """ + best_feat = None + best_positives = 0 + new_labels = None + for desc in (True, False): + num_passing = self._targets_count_by_feature(desc, eval_fdr) + feat_idx = num_passing.idxmax() + num_passing = num_passing[feat_idx] + + if num_passing > best_positives: + best_positives = num_passing + best_feat = feat_idx + new_labels = self._update_labels( + self.data.loc[:, feat_idx], eval_fdr=eval_fdr, desc=desc + ) + best_desc = desc + + if best_feat is None: + raise RuntimeError(f"No PSMs found below the 'eval_fdr' {eval_fdr}.") + + return best_feat, best_positives, new_labels, best_desc + + def _calibrate_scores(self, scores, eval_fdr, desc=True): + calibrate_scores( + scores=scores, eval_fdr=eval_fdr, desc=desc, targets=self.targets + ) + + +@typechecked +class OnDiskPsmDataset(PsmDataset): def __init__( self, - filename: Path | None, + filename_or_reader: Path | TabularDataReader, columns, target_column, spectrum_columns, @@ -472,8 +444,13 @@ def __init__( charge_column, spectra_dataframe, ): - """Initialize a PsmDataset object.""" - self.filename = filename + """Initialize an OnDiskPsmDataset object.""" + super().__init__(rng=None) + if isinstance(filename_or_reader, TabularDataReader): + self.reader = filename_or_reader + else: + self.reader = TabularDataReader.from_path(filename_or_reader) + self.columns = columns self.target_column = target_column self.peptide_column = peptide_column @@ -492,38 +469,39 @@ def __init__( self.specId_column = specId_column self.spectra_dataframe = spectra_dataframe - # todo: btw: should not get the filename but a reader object or - # something, in order to parse other filetypes without if's - if filename: - columns = TabularDataReader.from_path(filename).get_column_names() - - def check_column(column): - if column and column not in columns: - raise ValueError( - f"Column '{column}' not found in data columns of file" - f" '{filename}' ({columns})" - ) + columns = self.reader.get_column_names() + + # todo: nice to have: here reader.file_name should be something like + # reader.user_repr() which tells the user where to look for the + # error, however, we cannot expect the reader to have a file_name + def check_column(column): + if column and column not in columns: + file_name = getattr(self.reader, "file_name", "") + raise ValueError( + f"Column '{column}' not found in data columns of file" + f" '{file_name}' ({columns})" + ) - def check_columns(columns): - if columns: - for column in columns: - check_column(column) - - check_columns(self.columns) - check_column(self.target_column) - check_column(self.peptide_column) - check_column(self.protein_column) - check_columns(self.spectrum_columns) - check_columns(self.feature_columns) - check_columns(self.metadata_columns) - check_columns(self.level_columns) - check_column(self.filename_column) - check_column(self.scan_column) - check_column(self.calcmass_column) - check_column(self.expmass_column) - check_column(self.rt_column) - check_column(self.charge_column) - check_column(self.specId_column) + def check_columns(columns): + if columns: + for column in columns: + check_column(column) + + check_columns(self.columns) + check_column(self.target_column) + check_column(self.peptide_column) + check_column(self.protein_column) + check_columns(self.spectrum_columns) + check_columns(self.feature_columns) + check_columns(self.metadata_columns) + check_columns(self.level_columns) + check_column(self.filename_column) + check_column(self.scan_column) + check_column(self.calcmass_column) + check_column(self.expmass_column) + check_column(self.rt_column) + check_column(self.charge_column) + check_column(self.specId_column) def calibrate_scores(self, scores, eval_fdr, desc=True): """ @@ -553,9 +531,7 @@ def calibrate_scores(self, scores, eval_fdr, desc=True): labels = _update_labels(scores, targets, eval_fdr, desc) pos = labels == 1 if not pos.sum(): - raise RuntimeError( - "No target PSMs were below the 'eval_fdr' threshold." - ) + raise RuntimeError("No target PSMs were below the 'eval_fdr' threshold.") target_score = np.min(scores[pos]) decoy_score = np.median(scores[labels == -1]) @@ -613,9 +589,7 @@ def find_best_feature(self, eval_fdr): best_desc = desc if best_feat is None: - raise RuntimeError( - f"No PSMs found below the 'eval_fdr' {eval_fdr}." - ) + raise RuntimeError(f"No PSMs found below the 'eval_fdr' {eval_fdr}.") return best_feat, best_positives, new_labels, best_desc @@ -630,6 +604,32 @@ def update_labels(self, scores, target_column, eval_fdr=0.01, desc=True): desc=desc, ) + @staticmethod + def _hash_row(x: np.ndarray) -> int: + """ + Hash array for splitting of test/training sets. + + Parameters + ---------- + x : np.ndarray + Input array to be hashed. + + Returns + ------- + int + Computed hash of the input array. + """ + + def to_base_val(v): + """Return base python value also for numpy types""" + try: + return v.item() + except AttributeError: + return v + + tup = tuple(to_base_val(x) for x in x) + return crc32(str(tup).encode()) + def _split(self, folds, rng): """ Get the indices for random, even splits of the dataset. @@ -652,13 +652,7 @@ def _split(self, folds, rng): """ spectra = self.spectra_dataframe[self.spectrum_columns].values del self.spectra_dataframe - spectra = np.apply_along_axis( - lambda x: crc32( - str((x[0], x[1])).encode() - ), # fixme: why not just str(x) or str(tuple(x)) - 1, - spectra, - ) + spectra = np.apply_along_axis(OnDiskPsmDataset._hash_row, 1, spectra) # sort values to get start position of unique hashes spectra_idx = np.argsort(spectra) @@ -687,13 +681,12 @@ def _split(self, folds, rng): return spectra_idx def read_data(self, columns=None, chunk_size=None): - reader = TabularDataReader.from_path(self.filename) if chunk_size: - return reader.get_chunked_data_iterator( + return self.reader.get_chunked_data_iterator( chunk_size=chunk_size, columns=columns ) else: - return reader.read(columns=columns) + return self.reader.read(columns=columns) @typechecked @@ -765,9 +758,7 @@ def calibrate_scores(scores, targets, eval_fdr, desc=True): labels = _update_labels(scores, targets, eval_fdr, desc) pos = labels == 1 if not pos.sum(): - raise RuntimeError( - "No target PSMs were below the 'eval_fdr' threshold." - ) + raise RuntimeError("No target PSMs were below the 'eval_fdr' threshold.") target_score = np.min(scores[pos]) decoy_score = np.median(scores[labels == -1]) @@ -777,13 +768,12 @@ def calibrate_scores(scores, targets, eval_fdr, desc=True): @typechecked def update_labels( - file_name: Path, + reader: TabularDataReader, scores, target_column, eval_fdr=0.01, desc=True, ): - reader = TabularDataReader.from_path(file_name) df = reader.read(columns=[target_column]) return _update_labels( scores=scores, diff --git a/mokapot/model.py b/mokapot/model.py index 1aae56cc..9956a123 100644 --- a/mokapot/model.py +++ b/mokapot/model.py @@ -21,13 +21,15 @@ import numpy as np import pandas as pd from sklearn.base import clone -from sklearn.svm import LinearSVC +from sklearn.exceptions import NotFittedError from sklearn.model_selection import GridSearchCV, KFold from sklearn.model_selection._search import BaseSearchCV from sklearn.preprocessing import StandardScaler -from sklearn.exceptions import NotFittedError +from sklearn.svm import LinearSVC from typeguard import typechecked +from mokapot import LinearPsmDataset + LOGGER = logging.getLogger(__name__) # Constants ------------------------------------------------------------------- @@ -39,6 +41,7 @@ # Classes --------------------------------------------------------------------- +@typechecked class Model: """ A machine learning model to re-score PSMs. @@ -207,13 +210,13 @@ def save(self, out_file: Path): return out_file - def decision_function(self, psms): + def decision_function(self, dataset: LinearPsmDataset): """ Score a collection of PSMs Parameters ---------- - psms : PsmDataset object + dataset : PsmDataset object :doc:`A collection of PSMs ` to score. Returns @@ -224,7 +227,7 @@ def decision_function(self, psms): if not self.is_trained: raise NotFittedError("This model is untrained. Run fit() first.") - feat_names = psms.features.columns.tolist() + feat_names = dataset.features.columns.tolist() if set(feat_names) != set(self.features): raise ValueError( "Features of the input data do not match the " @@ -232,16 +235,16 @@ def decision_function(self, psms): ) feat = self.scaler.transform( - psms.features.loc[:, self.features].values + dataset.features.loc[:, self.features].values ) return _get_scores(self.estimator, feat) - def predict(self, psms): + def predict(self, dataset: LinearPsmDataset): """Alias for :py:meth:`decision_function`.""" - return self.decision_function(psms) + return self.decision_function(dataset) - def fit(self, psms): + def fit(self, dataset: LinearPsmDataset): """ Fit the model using the Percolator algorithm. @@ -254,7 +257,7 @@ def fit(self, psms): Parameters ---------- - psms : PsmDataset object + dataset : PsmDataset object :doc:`A collection of PSMs ` from which to train the model. @@ -262,17 +265,17 @@ def fit(self, psms): ------- self """ - if not (psms.targets).sum(): + if not (dataset.targets).sum(): raise ValueError("No target PSMs were available for training.") - if not (~psms.targets).sum(): + if not (~dataset.targets).sum(): raise ValueError("No decoy PSMs were available for training.") - if len(psms.data) <= 200: + if len(dataset.data) <= 200: LOGGER.warning( "Few PSMs are available for model training (%i). " "The learned models may be unstable.", - len(psms.data), + len(dataset.data), ) # Choose the initial direction @@ -281,11 +284,11 @@ def fit(self, psms): self.feat_pass, self.best_feat, self.desc, - ) = _get_starting_labels(psms, self) + ) = _get_starting_labels(dataset, self) # Normalize Features - self.features = psms.features.columns.tolist() - norm_feat = self.scaler.fit_transform(psms.features.values) + self.features = dataset.features.columns.tolist() + norm_feat = self.scaler.fit_transform(dataset.features.values) # Shuffle order shuffled_idx = self.rng.permutation(np.arange(len(start_labels))) @@ -312,7 +315,7 @@ def fit(self, psms): scores = scores[original_idx] # Update target - target = psms._update_labels(scores, eval_fdr=self.train_fdr) + target = dataset._update_labels(scores, eval_fdr=self.train_fdr) target = target[shuffled_idx] num_passed.append((target == 1).sum()) @@ -323,6 +326,7 @@ def fit(self, psms): if num_passed[i] == 0: raise RuntimeError("Model performs worse after training.") + # If the model performs worse than what was initialized: if ( num_passed[-1] < (start_labels == 1).sum() @@ -537,13 +541,13 @@ def load_model(model_file: Path): # Private Functions ----------------------------------------------------------- -def _get_starting_labels(psms, model): +def _get_starting_labels(dataset: LinearPsmDataset, model): """ Get labels using the initial direction. Parameters ---------- - psms : a collection of PSMs + dataset : a collection of PSMs The PsmDataset object model : mokapot.Model A model object (this is likely `self`) @@ -557,7 +561,7 @@ def _get_starting_labels(psms, model): """ LOGGER.debug("Finding initial direction...") if model.direction is None and not model.is_trained: - feat_res = psms._find_best_feature(model.train_fdr) + feat_res = dataset._find_best_feature(model.train_fdr) best_feat, feat_pass, start_labels, desc = feat_res LOGGER.info( "\t- Selected feature %s with %i PSMs at q<=%g.", @@ -568,11 +572,11 @@ def _get_starting_labels(psms, model): elif model.is_trained: try: - scores = model.estimator.decision_function(psms.features.values) + scores = model.estimator.decision_function(dataset.features.values) except AttributeError: - scores = model.estimator.predict_proba(psms.features).flatten() + scores = model.estimator.predict_proba(dataset.features).flatten() - start_labels = psms._update_labels(scores, eval_fdr=model.train_fdr) + start_labels = dataset._update_labels(scores, eval_fdr=model.train_fdr) feat_pass = (start_labels == 1).sum() best_feat = model.best_feat desc = model.desc @@ -583,9 +587,9 @@ def _get_starting_labels(psms, model): ) else: - feat = psms.features[model.direction].values - desc_labels = psms._update_labels(feat, model.train_fdr, desc=True) - asc_labels = psms._update_labels(feat, model.train_fdr, desc=False) + feat = dataset.features[model.direction].values + desc_labels = dataset._update_labels(feat, model.train_fdr, desc=True) + asc_labels = dataset._update_labels(feat, model.train_fdr, desc=False) best_feat = feat desc_pass = (desc_labels == 1).sum() diff --git a/mokapot/mokapot.py b/mokapot/mokapot.py index c7647a64..bb235389 100644 --- a/mokapot/mokapot.py +++ b/mokapot/mokapot.py @@ -7,19 +7,17 @@ import sys import time import warnings -import shutil from pathlib import Path import numpy as np from . import __version__ -from .config import Config -from .parsers.pin import read_pin -from .parsers.pin_to_tsv import is_valid_tsv, pin_to_valid_tsv -from .parsers.fasta import read_fasta from .brew import brew -from .model import PercolatorModel, load_model from .confidence import assign_confidence +from .config import Config +from .model import PercolatorModel, load_model +from .parsers.fasta import read_fasta +from .parsers.pin import read_pin def main(main_args=None): @@ -44,34 +42,31 @@ def main(main_args=None): level=verbosity_dict[config.verbosity], ) logging.captureWarnings(True) + numba_logger = logging.getLogger('numba') + numba_logger.setLevel(logging.WARNING) # Suppress warning if asked for if config.suppress_warnings: warnings.filterwarnings("ignore") + # Write header logging.info("mokapot version %s", str(__version__)) logging.info("Written by William E. Fondrie (wfondrie@uw.edu) in the") logging.info( "Department of Genome Sciences at the University of Washington." ) + + # Check config parameter validity + if config.stream_confidence and config.peps_algorithm != "hist_nnls": + raise ValueError( + f"Streaming and PEPs algorithm `{config.peps_algorithm}` not " + "compatible. Use `--peps_algorithm=hist_nnls` instead.`" + ) + + # Start analysis logging.info("Command issued:") logging.info("%s", " ".join(sys.argv)) logging.info("") - - logging.info("Verify PIN format") - logging.info("=================") - if config.verify_pin: - for path_pin in config.psm_files: - with open(path_pin, 'r') as f_pin: - valid_tsv = is_valid_tsv(f_pin) - if not valid_tsv: - logging.info(f"{path_pin} invalid tsv, converting") - path_tsv = f"{path_pin}.tsv" - with open(path_pin, 'r') as f_pin: - with open(path_tsv, 'a') as f_tsv: - pin_to_valid_tsv(f_in=f_pin, f_out=f_tsv) - shutil.move(path_tsv, path_pin) - logging.info("Starting Analysis") logging.info("=================") @@ -116,7 +111,7 @@ def main(main_args=None): ) # Fit the models: - psms, models, scores, desc = brew( + models, scores = brew( datasets, model=model, test_fdr=config.test_fdr, @@ -137,21 +132,22 @@ def main(main_args=None): file_root = "" assign_confidence( - psms=psms, + datasets=datasets, max_workers=config.max_workers, - scores=scores, - descs=desc, + scores_list=scores, eval_fdr=config.test_fdr, dest_dir=config.dest_dir, file_root=file_root, prefixes=prefixes, - decoys=config.keep_decoys, + write_decoys=config.keep_decoys, + deduplication=not config.skip_deduplication, do_rollup=not config.skip_rollup, proteins=proteins, peps_error=config.peps_error, peps_algorithm=config.peps_algorithm, qvalue_algorithm=config.qvalue_algorithm, sqlite_path=config.sqlite_db_path, + stream_confidence=config.stream_confidence, ) if config.save_models: diff --git a/mokapot/parsers/fasta.py b/mokapot/parsers/fasta.py index fff23eb8..f0d0a273 100644 --- a/mokapot/parsers/fasta.py +++ b/mokapot/parsers/fasta.py @@ -7,8 +7,8 @@ import numpy as np -from ..utils import tuplize -from ..proteins import Proteins +from mokapot.utils import tuplize +from mokapot.proteins import Proteins LOGGER = logging.getLogger(__name__) diff --git a/mokapot/parsers/helpers.py b/mokapot/parsers/helpers.py index 86d73b18..d92e97d3 100644 --- a/mokapot/parsers/helpers.py +++ b/mokapot/parsers/helpers.py @@ -1,4 +1,3 @@ -from __future__ import annotations from typeguard import typechecked @@ -43,6 +42,7 @@ def find_column( def str_compare(str1, str2): return str1.lower() == str2.lower() + else: def str_compare(str1, str2): diff --git a/mokapot/parsers/pepxml.py b/mokapot/parsers/pepxml.py index ba0a4745..1aa0a921 100644 --- a/mokapot/parsers/pepxml.py +++ b/mokapot/parsers/pepxml.py @@ -10,8 +10,8 @@ import pandas as pd from lxml import etree -from .. import utils -from ..dataset import LinearPsmDataset +from mokapot import utils +from mokapot.dataset import LinearPsmDataset LOGGER = logging.getLogger(__name__) diff --git a/mokapot/parsers/pin.py b/mokapot/parsers/pin.py index 6ca23a3a..507d1915 100644 --- a/mokapot/parsers/pin.py +++ b/mokapot/parsers/pin.py @@ -5,25 +5,29 @@ import logging import warnings from pathlib import Path +from typing import List, Iterable import pandas as pd from joblib import Parallel, delayed +from typeguard import typechecked -from .helpers import find_optional_column, find_columns, find_required_column -from ..utils import ( - open_file, +from mokapot.constants import ( + CHUNK_SIZE_COLUMNS_FOR_DROP_COLUMNS, + CHUNK_SIZE_ROWS_FOR_DROP_COLUMNS, +) +from mokapot.dataset import OnDiskPsmDataset +from mokapot.parsers.helpers import ( + find_optional_column, + find_columns, + find_required_column, +) +from mokapot.tabular_data import TabularDataReader +from mokapot.utils import ( tuplize, create_chunks, convert_targets_column, flatten, ) -from ..dataset import OnDiskPsmDataset -from ..constants import ( - CHUNK_SIZE_COLUMNS_FOR_DROP_COLUMNS, - CHUNK_SIZE_ROWS_FOR_DROP_COLUMNS, -) -from ..tabular_data import TabularDataReader -from typing import List LOGGER = logging.getLogger(__name__) @@ -37,7 +41,7 @@ def read_pin( expmass_column=None, rt_column=None, charge_column=None, -): +) -> list[OnDiskPsmDataset]: """Read Percolator input (PIN) tab-delimited files. Read PSMs from one or more Percolator input (PIN) tab-delmited files, @@ -93,9 +97,8 @@ def read_pin( Returns ------- - LinearPsmDataset - A :py:class:`~mokapot.dataset.LinearPsmDataset` object containing the - PSMs from all of the PIN files. + A list of :py:class:`~mokapot.dataset.OnDiskPsmDataset` objects + containing the PSMs from all of the PIN files. """ logging.info("Parsing PSMs...") return [ @@ -239,16 +242,16 @@ def read_percolator( LOGGER.warning(" - %s", col) LOGGER.warning("Dropping features with missing values...") - _feature_columns = tuple( - [feature for feature in features if feature not in features_to_drop] - ) + _feature_columns = tuple([ + feature for feature in features if feature not in features_to_drop + ]) LOGGER.info("Using %i features:", len(_feature_columns)) for i, feat in enumerate(_feature_columns): LOGGER.debug(" (%i)\t%s", i + 1, feat) return OnDiskPsmDataset( - filename=perc_file, + perc_file, columns=columns, target_column=labels, spectrum_columns=spectra, @@ -298,27 +301,14 @@ def drop_missing_values_and_fill_spectra_dataframe( return list(na_mask[na_mask].index) -def read_file_in_chunks(file, chunk_size, use_cols): - """ - when reading in chunks an open file object is required as input to - iterate over the chunks - """ - for df in pd.read_csv( - file, - sep="\t", - chunksize=chunk_size, - usecols=use_cols, - index_col=False, - ): - yield df[use_cols] - - -def get_column_names_from_file(file): - with open_file(file) as perc: - return perc.readline().rstrip().split("\t") - - -def get_rows_from_dataframe(idx, chunk, train_psms, psms, file_idx): +@typechecked +def get_rows_from_dataframe( + idx: Iterable, + chunk: pd.DataFrame, + train_psms, + dataset: OnDiskPsmDataset, + file_idx: int, +): """ extract rows from a chunk of a dataframe @@ -330,7 +320,7 @@ def get_rows_from_dataframe(idx, chunk, train_psms, psms, file_idx): Contains subsets of dataframes that are already extracted. chunk : dataframe Subset of a dataframe. - psms : OnDiskPsmDataset + dataset : OnDiskPsmDataset A collection of PSMs. file_idx : the index of the file being searched @@ -341,7 +331,7 @@ def get_rows_from_dataframe(idx, chunk, train_psms, psms, file_idx): """ chunk = convert_targets_column( data=chunk, - target_column=psms.target_column, + target_column=dataset.target_column, ) for k, train in enumerate(idx): idx_ = list(set(train) & set(chunk.index)) @@ -355,13 +345,19 @@ def concat_and_reindex_chunks(df, orig_idx): ] -def parse_in_chunks(psms, train_idx, chunk_size, max_workers): +@typechecked +def parse_in_chunks( + datasets: list[OnDiskPsmDataset], + train_idx: list, + chunk_size: int, + max_workers: int, +) -> list[pd.DataFrame]: """ Parse a file in chunks Parameters ---------- - psms : OnDiskPsmDataset + datasets : OnDiskPsmDataset A collection of PSMs. train_idx : list of a list of a list of indexes (first level are training splits, second one is the number of input files, third level the @@ -378,16 +374,18 @@ def parse_in_chunks(psms, train_idx, chunk_size, max_workers): """ train_psms = [ - [[] for _ in range(len(train_idx))] for _ in range(len(psms)) + [[] for _ in range(len(train_idx))] for _ in range(len(datasets)) ] - for _psms, idx, file_idx in zip(psms, zip(*train_idx), range(len(psms))): - reader = TabularDataReader.from_path(_psms.filename) + for dataset, idx, file_idx in zip( + datasets, zip(*train_idx), range(len(datasets)) + ): + reader = dataset.reader file_iterator = reader.get_chunked_data_iterator( - chunk_size=chunk_size, columns=_psms.columns + chunk_size=chunk_size, columns=dataset.columns ) Parallel(n_jobs=max_workers, require="sharedmem")( delayed(get_rows_from_dataframe)( - idx, chunk, train_psms, _psms, file_idx + idx, chunk, train_psms, dataset, file_idx ) for chunk in file_iterator ) diff --git a/mokapot/parsers/pin_to_tsv.py b/mokapot/parsers/pin_to_tsv.py index cdf14a29..9a559159 100644 --- a/mokapot/parsers/pin_to_tsv.py +++ b/mokapot/parsers/pin_to_tsv.py @@ -1,14 +1,14 @@ from pathlib import Path -from io import StringIO from typing import TextIO -from unittest.mock import Mock import argparse # PIN file specification from # https://github.com/percolator/percolator/wiki/Interface#tab-delimited-file-format """ -PSMId Label ScanNr feature1name ... featureNname Peptide Proteins -DefaultDirection - - feature1weight ... featureNweight [optional] +PSMId Label ScanNr feature1name ... featureNname \ + Peptide Proteins +DefaultDirection - - feature1weight ... \ +featureNweight [optional] """ EXAMPLE_PIN = """SpecId\tLabel\tScanNr\tExpMass\tPeptide\tProteins @@ -72,7 +72,8 @@ def convert_line_pin_to_tsv( idx_protein_col : int The index of the first protein column. n_col : int - The total number of columns in the PIN file (excluding additional protein columns). + The total number of columns in the PIN file (excluding additional protein + columns). sep_column : str, optional The separator used between columns (default is "\t"). sep_protein : str, optional @@ -87,10 +88,13 @@ def convert_line_pin_to_tsv( -------- >>> header = EXAMPLE_HEADER >>> n_col, idx_protein_col = parse_pin_header_columns(header) - >>> tsv_line = convert_line_pin_to_tsv(EXAMPLE_LINE_1, n_col=n_col, idx_protein_col=idx_protein_col) - >>> tsv_line.expandtabs(4) # needed for docstring to work - 'target_0_16619_2_-1 1 16619 750.4149 K.SEFLVR.E sp|Q96QR8|PURB_HUMAN:sp|Q00577|PURA_HUMAN' - >>> tsv_line = convert_line_pin_to_tsv(EXAMPLE_LINE_2, n_col=n_col, idx_protein_col=idx_protein_col) + >>> tsv_line = convert_line_pin_to_tsv(EXAMPLE_LINE_1, n_col=n_col, + ... idx_protein_col=idx_protein_col) + >>> print(tsv_line.expandtabs(4)) # needed for docstring to work + target_0_16619_2_-1 1 16619 750.4149 K.SEFLVR.E sp|Q96QR8|PURB_HUMAN:\ +sp|Q00577|PURA_HUMAN + >>> tsv_line = convert_line_pin_to_tsv(EXAMPLE_LINE_2, n_col=n_col, + ... idx_protein_col=idx_protein_col) >>> tsv_line.expandtabs(4) # needed for docstring to work 'target_0_2025_2_-1 1 2025 751.4212 R.HTALGPR.S sp|Q9Y4H4|GPSM3_HUMAN' """ @@ -129,6 +133,7 @@ def is_valid_tsv( Examples -------- + >>> from io import StringIO >>> input = StringIO(EXAMPLE_PIN) >>> is_valid_tsv(input) False @@ -181,6 +186,8 @@ def pin_to_valid_tsv( Examples -------- + >>> from io import StringIO + >>> from unittest.mock import Mock >>> mock_input = StringIO(EXAMPLE_PIN) >>> mock_output = Mock() >>> mock_output.write = Mock() @@ -240,4 +247,4 @@ def main(): if __name__ == "__main__": - main() + main() \ No newline at end of file diff --git a/mokapot/peps.py b/mokapot/peps.py index 9aace66d..ab8a7019 100644 --- a/mokapot/peps.py +++ b/mokapot/peps.py @@ -1,110 +1,216 @@ +from __future__ import annotations + +import logging +from typing import TypeVar, Callable, Iterator + import numpy as np import scipy.stats as stats -import matplotlib.pyplot as plt +from scipy.optimize import nnls +from scipy.optimize._nnls import _nnls from triqler import qvality +from typeguard import typechecked + +from mokapot.statistics import HistData + +LOGGER = logging.getLogger(__name__) -from scipy.optimize import nnls PEP_ALGORITHM = { - "qvality": lambda scores, targets: peps_from_scores_qvality( - scores, targets, use_binary=False + "qvality": lambda scores, targets, is_tdc: peps_from_scores_qvality( + scores, targets, is_tdc, use_binary=False ), - "qvality_bin": lambda scores, targets: peps_from_scores_qvality( - scores, targets, use_binary=True + "qvality_bin": lambda scores, targets, is_tdc: peps_from_scores_qvality( + scores, targets, is_tdc, use_binary=True ), - "kde_nnls": lambda scores, targets: peps_from_scores_kde_nnls( - scores, targets + "kde_nnls": lambda scores, targets, is_tdc: peps_from_scores_kde_nnls( + scores, targets, is_tdc ), - "hist_nnls": lambda scores, targets: peps_from_scores_hist_nnls( - scores, targets + "hist_nnls": lambda scores, targets, is_tdc: peps_from_scores_hist_nnls( + scores, targets, is_tdc ), } -def peps_from_scores(scores, targets, pep_algorithm="qvality"): - """ - Compute PEPs from scores. - - :param scores: A numpy array containing the scores for each - target and decoy peptide. - :param targets: A boolean array indicating whether each peptide is a target (True) - or a decoy (False). - :param pep_algorithm: The PEPS calculation algorithm to use. Defaults to 'qvality'. - - :return: The PEPS (Posterior Error Probabilities) calculated using the specified +class PepsConvergenceError(Exception): + """Raised when nnls iterations do not converge.""" + + pass + + +@typechecked +def peps_from_scores( + scores: np.ndarray[float], + targets: np.ndarray[bool], + is_tdc: bool, + pep_algorithm: str = "qvality", +) -> np.ndarray[float]: + """Compute PEPs from scores. + + Parameters + ---------- + scores: + A numpy array containing the scores for each target and decoy peptide. + targets: + A boolean array indicating whether each peptide is a target (True) or a + decoy (False). + pep_algorithm: + The PEPS calculation algorithm to use. Defaults to 'qvality'. + is_tdc: + Scores and targets come from competition, rather than separate search. + pep_algorithm: + PEPS algorithm to use. Defaults to 'qvality'. Possible values are the + keys of `PEP_ALGORITHM`. + + Returns + ------- + array: + The PEPS (Posterior Error Probabilities) calculated using the specified algorithm. - :raises AssertionError: If the specified algorithm is unknown. - """ # noqa: E501 + Raises + ------ + ValueError + If the specified algorithm is unknown. + """ pep_function = PEP_ALGORITHM[pep_algorithm] if pep_function is not None: - return pep_function(scores, targets) + return pep_function(scores, targets, is_tdc) else: - raise AssertionError( - f"Unknown pep algorithm in peps_from_scores: {pep_algorithm}" - ) - - -def peps_from_scores_qvality(scores, targets, use_binary=False): + raise ValueError(f"Unknown pep algorithm in peps_from_scores: {pep_algorithm}") + + +@typechecked +def peps_from_scores_qvality( + scores: np.ndarray[float], + targets: np.ndarray[bool], + is_tdc: bool, + use_binary: bool = False, +) -> np.ndarray[float]: + """Compute PEPs from scores using the triqler pep algorithm. + + Parameters + ---------- + scores: + A numpy array containing the scores for each target and decoy peptide. + targets: + A boolean array indicating whether each peptide is a target (True) or a + decoy (False). + is_tdc: + Scores and targets come from competition, rather than separate search. + use_binary: + Whether to the binary (Percolator) version of qvality (True), or the + Python (triqler) version (False). Defaults to False. If True, the + compiled `qvality` binary must be on the shell search path. + + Returns + ------- + array: + A numpy array containing the posterior error probabilities (PEPs) + calculated using the qvality method. The PEPs are calculated based on + the provided scores and targets, and are returned in the same order as + the targets array. """ - Compute PEPs from scores using the triqler pep algorithm. - - :param scores: A numpy array containing the scores for each target and decoy peptide. - :param targets: A boolean array indicating whether each peptide is a target (True) or a decoy (False). - :param use_binary: Whether to the binary (Percolator) version of qvality (True), or the Python (triqler) version - (False). Defaults to False. If True, the compiled `qvality` binary must be on the shell search path. - :return: A numpy array containing the posterior error probabilities (PEPs) calculated using the qvality - method. The PEPs are calculated based on the provided scores and targets, and are returned in the same order - as the targets array. - """ # noqa: E501 - # todo: this method should contain the logic of sorting the scores - # (and the returned peps afterwards) - # todo: should also do the error handling, since getQvaluesFromScores may - # throw a SystemExit exception qvalues_from_scores = ( qvality.getQvaluesFromScoresQvality if use_binary else qvality.getQvaluesFromScores ) - old_verbosity, qvality.VERB = qvality.VERB, 0 - _, peps = qvalues_from_scores( - scores[targets], - scores[~targets], - includeDecoys=True, - includePEPs=True, - tdcInput=False, - ) - qvality.VERB = old_verbosity + + # Triqler returns the peps for reverse sorted scores, so we sort the scores + # ourselves and later sort them back + index = np.argsort(scores)[::-1] + scores_sorted, targets_sorted = scores[index], targets[index] + + try: + old_verbosity, qvality.VERB = qvality.VERB, 0 + _, peps_sorted = qvalues_from_scores( + scores_sorted[targets_sorted], + scores_sorted[~targets_sorted], + includeDecoys=True, + includePEPs=True, + tdcInput=is_tdc, + ) + if use_binary: + peps_sorted = np.array(peps_sorted, dtype=float) + + inverse_idx = np.argsort(index) + peps = peps_sorted[inverse_idx] + except SystemExit as msg: + if "no decoy hits available for PEP calculation" in str(msg): + peps = np.zeros_like(scores) + else: + raise + finally: + qvality.VERB = old_verbosity + return peps -def monotonize_simple(x, ascending): +_AnyArray = TypeVar("_AnyArray") + + +@typechecked +def monotonize_simple( + x: _AnyArray, ascending: bool, reverse: bool = False +) -> _AnyArray: + """Monotonizes the input array `x` in either ascending or descending order + beginning from the start of the array. + + Parameters + ---------- + x: + Input array to be monotonized. + ascending: + Specifies whether to monotonize in ascending order (`True`) or + descending order (`False`). Direction is always w.r.t. to the start of + the array, independently of `reverse`. + reverse: + Specify whether the process should run from the start (`False`) or the + end (`True`) of the array. Defaults to `False`. + + Returns + ------- + array: + Monotonized array `x` """ - Monotonizes the input array `x` in either ascending or descending order beginning from the start of the array. + if reverse: + return monotonize_simple(x[::-1], not ascending, False)[::-1] - :param x: Input array to be monotonized. - :param ascending: Specifies whether to monotonize in ascending order (`True`) or descending order (`False`). - :return: Monotonized array `x` - """ # noqa: E501 if ascending: return np.maximum.accumulate(x) else: return np.minimum.accumulate(x) -def monotonize(x, ascending, simple_averaging=False): +@typechecked +def monotonize( + x: np.ndarray[float], ascending: bool, simple_averaging: bool = False +) -> np.ndarray[float]: + """Monotonizes the input array `x` in either ascending or descending order + averaging over both directions. + + Note: it makes a difference whether you start with monotonization from the + start or the end of the array. + + Parameters + ---------- + x: + Input array to be monotonized. + ascending: + Specifies whether to monotonize in ascending order (`True`) or + descending order (`False`). + simple_averaging: + Specifies whether to use a simple average (`True`) or weighted average + (`False`) when computing the average of the monotonized arrays. Only + used if `average` is `True`. Default is `False`. Note: the weighted + average tries to minimize the L2 difference in the change between the + original and the returned arrays. + + Returns + ------- + array: + Monotonized array `x` based on the specified parameters. """ - Monotonizes the input array `x` in either ascending or descending order averaging over both directions. Note: it - makes a difference whether you start with monotonization from the start or the end of the array. - - :param x: Input array to be monotonized. - :param ascending: Specifies whether to monotonize in ascending order (`True`) or descending order (`False`). - :param simple_averaging: Specifies whether to use a simple average (`True`) or weighted average (`False`) when - computing the average of the monotonized arrays. Only used if `average` is `True`. Default is `False`. - Note: the weighted average tries to minimize the L2 difference in the change between the original and the - returned arrays. - :return: Monotonized array `x` based on the specified parameters. - """ # noqa: E501 x1 = monotonize_simple(x, ascending) if np.all(x1 == x): return x # nothing to do here @@ -117,15 +223,34 @@ def monotonize(x, ascending, simple_averaging=False): return alpha * x1 + (1 - alpha) * x2 -def monotonize_nnls(x, w=None, ascending=True): +@typechecked +def monotonize_nnls( + x: np.ndarray[float], + w: np.ndarray[float] | None = None, + ascending: bool = True, +) -> np.ndarray[float]: + """Monotonizes a given array `x` using non-negative least squares (NNLS) + optimization. + + The returned array is the monotone array `y` that minimized `x-y` in the + L2-norm. The of all monotone arrays `y` is such that `x-y` has minimum + mean squared error (MSE). + + Parameters + ---------- + x: + numpy array to be monotonized. + w: + numpy array containing weights. If None, equal weights are assumed. + ascending: + Boolean indicating whether the monotonized array should be in ascending + order. + + Returns + ------- + array: + The monotonized array. """ - Monotonizes a given array `x` using non-negative least squares (NNLS) optimization. - - :param x: numpy array to be monotonized. - :param w: numpy array containing weights. If None, equal weights are assumed. - :param ascending: Boolean indicating whether the monotonized array should be in ascending order. - :return: Monotonized array. - """ # noqa: E501 if not ascending: # If descending, just return the reversed output of the algo with # reversed inputs. @@ -154,35 +279,66 @@ def monotonize_nnls(x, w=None, ascending=True): return xm -def estimate_pi0_by_slope(target_pdf, decoy_pdf, threshold=0.9): +def estimate_pi0_by_slope( + target_pdf: np.ndarray[float], + decoy_pdf: np.ndarray[float], + threshold: float = 0.9, +): + r"""Estimate pi0 using the slope of decoy vs target PDFs. + + The idea is that :math:`f_T(s) = \pi_0 f_D(s) + (1-\pi_0) f_{TT}(s)` and + that for small scores `s` and a scoring function that sufficiently + separates targets and decoys (or false targets) it holds that + :math:`f_T(s) \simeq \pi_0 f_D(s)`. + The algorithm works by determining the maximum of the decoy distribution + and then estimating the slope of the target vs decoy density for all scores + left of 90% of the maximum of the decoy distribution. + + Parameters + ---------- + target_pdf: + An estimate of the target PDF. + decoy_pdf: + An estimate of the decoy PDF. + threshold: + The threshold for selecting decoy PDF values (default is 0.9). + + Returns + ------- + float: + The estimated value of pi0. """ - Estimate pi0 using the slope of decoy vs target PDFs. - The idea is that :math:`f_T(s) = \pi_0 f_D(s) + (1-\pi_0) f_{TT}(s)` and that for small scores `s` and a scoring - function that sufficiently separates targets and decoys (or false targets) it holds that :math:`f_T(s) \simeq \pi_0 f_D(s)`. - The algorithm works by determining the maximum of the decoy distribution and then estimating the slope of the target - vs decoy density for all scores left of 90% of the maximum of the decoy distribution. - - :param target_pdf: An estimate of the target PDF. - :param decoy_pdf: An estimate of the decoy PDF. - :param threshold: The threshold for selecting decoy PDF values (default is 0.9). - :return: The estimated value of pi0. - - """ # noqa: E501 max_decoy = np.max(decoy_pdf) last_index = np.argmax(decoy_pdf >= threshold * max_decoy) pi0_est, _ = np.polyfit(decoy_pdf[:last_index], target_pdf[:last_index], 1) return max(pi0_est, 1e-10) -def pdfs_from_scores(scores, targets, num_eval_scores=500): +@typechecked +def pdfs_from_scores( + scores: np.ndarray[float], + targets: np.ndarray[bool], + num_eval_scores: int = 500, +) -> tuple[np.ndarray[float], np.ndarray[float], np.ndarray[float]]: + """Compute target and decoy probability density functions (PDFs) from + scores using kernel density estimation (KDE). + + Parameters + ---------- + scores: + A numpy array containing the scores for each target and decoy peptide. + targets: + A boolean array indicating whether each peptide is a target (True) or a + decoy (False). + num_eval_scores: + Number of evaluation scores to compute in the PDFs. Defaults to 500. + + Returns + ------- + tuple: + A tuple containing the evaluation scores, the target PDF, and the decoy + PDF at those scores. """ - Compute target and decoy probability density functions (PDFs) from scores using kernel density estimation (KDE). - - :param scores: A numpy array containing the scores for each target and decoy peptide. - :param targets: A boolean array indicating whether each peptide is a target (True) or a decoy (False). - :param num_eval_scores: Number of evaluation scores to compute in the PDFs. Defaults to 500. - :return: A tuple containing the evaluation scores, the target PDF, and the decoy PDF at those scores. - """ # noqa: E501 # Compute score range and evaluation points min_score = min(scores) max_score = max(scores) @@ -198,27 +354,56 @@ def pdfs_from_scores(scores, targets, num_eval_scores=500): return eval_scores, target_pdf, decoy_pdf +@typechecked def peps_from_scores_kde_nnls( - scores, targets, num_eval_scores=500, pi0_estimation_threshold=0.9 -): - """ - :param scores: A numpy array containing the scores for each target and decoy peptide. - :param targets: A boolean array indicating whether each peptide is a target (True) or a decoy (False). - :param num_eval_scores: The number of evaluation scores to be computed. Default is 500. - :param pi0_estimation_threshold: The threshold for pi0 estimation. Default is 0.9. - :return: The estimated probabilities of target being incorrect (peps) for the given scores. - - This method computes the estimated probabilities of target being incorrect (peps) based on the given scores and targets. It uses the following steps: - 1. Compute evaluation scores, target probability density function (PDF), and decoy probability density function - (PDF) evaluated at the given scores. - 2. Estimate pi0 and the number of correct targets using the target PDF, decoy PDF, and pi0EstThresh. - 3. Calculate the number of correct targets by subtracting the decoy PDF multiplied by pi0Est from the target PDF, - and clip it to ensure non-negative values. - 4. Estimate peps by dividing the number of correct targets by the target PDF, and clip the result between 0 and 1. + scores: np.ndarray[float], + targets: np.ndarray[bool], + is_tdc: bool, + num_eval_scores: int = 500, + pi0_estimation_threshold: float = 0.9, +) -> np.ndarray[float]: + """Estimate peps from scores using density estimates and monotonicity. + + This method computes the estimated probabilities of target being + incorrect (peps) based on the given scores and targets. It uses the + following steps: + + 1. Compute evaluation scores, target probability density function + (PDF), and decoy probability density function evaluated at the + given scores. + 2. Estimate pi0 and the number of correct targets using the target + PDF, decoy PDF, and pi0EstThresh. + 3. Calculate the number of correct targets by subtracting the decoy + PDF multiplied by pi0Est from the target PDF, and clip it to + ensure non-negative values. + 4. Estimate peps by dividing the number of correct targets by the + target PDF, and clip the result between 0 and 1. 5. Monotonize the pep estimates. - 6. Linearly interpolate the pep estimates from the evaluation scores to the given scores of interest. - 7. Return the estimated probabilities of target being incorrect (peps). - """ # noqa: E501 + 6. Linearly interpolate the pep estimates from the evaluation + scores to the given scores of interest. + 7. Return the estimated probabilities of target being incorrect + (peps). + + Parameters + ---------- + scores: + A numpy array containing the scores for each target and decoy peptide. + targets: + A boolean array indicating whether each peptide is a target (True) or + a decoy (False). + is_tdc: + Scores and targets come from competition, rather than separate search. + num_eval_scores: + The number of evaluation scores to be computed. Default is 500. + pi0_estimation_threshold: + The threshold for pi0 estimation. Default is 0.9. + + Returns + ------- + array: + The estimated probabilities of target being incorrect (peps) for the + given scores. + """ # Compute evaluation scores, and target and decoy pdfs # (evaluated at given scores) @@ -226,16 +411,17 @@ def peps_from_scores_kde_nnls( scores, targets, num_eval_scores ) - # Estimate pi0 and estimate number of correct targets - pi0_est = estimate_pi0_by_slope( - target_pdf, decoy_pdf, pi0_estimation_threshold - ) + if is_tdc: + factor = (~targets).sum() / targets.sum() + else: + # Estimate pi0 and estimate number of correct targets + factor = estimate_pi0_by_slope(target_pdf, decoy_pdf, pi0_estimation_threshold) - correct = target_pdf - decoy_pdf * pi0_est + correct = target_pdf - decoy_pdf * factor correct = np.clip(correct, 0, None) # Estimate peps from #correct targets, clip it - pepEst = np.clip(1.0 - correct / target_pdf, 0, 1) + pepEst = np.clip(1.0 - correct / target_pdf, 0.0, 1.0) # Now monotonize using the NNLS algo putting more weight on areas with high # target density @@ -244,52 +430,85 @@ def peps_from_scores_kde_nnls( # Linearly interpolate the pep estimates from the eval points to the scores # of interest. peps = np.interp(scores, eval_scores, pepEst) - peps = np.clip(peps, 0, 1) + peps = np.clip(peps, 0.0, 1.0) return peps -def fit_nnls(n, k, ascending=True, *, weight_exponent=1, erase_zeros=False): +def fit_nnls(n, k, ascending=True, *, weight_exponent=-1.0, erase_zeros=False): + """Do monotone nnls fit on binomial model. + + This method performs a non-negative least squares (NNLS) fit on given + input parameters 'n' and 'k', such that `k[i]` is close to `p[i] * n[i]` in + a weighted least squared sense (weight is determined by + `n[i] ** weightExponent`) and the `p[i]` are monotone. + + Note: neither `n` nor `k` need to integer valued or positive nor does `k` + need to be between `0` and `n`. + + Parameters + ---------- + n: + The input array of length N + k: + The input array of length N + ascending: + Optional bool (Default value = True). Whether the result should be + monotone ascending or descending. + weight_exponent: + Optional (Default value = -1.0). The weight exponent to use. + erase_zeros: + Optional (Default value = False). Whether 0s in `n` should be erased + prior to fitting or not. + + Returns + ------- + array: + The monotonically increasing or decreasing array `p` of length N. + """ - This method performs a non-negative least squares (NNLS) fit on given input parameters 'n' and 'k', such that - `k[i]` is close to `p[i] * n[i]` in a weighted least squared sense (weight is determined by `n[i] ** weightExponent`) - and the `p[i]` are monotone. - - Note: neither `n` nor `k` need to integer valued or positive nor does `k` need to be between `0` and `n`. - - Parameters: - - n: The input array of length N. - - k: The input array of length N. - - ascending: (optional) A boolean value indicating whether the output array should be in ascending order. Default value is True. - - weight_exponent: (optional) The exponent to be used for the weight array. Default value is 1. - - erase_zeros: (optional) If True, rows corresponding to n==0 will be erased from the system of equations, - whereas if False (default), there will be equations inserted that try to minimize the jump in probabilities, - i.e. distribute the jumps evenly - Returns: - - p: The monotonically increasing or decreasing array of length N. - - """ # noqa: E501 - # For the basic idea of this algorithm, see the `monotonize_nnls` - # algorithm. - # This is more or less the same, just with - # JSPP Q: With what?? + # For the basic idea of this algorithm (i.e. monotonize under constraints), + # see the `monotonize_nnls` algorithm. This is more or less the same, just + # that the functional to be minimized is different here. if not ascending: - return fit_nnls(n[::-1], k[::-1])[::-1] + n = n[::-1] + k = k[::-1] + N = len(n) D = np.diag(n) A = D @ np.tril(np.ones((N, N))) - w = n ** (0.5 * weight_exponent) + w = np.zeros_like(n, dtype=float) + w[n != 0] = n[n != 0] ** (0.5 * weight_exponent) W = np.diag(w) - - nz = (n == 0).nonzero()[0] - if len(nz) > 0: - if not erase_zeros: - A[nz, nz] = 1 - A[nz, np.minimum(nz + 1, N - 1)] = -1 - w[nz] = 1 - k[nz] = 0 - W = np.diag(w) - else: - W = np.delete(W, nz, axis=0) + R = np.eye(N) + + zeros = (n == 0).nonzero()[0] + if len(zeros) > 0: + A[zeros, zeros] = 1 + A[zeros, np.minimum(zeros + 1, N - 1)] = -1 + w[zeros] = 1 + k[zeros] = 0 + W = np.diag(w) + + if erase_zeros: + # The following lines remove variables that will end up being the + # same (matrix R) as well as equations that are essentially zero on + # both sides U). However, since this is a bit tricky, and difficult + # to maintain and does not seem to lower the condition of the + # matrix substantially, it is only activated on demand and left + # here more for further reference, in case it will be needed in the + # future. + nnz = n != 0 + nnz[-1] = True + nnz2 = np.insert(nnz, 0, True)[:-1] + + def make_perm_mat(rows, cols): + M = np.zeros((np.max(rows) + 1, np.max(cols) + 1)) + M[rows, cols] = 1 + return M + + R = make_perm_mat(np.arange(N), nnz2.cumsum() - 1) + U = make_perm_mat(np.arange(sum(nnz)), nnz.nonzero()[0]) + W = U @ W # The default tolerance of nnls is too low, leading sometimes to # non-convergent iterations and subsequent failures. A good tolerance @@ -301,51 +520,177 @@ def fit_nnls(n, k, ascending=True, *, weight_exponent=1, erase_zeros=False): # non-convergence and b) is fitting for the typical condition numbers and # values of k seen in experiments. tol = 1e-7 - d, _ = nnls(W @ A, W @ k, atol=tol) - p = np.cumsum(d) - return p + d, _, mode = _nnls(W @ A @ R, W @ k, tol=tol) + if mode != 1: + LOGGER.debug("\t - Warning: nnls went into loop. Taking last solution.") + p = np.cumsum(R @ d) + + if not ascending: + return p[::-1] + else: + return p + + +@typechecked +class TDHistData: + """ """ + + targets: HistData + decoys: HistData + + def __init__( + self, + bin_edges: np.ndarray[float], + target_counts: np.ndarray[int], + decoy_counts: np.ndarray[int], + ): + self.targets = HistData(bin_edges, target_counts) + self.decoys = HistData(bin_edges, decoy_counts) + + @staticmethod + def from_scores_targets( + bin_edges: np.ndarray[float], + scores: np.ndarray[float], + targets: np.ndarray[bool], + ) -> TDHistData: + """Create TDHistData object from scores and targets.""" + return hist_data_from_scores(scores, targets, bin_edges) + + @staticmethod + def from_score_target_iterator( + bin_edges: np.ndarray[float], score_target_iterator: Iterator + ) -> TDHistData: + """Create TDHistData from an iterator over scores and targets.""" + return hist_data_from_iterator(score_target_iterator, bin_edges) + + def as_counts( + self, + ) -> tuple[np.ndarray[float], np.ndarray[int], np.ndarray[int]]: + """Return bin centers and target and decoy counts.""" + return ( + self.targets.bin_centers, + self.targets.counts, + self.decoys.counts, + ) + + def as_densities( + self, + ) -> tuple[np.ndarray[float], np.ndarray[float], np.ndarray[float]]: + """Return bin centers and target and decoy densities.""" + return ( + self.targets.bin_centers, + self.targets.density, + self.decoys.density, + ) -def hist_data_from_scores(scores, targets, bins=None, density=False): +@typechecked +def hist_data_from_scores( + scores: np.ndarray[float], + targets: np.ndarray[bool], + bins: np.ndarray[float] | str | None = None, +) -> TDHistData: + """Generate histogram data from scores and target/decoy information. + + Parameters + ---------- + scores: + A numpy array containing the scores for each target and decoy peptide. + targets: + A boolean array indicating whether each peptide is a target (True) or a + decoy (False). + bins: + Either: The number of bins to use for the histogram. Or: the edges of + the bins to take. Or: None, which lets numpy determines the bins from + all scores (which is the default). + + Returns + ------- + TDHistData: + A `TDHistData` object, encapsulating the histogram data. """ - Generate histogram data from scores and target/decoy information. - - :param scores: A numpy array containing the scores for each target and decoy peptide. - :param targets: A boolean array indicating whether each peptide is a target (True) or a decoy (False). - :param bins: The number of bins to use for the histogram. Defaults to None (which lets numpy determines the bins from all scores). - :param density: If True, the histogram is normalized to form a probability density. Defaults to False. - - :return: A tuple of three numpy arrays: the evaluation scores, target counts, and decoy counts. - The evaluation scores are the midpoint of each bin. - The target counts represent the number of target scores in each bin. - The decoy counts represent the number of decoy scores in each bin. - """ # noqa: E501 - if bins is None: - bins = np.histogram_bin_edges(scores, bins="auto") - target_counts, _ = np.histogram( - scores[targets], bins=bins, density=density - ) - decoy_counts, _ = np.histogram( - scores[~targets], bins=bins, density=density - ) - eval_scores = 0.5 * (bins[:-1] + bins[1:]) - return eval_scores, target_counts, decoy_counts + if isinstance(bins, np.ndarray): + bin_edges = bins + else: + bin_edges = np.histogram_bin_edges(scores, bins=bins or "scott") + + target_counts, _ = np.histogram(scores[targets], bins=bin_edges) + decoy_counts, _ = np.histogram(scores[~targets], bins=bin_edges) + + return TDHistData(bin_edges, target_counts, decoy_counts) -def estimate_trials_and_successes(decoy_counts, target_counts, restrict=True): +@typechecked +def hist_data_from_iterator( + score_target_iterator, bin_edges: np.ndarray[float] +) -> TDHistData: + """Generate histogram data from scores and target/decoy information + provided by an iterator. + + This is for streaming algorithms. + + Parameters + ---------- + score_target_iterator: + An iterator that yields scores and target/decoy information. For each + iteration a tuple consisting of a score array and a corresponding + target must be yielded. + bin_edges: + The bins to use for the histogram. Must be provided (since they cannot + be determined at the start of the algorithm). + + Returns + ------- + TDHistData: + A `TDHistData` object, encapsulating the histogram data. """ - Estimate trials/successes (assuming a binomial model) from decoy and target counts. - :param decoy_counts: A numpy array containing the counts of decoy occurrences (histogram). - :param target_counts: A numpy array containing the counts of target occurrences (histogram). - :param restrict: A boolean indicating whether to restrict the estimated trials and successes per bin. - If True, the estimated values will be bounded by a minimum of 0 and a maximum of the corresponding target count. - If False, the estimated values will be unrestricted. - :return: A tuple (n, k) where n is a numpy array representing the estimated trials per bin, and k is a numpy array representing the estimated successes per bin. + target_counts = np.zeros(len(bin_edges) - 1, dtype=int) + decoy_counts = np.zeros(len(bin_edges) - 1, dtype=int) + for scores, targets in score_target_iterator: + target_counts += np.histogram(scores[targets], bins=bin_edges)[0] + decoy_counts += np.histogram(scores[~targets], bins=bin_edges)[0] + + return TDHistData(bin_edges, target_counts, decoy_counts) - """ # noqa: E501 - # Find correction factor (equivalent to pi0 for target/decoy density) - factor = estimate_pi0_by_slope(target_counts, decoy_counts) + +@typechecked +def estimate_trials_and_successes( + decoy_counts: np.ndarray[int], + target_counts: np.ndarray[int], + is_tdc: bool, + restrict: bool = True, +): + """Estimate trials/successes (assuming a binomial model) from decoy and + target counts. + + Parameters + ---------- + decoy_counts: + A numpy array containing the counts of decoy occurrences (histogram). + target_counts: + A numpy array containing the counts of target occurrences (histogram). + is_tdc: + Scores and targets come from competition, rather than separate search. + restrict: + A boolean indicating whether to restrict the estimated trials and + successes per bin. If True, the estimated values will be bounded by a + minimum of 0 and a maximum of the corresponding target count. If False, + the estimated values will be unrestricted. + + Returns + ------- + tuple: + A tuple (n, k) where n is a numpy array representing the estimated + trials per bin, and k is a numpy array representing the estimated + successes per bin. + """ + + if is_tdc: + factor = 1 + else: + # Find correction factor (equivalent to pi0 for target/decoy density) + factor = estimate_pi0_by_slope(target_counts, decoy_counts) # Estimate trials and successes per bin if restrict: @@ -358,151 +703,104 @@ def estimate_trials_and_successes(decoy_counts, target_counts, restrict=True): return n, k -def peps_from_scores_hist_nnls(scores, targets, scale_to_one=True): +@typechecked +def peps_from_scores_hist_nnls( + scores: np.ndarray[float], + targets: np.ndarray[bool], + is_tdc: bool, + scale_to_one: bool = False, + weight_exponent: float = -1.0, +): + """Calculate the PEP (Posterior Error Probability) estimates from scores + and targets using the NNLS (Non-negative Least Squares) method. + + The algorithm follows the steps outlined below: + 1. Define joint bins for targets and decoys. + 2. Estimate the number of trials and successes inside each bin. + 3. Perform a monotone fit by minimizing the objective function + || n - diag(p) * k || with weights n over monotone descending p. + 4. If scaleToOne is True and the first element of the pepEst array is + less than 1, scale the pepEst array by dividing it by the first + element. + 5. Linearly interpolate the pep estimates from the evaluation points to + the scores of interest. + 6. Clip the interpolated pep estimates between 0 and 1 in case they + went slightly out of bounds. + 7. Return the interpolated and clipped PEP estimates. + + Parameters + ---------- + scores: + numpy array containing the scores of interest. + targets: + numpy array containing the target values corresponding to each score. + is_tdc: + Scores and targets come from competition, rather than separate search. + scale_to_one: + Boolean value indicating whether to scale the PEP estimates to 1 for + small score values. Default is False. + + Returns + ------- + array: + Array of PEP estimates at the scores of interest. """ - :param scores: numpy array containing the scores of interest. - :param targets: numpy array containing the target values corresponding to each score. - :param scale_to_one: Boolean value indicating whether to scale the PEP estimates to 1 for small score values. Default is True. - :return: Array of PEP estimates at the scores of interest. - - This method calculates the PEP (Posterior Error Probability) estimates from scores and targets using the NNLS - (Non-negative Least Squares) method. The algorithm follows the steps outlined below. - - 1. Define joint bins for targets and decoys. - 2. Estimate the number of trials and successes inside each bin. - 3. Perform a monotone fit by minimizing the objective function || n - diag(p) * k || with weights n over monotone descending p. - 4. If scaleToOne is True and the first element of the pepEst array is less than 1, scale the pepEst array by dividing it by the first element. - 5. Linearly interpolate the pep estimates from the evaluation points to the scores of interest. - 6. Clip the interpolated pep estimates between 0 and 1 in case they went slightly out of bounds. - 7. Return the interpolated and clipped PEP estimates. - """ # noqa: E501 - # Define joint bins for targets and decoys - eval_scores, target_counts, decoy_counts = hist_data_from_scores( - scores, targets + + hist_data = hist_data_from_scores(scores, targets) + peps_func = peps_func_from_hist_nnls( + hist_data, is_tdc, scale_to_one, weight_exponent ) + return peps_func(scores) + + +@typechecked +def peps_func_from_hist_nnls( + hist_data: TDHistData, + is_tdc: bool, + scale_to_one: bool = False, + weight_exponent: float = -1.0, +) -> Callable[[np.ndarray[float]], np.ndarray[float]]: + """Compute a function that calculates the PEP (Posterior Error Probability) + estimates from scores and targets using the NNLS (Non-negative Least + Squares) method. + + For a description see `peps_from_scores_hist_nnls`. + + Parameters + ---------- + hist_data: + Histogram data as `TDHistData` object. + is_tdc: + Scores and targets come from competition, rather than separate search. + scale_to_one: Scale the result if the maximum PEP is smaller than 1. + (Default value = False) + weight_exponent: + The weight exponent for the `fit_nnls` fit (see there, default 1). + + Returns + ------- + function: + A function that computes PEPs, given scores as input. Input must be an + numpy array. + """ + # Define joint bins for targets and decoys + eval_scores, target_counts, decoy_counts = hist_data.as_counts() + n, k = estimate_trials_and_successes( - decoy_counts, target_counts, restrict=False + decoy_counts, target_counts, is_tdc, restrict=False ) # Do monotone fit, minimizing || n - diag(p) * k || with weights n over # monotone descending p - pep_est = fit_nnls(n, k, ascending=False) + try: + pep_est = fit_nnls(n, k, ascending=False, weight_exponent=weight_exponent) + except RuntimeError as e: + raise PepsConvergenceError from e if scale_to_one and pep_est[0] < 1: pep_est = pep_est / pep_est[0] - # Linearly interpolate the pep estimates from the eval points to the - # scores of interest (keeping monotonicity) clip in case we went - # slightly out of bounds - return np.clip(np.interp(scores, eval_scores, pep_est), 0, 1) - - -def peps_from_scores_hist_direct(scores, targets): - """ - Compute a PEP estimate directly from the binned scores (histogram) without any monotonization, just restricting - peps between 0 and 1. - - :param scores: A numpy array of scores. - :param targets: A numpy array of target labels corresponding to the scores. - :return: A numpy array of estimated PEP (Posterior Error Probability) values based on the scores. - """ # noqa: E501 - # Define joint bins for targets and decoys - eval_scores, target_counts, decoy_counts = hist_data_from_scores( - scores, targets - ) - - # Estimate number of trials and successes per bin - n, k = estimate_trials_and_successes(decoy_counts, target_counts) - - # Direct "no-frills" estimation of the PEP without monotonization - # or anything else - pep_est = k / n - - # Linearly interpolate the pep estimates from the eval points to the - # scores of interest (keeping monotonicity) clip in case we went slightly - # out of bounds - return np.clip(np.interp(scores, eval_scores, pep_est), 0, 1) - - -def plot_peps( - scores, - targets, - ax=None, - peps_true=None, - show_pdfs=True, - show_hists=True, - show_qvality=True, - show_kde_nnls=True, - show_hist_nnls=True, - show_peps_direct=True, -): - if ax is None: - plt.cla() - plt.clf() - ax = plt.gca() - ax.clear() - if show_pdfs: - eval_scores, target_pdf, decoy_pdf = pdfs_from_scores( - scores, targets, 200 - ) - ax.plot(eval_scores, target_pdf, label="Target PDF") - ax.plot(eval_scores, decoy_pdf, label="Decoy PDF") - if show_hists: - bins = np.histogram_bin_edges(scores, bins="auto") - ax.hist(scores[~targets], bins=bins, density=True, color=("C1", 0.5)) - ax.hist(scores[targets], bins=bins, density=True, color=("C0", 0.5)) - if show_qvality: - peps_qv = ( - peps_from_scores_qvality(scores, targets, use_binary=False) + 0.01 - ) - ax.plot(scores, peps_qv, label="Mokapot (triqler)") - if show_kde_nnls: - peps_km = peps_from_scores_kde_nnls(scores, targets, 200) - ax.plot(scores, peps_km, label="KDEnnls") - if show_hist_nnls: - peps_bg = peps_from_scores_hist_nnls(scores, targets) - ax.plot(scores, peps_bg, label="HistNNLS") - if show_qvality: - import shutil - - if shutil.which("qvality") is not None: - peps_qv = peps_from_scores_qvality( - scores, targets, use_binary=True - ) - ax.plot(scores, peps_qv, label="QVality C++") - if show_peps_direct: - peps_bg = peps_from_scores_hist_direct(scores, targets) - ax.plot( - scores, peps_bg, label="Direct", color=("k", 0.5), linewidth=0.5 - ) - if peps_true is not None: - ax.plot( - scores, peps_true, color=("k", 0.5), linestyle="--", label="Truth" - ) - ax.set_xlabel("Score") - ax.set_ylabel("Prob.") - ax.legend(loc="lower right") - - bin_edges, target_counts, decoy_counts = hist_data_from_scores( - scores, targets - ) - delta = bin_edges[1] - bin_edges[0] - - from mpl_toolkits.axes_grid1.inset_locator import inset_axes - - inset_ax = inset_axes(ax, loc="upper right", width="30%", height="30%") - # Plot some data in the inset subplot - if show_pdfs: - inset_ax.plot(decoy_pdf, target_pdf, color="C0", label="pdf") - inset_ax.plot( - decoy_counts / (delta * sum(decoy_counts)), - target_counts / (delta * sum(target_counts)), - color="C1", - label="hist", - ) - # Set labels, legend, grid, and aspect - inset_ax.set_xlabel("decoy") - inset_ax.set_ylabel("target") - inset_ax.grid(True) - inset_ax.set_aspect("equal") - return ax + # Linearly interpolate the pep estimates from the eval points to the scores + # of interest (keeping monotonicity) clip in case we went slightly out of + # bounds + return lambda scores: np.clip(np.interp(scores, eval_scores, pep_est), 0, 1) diff --git a/mokapot/picked_protein.py b/mokapot/picked_protein.py index 13faf1d4..10e14133 100644 --- a/mokapot/picked_protein.py +++ b/mokapot/picked_protein.py @@ -6,8 +6,8 @@ import logging import pandas as pd -from .peptides import match_decoy -from . import utils +from mokapot import utils +from mokapot.peptides import match_decoy LOGGER = logging.getLogger(__name__) diff --git a/mokapot/qvalues.py b/mokapot/qvalues.py index e0c96da0..c9131921 100644 --- a/mokapot/qvalues.py +++ b/mokapot/qvalues.py @@ -1,25 +1,26 @@ """ This module estimates q-values. """ - -from __future__ import annotations - import numpy as np import numba as nb from typeguard import typechecked +from typing import Callable -from .peps import ( +from mokapot.peps import ( peps_from_scores_hist_nnls, monotonize_simple, hist_data_from_scores, estimate_pi0_by_slope, + TDHistData, ) QVALUE_ALGORITHM = { "tdc": lambda scores, targets: tdc(scores, targets, desc=True), - "from_peps": lambda scores, targets: qvalues_from_peps(scores, targets), + "from_peps": lambda scores, targets: qvalues_from_peps( + scores, targets, is_tdc=True + ), "from_counts": lambda scores, targets: qvalues_from_counts( - scores, targets + scores, targets, is_tdc=True ), } @@ -28,8 +29,7 @@ def tdc( scores: np.ndarray[float], target: np.ndarray[bool], desc: bool = True ): - """ - Estimate q-values using target decoy competition. + """Estimate q-values using target decoy competition. Estimates q-values using the simple target decoy competition method. For set of target and decoy PSMs meeting a specified score threshold, @@ -54,14 +54,12 @@ def tdc( ---------- scores : numpy.ndarray of float A 1D array containing the score to rank by - target : numpy.ndarray of bool A 1D array indicating if the entry is from a target or decoy hit. This should be boolean, where `True` indicates a target and `False` indicates a decoy. `target[i]` is the label for `metric[i]`; thus `target` and `metric` should be of equal length. - desc : bool Are higher scores better? `True` indicates that they are, `False` indicates that they are not. @@ -146,8 +144,7 @@ def tdc( @nb.njit def _fdr2qvalue(fdr, num_total, met, indices): - """ - Quickly turn a list of FDRs to q-values. + """Quickly turn a list of FDRs to q-values. All of the inputs are assumed to be sorted. @@ -155,13 +152,10 @@ def _fdr2qvalue(fdr, num_total, met, indices): ---------- fdr : numpy.ndarray A vector of all unique FDR values. - num_total : numpy.ndarray A vector of the cumulative number of PSMs at each score. - met : numpy.ndarray A vector of the scores for each PSM. - indices : tuple of numpy.ndarray Tuple where the vector at index i indicates the PSMs that shared the unique FDR value in `fdr`. @@ -192,51 +186,90 @@ def _fdr2qvalue(fdr, num_total, met, indices): return qvals -def qvalues_from_scores(scores, targets, qvalue_algorithm="tdc"): - """ - Compute q-values from scores. +@typechecked +def qvalues_from_scores( + scores: np.ndarray[float], + targets: np.ndarray[bool], + qvalue_algorithm: str = "tdc", +): + """Compute q-values from scores. - :param scores: A numpy array containing the scores for each target and decoy peptide. - :param targets: A boolean array indicating whether each peptide is a target (True) or a decoy (False). - :param qvalue_algorithm: The q-value calculation algorithm to use. Defaults to 'tdc' (mokapot builtin). + Parameters + ---------- + scores: + A numpy array containing the scores for each target and decoy peptide. + targets: + A boolean array indicating whether each peptide is a target (True) or a + decoy (False). + qvalue_algorithm: + The q-value calculation algorithm to use. Defaults to 'tdc' (mokapot + builtin). - :return: The q-values calculated using the specified algorithm. + Returns + ------- + array: + The q-values calculated using the specified algorithm. - :raises AssertionError: If the specified algorithm is unknown. - """ # noqa: E501 + Raises + ------ + ValueError + If the specified algorithm is unknown. + """ qvalue_function = QVALUE_ALGORITHM[qvalue_algorithm] if qvalue_function is not None: return qvalue_function(scores, targets) else: - raise AssertionError( + raise ValueError( "Unknown qvalue algorithm in qvalues_from_scores:" f" {qvalue_algorithm}" ) -def qvalues_from_peps(scores, targets, peps=None): - r""" - Compute q-values from peps according to Käll et al. (Section 3.2, first formula) - Non-parametric estimation of posterior error probabilities associated with peptides - identified by tandem mass spectrometry - Bioinformatics, Volume 24, Issue 16, August 2008, Pages i42-i48 +@typechecked +def qvalues_from_peps( + scores: np.ndarray[float], + targets: np.ndarray[bool], + is_tdc: bool, + peps: np.ndarray[float] | None = None, +): + r"""Compute q-values from peps. + + Computation is done according to Käll et al. (Section 3.2, first formula) + Non-parametric estimation of posterior error probabilities associated with + peptides identified by tandem mass spectrometry Bioinformatics, Volume 24, + Issue 16, August 2008, Pages i42-i48 https://doi.org/10.1093/bioinformatics/btn294 The formula used is: - .. math:: q_{PEP}(x^t) = \min_{x'\ge {x^t}} \frac{\sum_{x\in\{y|y\ge x',y\in T\}}P(H_0|X=x)}{|\{y|y\ge x',y\in T\}|} + .. math:: q_{PEP}(x^t) = \min_{x'\ge {x^t}} + \frac{\sum_{x\in\{y|y\ge x',y\in T\}}P(H_0|X=x)} + {|\{y|y\ge x',y\in T\}|} + + Note: the formula in the paper has an :math:`x^t` in the denominator, which + does not make a whole lot of sense. Shall probably be :math:`x'` instead. + + Parameters + ---------- + scores: + Array-like object representing the scores. + targets: + Boolean array-like object indicating the targets. + peps: + Array-like object representing the posterior error probabilities + associated with the peptides. Default is None (then it's computed via + the HistNNLS algorithm). + is_tdc: + Scores and targets come from competition, rather than separate search. - Note: the formula in the paper has an :math:`x^t` in the denominator, which does not make a whole lot of sense. - Shall probably be :math:`x'` instead. + Returns + ------- + array: + Array of q-values computed from peps. + """ - :param scores: Array-like object representing the scores. - :param targets: Boolean array-like object indicating the targets. - :param peps: Array-like object representing the posterior error probabilities associated - with the peptides. Default is None (then it's computed via the HistNNLS algorithm). - :return: Array of q-values computed from peps. - """ # noqa: E501 if peps is None: - peps = peps_from_scores_hist_nnls(scores, targets) + peps = peps_from_scores_hist_nnls(scores, targets, is_tdc) # We need to sort scores in descending order for the formula to work # (it involves to cumsum's from the maximum scores downwards) @@ -250,7 +283,9 @@ def qvalues_from_peps(scores, targets, peps=None): target_fdr = target_peps.cumsum() / np.arange( 1, len(target_peps) + 1, dtype=float ) - target_qvalues = monotonize_simple(target_fdr, ascending=True) + target_qvalues = monotonize_simple( + target_fdr, ascending=True, reverse=True + ) # Note: we need to flip the arrays again, since interp needs scores in # ascending order @@ -260,47 +295,131 @@ def qvalues_from_peps(scores, targets, peps=None): return qvalues -def qvalues_from_counts(scores, targets): +@typechecked +def qvalues_from_counts( + scores: np.ndarray[float], targets: np.ndarray[bool], is_tdc: bool +): r""" - Compute qvalues from target/decoy counts according to Käll et al. (Section 3.2, second formula) - Non-parametric estimation of posterior error probabilities associated with peptides - identified by tandem mass spectrometry - Bioinformatics, Volume 24, Issue 16, August 2008, Pages i42–i48 + Compute qvalues from target/decoy counts. + + Computed according to Käll et al. (Section 3.2, second formula) + Non-parametric estimation of posterior error probabilities associated with + peptides identified by tandem mass spectrometry Bioinformatics, Volume 24, + Issue 16, August 2008, Pages i42–i48 https://doi.org/10.1093/bioinformatics/btn294 The formula used is: - .. math:: q_{TD}(x^t) = \min_{x\ge {x^t}} \pi_0 \frac{\#T}{\#D} {|\{y|y\ge x, y\in D\}|}/{|\{y|y\ge x, y\in T\}|} + .. math:: q_{TD}(x^t) = \pi_0 \frac{\#T}{\#D} \min_{x\ge {x^t}} + {|\{y|y\ge x, y\in D\}|}/ + {|\{y|y\ge x, y\in T\}|} - Note: the factor :math:`\frac{\#T}{\#D}` is not in the original equation, but should be there to account of lists - of targets and decoys of different lengths. + Note: the factor :math:`\frac{\#T}{\#D}` is not in the original equation, + but should be there to account of lists of targets and decoys of different + lengths. - :param scores: Array-like object representing the scores. - :param targets: Boolean array-like object indicating the targets. - :return: Array of q-values computed from peps. - """ # noqa: E501 + Note 2: for tdc the estimator #D/#T is used for $\pi_0$, effectively + cancelling out the factor. - eval_scores, target_density, decoy_density = hist_data_from_scores( - scores, targets, density=True - ) - pi0 = estimate_pi0_by_slope(target_density, decoy_density) + Parameters + ---------- + scores : + Array-like object representing the scores. + targets : + Boolean array-like object indicating the targets. + is_tdc: + Scores and targets come from competition, rather than separate search. - target_decoy_ratio = targets.sum() / (~targets).sum() + Returns + ------- + array: + Array of q-values computed from peps. + """ - ind = np.argsort(-scores) + if is_tdc: + factor = 1 + else: + hist_data = hist_data_from_scores(scores, targets) + eval_scores, target_density, decoy_density = hist_data.as_densities() + pi0 = estimate_pi0_by_slope(target_density, decoy_density) + target_decoy_ratio = targets.sum() / (~targets).sum() + factor = pi0 * target_decoy_ratio + + # Sort by score, but take also care of multiple targets/decoys per score + ind = np.lexsort((targets, -scores)) targets_sorted = targets[ind] scores_sorted = scores[ind] fdr_sorted = ( - pi0 - * target_decoy_ratio - * (~targets_sorted).cumsum() - / targets_sorted.cumsum() + factor + * ((~targets_sorted).cumsum() + 1) + / np.maximum(targets_sorted.cumsum(), 1) + ) + qvalues_sorted = monotonize_simple( + fdr_sorted, ascending=True, reverse=True ) - qvalues_sorted = monotonize_simple(fdr_sorted, ascending=True) - # Note: we need to flip the arrays again, since interp needs scores in - # ascending order - qvalues = np.interp( - scores, np.flip(scores_sorted), np.flip(qvalues_sorted) + # Extract unique scores and take qvalue from the last of them + scores_uniq, idx_uniq, rev_uniq, cnt_uniq = np.unique( + scores_sorted, + return_index=True, + return_counts=True, + return_inverse=True, ) - return qvalues + qvalues_uniq = qvalues_sorted[idx_uniq + cnt_uniq - 1] + qvalues = qvalues_uniq[rev_uniq] + + return np.clip(qvalues, 0.0, 1.0) + + +@typechecked +def qvalues_func_from_hist( + hist_data: TDHistData, is_tdc: bool +) -> Callable[[np.ndarray[float]], np.ndarray[float]]: + r"""Compute q-values from histogram counts. + + Compute qvalues from target/decoy counts according to Käll et al. (Section + 3.2, second formula), but from the histogram counts. + + Note that the formula is exact for the left edges of each histogram bin. + For the interiors of the bins the q-values are linearly interpolated. + + Parameters + ---------- + scores : + Array-like object representing the scores. + targets : + Boolean array-like object indicating the targets. + hist_data: + Histogram data in form of a `TDHistData` object. + is_tdc: + Scores and targets come from competition, rather than separate search. + + Returns + ------- + function: + Function the computes an array of q-values from an array of scores. + """ + + _, target_counts, decoy_counts = hist_data.as_counts() + if is_tdc: + factor = 1 + else: + factor = estimate_pi0_by_slope(target_counts, decoy_counts) + + targets_sum = np.flip(target_counts).cumsum() + decoys_sum = np.flip(decoy_counts).cumsum() + + fdr_flipped = factor * (decoys_sum + 1) / np.maximum(targets_sum, 1) + fdr_flipped = np.clip(fdr_flipped, 0.0, 1.0) + qvalues_flipped = monotonize_simple( + fdr_flipped, ascending=True, reverse=True + ) + qvalues = np.flip(qvalues_flipped) + + # We need to append zero to end of the qvalues for right edge of the last + # bin, the other q-values correspond to the left edges of the bins + # (because of the >= in the formula for the counts) + qvalues = np.append(qvalues, 0.0) + eval_scores = hist_data.targets.bin_edges + + return lambda scores: np.interp(scores, eval_scores, qvalues) diff --git a/mokapot/rollup.py b/mokapot/rollup.py new file mode 100644 index 00000000..9880a89f --- /dev/null +++ b/mokapot/rollup.py @@ -0,0 +1,241 @@ +import logging +from pathlib import Path + +import numpy as np +from typeguard import typechecked + +from mokapot.cli_helper import make_timer +from mokapot.column_defs import STANDARD_COLUMN_NAME_MAP +from mokapot.confidence import compute_and_write_confidence +from mokapot.statistics import OnlineStatistics +from mokapot.tabular_data import ( + TabularDataReader, + remove_columns, + BufferType, + TabularDataWriter, + auto_finalize, + ComputedTabularDataReader, + MergedTabularDataReader, +) +from mokapot.tabular_data.target_decoy_writer import TargetDecoyWriter + + +@typechecked +def compute_rollup_levels( + base_level: str, parent_levels: dict[str, str] | None = None +) -> list[str]: + if parent_levels is None: + parent_levels = DEFAULT_PARENT_LEVELS + levels = [base_level] + changed = True + while changed: + changed = False + for child, parent in parent_levels.items(): + if (parent in levels) and (child not in levels): + levels.append(child) + changed = True + return levels + + +def get_target_decoy_reader(path: Path, is_decoy: bool): + return ComputedTabularDataReader( + reader=TabularDataReader.from_path( + path, column_map=STANDARD_COLUMN_NAME_MAP + ), + column="is_decoy", + dtype=np.dtype("bool"), + func=lambda df: np.full(len(df), is_decoy), + ) + + +DEFAULT_PARENT_LEVELS = { + "precursor": "psm", + "modified_peptide": "precursor", + "peptide": "modified_peptide", + "peptide_group": "precursor", # due to "unknown nature" of peptide groups +} + + +@typechecked +def do_rollup(config): + # todo: refactor: this function is far too long. Should be split. Probably + # at least one function to configure the input readers, one to write the + # intermediate/temp files, and one that computes the statistics (q-values + # and peps and writes the output files) + base_level: str = config.level + src_dir: Path = config.src_dir + dest_dir: Path = config.dest_dir + file_root: str = config.file_root + "." + + # Determine input files + if len(list(src_dir.glob(f"*.{base_level}s.parquet"))) > 0: + if len(list(src_dir.glob(f"*.{base_level}s.csv"))) > 0: + raise RuntimeError( + "Only input files of either type CSV or type Parquet should " + f"exist in '{src_dir}', but both types were found." + ) + suffix = ".parquet" + else: + suffix = ".csv" + + target_files: list[Path] = sorted( + src_dir.glob(f"*.targets.{base_level}s{suffix}") + ) + decoy_files: list[Path] = sorted( + src_dir.glob(f"*.decoys.{base_level}s{suffix}") + ) + target_files = [ + file for file in target_files if not file.name.startswith(file_root) + ] + decoy_files = [ + file for file in decoy_files if not file.name.startswith(file_root) + ] + in_files: list[Path] = sorted(target_files + decoy_files) + logging.info(f"Reading files: {[str(file) for file in in_files]}") + if len(in_files) == 0: + raise ValueError("No input files found.") + + # Configure readers (read targets/decoys and adjoin is_decoy column) + target_readers = [ + get_target_decoy_reader(path, False) for path in target_files + ] + decoy_readers = [ + get_target_decoy_reader(path, True) for path in decoy_files + ] + reader = MergedTabularDataReader( + target_readers + decoy_readers, + priority_column="score", + reader_chunk_size=10000, + ) + + # Determine out levels + levels = compute_rollup_levels(base_level, DEFAULT_PARENT_LEVELS) + levels_not_found = [ + level for level in levels if level not in reader.get_column_names() + ] + levels = [level for level in levels if level in reader.get_column_names()] + logging.info(f"Rolling up to levels: {levels}") + if len(levels_not_found) > 0: + logging.info( + f" (Rollup levels not found in input: {levels_not_found})" + ) + + # Determine temporary files + temp_files = { + level: dest_dir / f"{file_root}temp.{level}s{suffix}" + for level in levels + } + logging.debug( + "Using temp files: " + f"{ {level: str(file) for level, file in temp_files.items()} }" + ) + + # Determine columns for output files and intermediate files + in_column_names = reader.get_column_names() + in_column_types = reader.get_column_types() + + temp_column_names, temp_column_types = remove_columns( + in_column_names, in_column_types, ["q_value", "posterior_error_prob"] + ) + + # Configure temp writers + merge_row_type = BufferType.Dicts + + temp_buffer_size = 1000 + + temp_writers = { + level: TabularDataWriter.from_suffix( + temp_files[level], + columns=temp_column_names, + column_types=temp_column_types, + buffer_size=temp_buffer_size, + buffer_type=merge_row_type, + ) + for level in levels + } + + # todo: discuss: We need an option to write parquet or sql for example + # (also, the output file type could depend on the input file type) + + # Write temporary files which contain only the best scoring entity of a + # given level + logging.debug( + "Writing temp files: %s", [str(file) for file in temp_files.values()] + ) + + timer = make_timer() + score_stats = OnlineStatistics() + with auto_finalize(temp_writers.values()): + count = 0 + seen_entities: dict[str, set] = {level: set() for level in levels} + for data_row in reader.get_row_iterator( + temp_column_names, row_type=merge_row_type + ): + count += 1 + if count % 10000 == 0: + logging.debug( + f" Processed {count} lines ({timer():.2f} seconds)" + ) + + for level in levels: + seen = seen_entities[level] + id_col = level + if merge_row_type == BufferType.DataFrame: + id = data_row.loc[0, id_col] + else: + id = data_row[id_col] + if id not in seen: + seen.add(id) + temp_writers[level].append_data(data_row) + + score_stats.update_single(data_row["score"]) + + logging.info(f"Read {count} PSMs") + logging.debug(f"Score statistics: {score_stats.describe()}") + for level in levels: + seen = seen_entities[level] + logging.info( + f"Rollup level {level}: found {len(seen)} unique entities" + ) + + # Determine output files + out_files_map = { + level: [ + dest_dir / f"{file_root}targets.{level}s{suffix}", + dest_dir / f"{file_root}decoys.{level}s{suffix}", + ] + for level in levels + } + + # Configure temp readers and output writers + buffer_size = 1000 + output_columns, output_types = remove_columns( + in_column_names, in_column_types, ["is_decoy"] + ) + output_options = dict( + columns=output_columns, + column_types=output_types, + buffer_size=buffer_size, + ) + + def create_writer(path: Path): + return TabularDataWriter.from_suffix(path, **output_options) + + for level in levels: + output_writers = list(map(create_writer, out_files_map[level])) + writer = TargetDecoyWriter( + output_writers, write_decoys=True, decoy_column="is_decoy" + ) + with auto_finalize(output_writers): + temp_reader = temp_writers[level].get_associated_reader() + compute_and_write_confidence( + temp_reader, + writer, + config.qvalue_algorithm, + config.peps_algorithm, + config.stream_confidence, + score_stats, + peps_error=True, + level=level, + eval_fdr=0.01, + ) diff --git a/mokapot/statistics.py b/mokapot/statistics.py new file mode 100644 index 00000000..771bd669 --- /dev/null +++ b/mokapot/statistics.py @@ -0,0 +1,196 @@ +import math +from collections import namedtuple + +import numpy as np +from typeguard import typechecked + +SummaryStatistics = namedtuple( + "SummaryStatistics", ("n", "min", "max", "sum", "mean", "var", "sd") +) + + +@typechecked +class OnlineStatistics: + """ + @class Statistics: + A class for performing basic statistical calculations. + + @attribute min: + The minimum value encountered so far. Initialized to positive infinity. + + @attribute max: + The maximum value encountered so far. Initialized to negative infinity. + + @attribute n: + The number of values encountered so far. Initialized to 0. + + @attribute sum: + The sum of all values encountered so far. Initialized to 0.0. + + @attribute mean: + The mean value calculated based on the encountered values. Initialized + to 0.0. + + @attribute var: + The variance value calculated based on the encountered values. + Initialized to 0.0. + + @attribute sd: + The standard deviation value calculated based on the encountered + values. Initialized to 0.0. + + @attribute M2n: + The intermediate value used in calculating variance. Initialized to + 0.0. + + @method update(vals: np.ndarray): + Updates the statistics with an array of values. + + Args: + vals (np.ndarray): An array of values to update the statistics. + + Returns: + None. + """ + + min: float = math.inf + max: float = -math.inf + n: int = 0 + sum: float = 0.0 + mean: float = 0.0 + + M2n: float = 0.0 + ddof: float = 1.0 + + @property + def var(self) -> float: + return self.M2n / (self.n - self.ddof) + + @property + def sd(self) -> float: + return math.sqrt(self.var) + + def __init__(self, unbiased: bool = True): + if unbiased: + self.ddof = 1 # Use unbiased variance estimator + else: + self.ddof = ( + 0 # Use maximum likelihood (best L2) variance estimator + ) + + def update(self, vals: np.ndarray) -> None: + """ + Update the statistics with an array of values. + + For updating the variance a variant of Welford's algo is used (see e.g. + https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm). + + Parameters + ---------- + vals : np.ndarray + The array of values to update the statistics with. + """ # noqa: E501 + + self.min = min(self.min, vals.min()) + self.max = max(self.max, vals.max()) + self.n += len(vals) + self.sum += vals.sum() + old_mean = self.mean + self.mean = self.sum / self.n + self.M2n += ((vals - old_mean) * (vals - self.mean)).sum() + + def update_single(self, val): + # Note: type checking is too tricky due to all the different numeric + # data type in vanilla python and in numpy + self.min = min(self.min, val) + self.max = max(self.max, val) + self.n += 1 + self.sum += val + old_mean = self.mean + self.mean = self.sum / self.n + self.M2n += (val - old_mean) * (val - self.mean) + + def describe(self): + return SummaryStatistics( + self.n, self.min, self.max, self.sum, self.mean, self.var, self.sd + ) + + +@typechecked +class HistData: + bin_edges: np.ndarray[float] + counts: np.ndarray[int] + + def __init__(self, bin_edges: np.ndarray[float], counts: np.ndarray[int]): + if len(bin_edges) != len(counts) + 1: + raise ValueError( + "`bin_edges` must have one more element than `counts` " + f"({len(bin_edges)=}, {len(counts)=})" + ) + + self.bin_edges = bin_edges + self.counts = counts + + @property + def bin_centers(self) -> np.ndarray[float]: + return 0.5 * (self.bin_edges[:-1] + self.bin_edges[1:]) + + @property + def density(self) -> np.ndarray[float]: + dx = np.diff(self.bin_edges) + counts = self.counts.astype(float) + return counts / (dx * counts.sum()) + + @staticmethod + def bin_size_sturges(stats: OnlineStatistics) -> float: + return (stats.max - stats.min) / (np.log2(stats.n) + 1.0) + + @staticmethod + def bin_size_scott(stats: OnlineStatistics) -> float: + factor = (24 * 24 * np.pi) ** (1.0 / 6.0) + return factor * stats.sd * stats.n ** (-1.0 / 3.0) + + @staticmethod + def bin_size_terrell_scott(stats: OnlineStatistics) -> float: + num_bins = np.ceil((2.0 * stats.n) ** (1.0 / 3.0)) + return (stats.max - stats.min) / num_bins + + @staticmethod + def get_bin_edges( + stats: OnlineStatistics, + name="scott", + clip: tuple[int, int] | None = None, + extend: bool = False, + ): + if name == "scott": + bin_size = HistData.bin_size_scott(stats) + elif name == "terrell_scott": + bin_size = HistData.bin_size_terrell_scott(stats) + elif name == "sturges": + bin_size = HistData.bin_size_sturges(stats) + elif name == "auto": + bin_size = bin_size = HistData.bin_size_scott(stats) + else: + raise ValueError(f"Unrecognized binning algorithm name: {name}") + + range = (stats.min, stats.max) + num_bins = int(np.ceil((range[1] - range[0]) / bin_size)) + if clip is not None: + num_bins = np.clip(num_bins, *clip) + + if extend: + bin_size = (range[1] - range[0]) / num_bins + num_bins += 1 + if clip is not None: + num_bins = np.clip(num_bins, *clip) + range = (stats.min - 0.5 * bin_size, stats.max + 0.5 * bin_size) + + bin_edges = np.histogram_bin_edges([], bins=num_bins, range=range) + return bin_edges + + +def gaussian_iqr(mu: float, sigma: float) -> tuple[float, float]: + # Quartiles for the standard normal distribution are about +-0.67. + # Get the exact value with `scipy.stats.norm.isf(0.25)`. + alpha = 0.6744897501960817 + return (mu - alpha * sigma, mu + alpha * sigma) diff --git a/mokapot/tabular_data.py b/mokapot/tabular_data.py deleted file mode 100644 index 5ced7452..00000000 --- a/mokapot/tabular_data.py +++ /dev/null @@ -1,723 +0,0 @@ -""" -Classes for reading and writing data in tabular form. -""" - -from __future__ import annotations - -import sqlite3 -import warnings -from contextlib import contextmanager -from enum import Enum -from typing import Generator - -import numpy as np -import pandas as pd -from abc import ABC, abstractmethod -from pathlib import Path -import pyarrow.parquet as pq -from typeguard import typechecked -import pyarrow as pa - -CSV_SUFFIXES = [ - ".csv", - ".pin", - ".tab", - ".peptides", - ".psms", - ".proteins", - ".modifiedpeptides", - ".peptidegroups", - ".modified_peptides", - ".peptide_groups", - ".precursors", -] -PARQUET_SUFFIXES = [".parquet"] -SQLITE_SUFFIXES = [".db"] - - -class TableType(Enum): - DataFrame = "DataFrame" - Records = "Records" - Dicts = "Dicts" - - -def get_score_column_type(suffix): - if suffix in PARQUET_SUFFIXES: - return pa.float64() - elif suffix in CSV_SUFFIXES: - return "float" - else: - raise ValueError(f"Suffix '{suffix}' does not match expected formats") - - -@typechecked -class TabularDataReader(ABC): - """ - An abstract class that represents a source for tabular data that can be - read in either completely or chunk-wise. - - Implementations can be classes that either read from files, from memory - (e.g. data frames), combine or modify other readers or represent computed - tabular results. - """ - @abstractmethod - def get_column_names(self) -> list[str]: - raise NotImplementedError - - @abstractmethod - def get_column_types(self) -> list: - raise NotImplementedError - - @abstractmethod - def read(self, columns: list[str] | None = None) -> pd.DataFrame: - raise NotImplementedError - - @abstractmethod - def get_chunked_data_iterator( - self, chunk_size: int, columns: list[str] | None = None - ) -> Generator[pd.DataFrame, None, None]: - raise NotImplementedError - - def _returned_dataframe_is_mutable(self): - return True - - @staticmethod - def from_path( - file_name: Path, column_map: dict[str, str] | None = None, **kwargs - ) -> "TabularDataReader": - # Currently, we look only at the suffix, however, in the future we - # could also look into the file itself (is it ascii? does it have - # some "magic bytes"? ...) - suffix = file_name.suffix - if suffix in CSV_SUFFIXES: - reader = CSVFileReader(file_name, **kwargs) - elif suffix in PARQUET_SUFFIXES: - reader = ParquetFileReader(file_name, **kwargs) - else: - # Fallback - warnings.warn( - f"Suffix '{suffix}' not recognized in file name '{file_name}'." - " Falling back to CSV..." - ) - reader = CSVFileReader(file_name, **kwargs) - - if column_map is not None: - reader = ColumnMappedReader(reader, column_map) - - return reader - - -@typechecked -class ColumnMappedReader(TabularDataReader): - """ - A tabular data reader that renames the columns of another tabular data - reader to new names. - - Attributes: - ----------- - reader : TabularDataReader - The underlying reader for the original data. - column_map : dict[str, str] - A dictionary that maps the original column names to the new - column names. - """ - def __init__(self, reader: TabularDataReader, column_map: dict[str, str]): - self.reader = reader - self.column_map = column_map - - def get_column_names(self) -> list[str]: - return [ - self.column_map.get(column, column) - for column in self.reader.get_column_names() - ] - - def get_column_types(self) -> list: - return self.reader.get_column_types() - - def _get_orig_columns(self, columns: list[str] | None) -> list[str] | None: - if columns is None: - return None - - all_orig_columns = self.reader.get_column_names() - all_columns = self.get_column_names() - reverse_column_map = dict(zip(all_columns, all_orig_columns)) - orig_columns = [reverse_column_map[column] for column in columns] - return orig_columns - - def _get_mapped_dataframe(self, df: pd.DataFrame) -> pd.DataFrame: - # todo: enable this again... Modifying in-place would be more - # efficient than creating a copy, but this implementation - # creates errors. Once those are ironed out we can re-enable - # this code. - # - # if self._returned_dataframe_is_mutable(): - # df.rename(columns=self.column_map, inplace=True, copy=False) - # else: - # df = df.rename( - # columns=self.column_map, inplace=False, copy=False - # ) - df = df.rename(columns=self.column_map, inplace=False) - return df - - def read(self, columns: list[str] | None = None) -> pd.DataFrame: - df = self.reader.read(columns=self._get_orig_columns(columns)) - return self._get_mapped_dataframe(df) - - def get_chunked_data_iterator( - self, chunk_size: int, columns: list[str] | None = None - ) -> Generator[pd.DataFrame, None, None]: - orig_columns = self._get_orig_columns(columns) - for chunk in self.reader.get_chunked_data_iterator( - chunk_size, columns=orig_columns - ): - yield self._get_mapped_dataframe(chunk) - - -@typechecked -class CSVFileReader(TabularDataReader): - """ - A tabular data reader for reading CSV files. - - Attributes: - ----------- - file_name : Path - The path to the CSV file. - stdargs : dict - Arguments for reading CSV file passed on to the pandas - `read_csv` function. - """ - def __init__(self, file_name: Path, sep: str = "\t"): - self.file_name = file_name - self.stdargs = {"sep": sep, "index_col": False} - - def __str__(self): - return f"CSVFileReader({self.file_name=})" - - def __repr__(self): - return f"CSVFileReader({self.file_name=},{self.stdargs=})" - - def get_column_names(self) -> list[str]: - df = pd.read_csv(self.file_name, **self.stdargs, nrows=0) - return df.columns.tolist() - - def get_column_types(self) -> list: - df = pd.read_csv(self.file_name, **self.stdargs, nrows=2) - return df.dtypes.tolist() - - def read(self, columns: list[str] | None = None) -> pd.DataFrame: - result = pd.read_csv(self.file_name, usecols=columns, **self.stdargs) - return result if columns is None else result[columns] - - def get_chunked_data_iterator( - self, chunk_size: int, columns: list[str] | None = None - ) -> Generator[pd.DataFrame, None, None]: - for chunk in pd.read_csv( - self.file_name, - usecols=columns, - chunksize=chunk_size, - **self.stdargs, - ): - yield chunk if columns is None else chunk[columns] - - -@typechecked -class DataFrameReader(TabularDataReader): - """ - This class allows reading pandas DataFrames in the context of tabular data - readers. - - Attributes: - ----------- - df : pd.DataFrame - The DataFrame being read from. - """ - df: pd.DataFrame - - def __init__(self, df: pd.DataFrame): - self.df = df - - def __str__(self): - return f"DataFrameReader({self.df.columns=})" - - def __repr__(self): - return f"DataFrameReader({self.df=})" - - def get_column_names(self) -> list[str]: - return self.df.columns.tolist() - - def get_column_types(self) -> list: - return self.df.dtypes.tolist() - - def read(self, columns: list[str] | None = None) -> pd.DataFrame: - return self.df if columns is None else self.df[columns] - - def get_chunked_data_iterator( - self, chunk_size: int, columns: list[str] | None = None - ) -> Generator[pd.DataFrame, None, None]: - for pos in range(0, len(self.df), chunk_size): - chunk = self.df.iloc[pos : pos + chunk_size] - yield chunk if columns is None else chunk[columns] - - def _returned_dataframe_is_mutable(self): - return False - - @staticmethod - def from_series(series: pd.Series, name=None) -> "DataFrameReader": - if name is not None: - return DataFrameReader(series.to_frame(name=name)) - else: - return DataFrameReader(series.to_frame()) - - @staticmethod - def from_array(array: list | np.ndarray, name: str) -> "DataFrameReader": - return DataFrameReader(pd.DataFrame({name: array})) - - -@typechecked -class ParquetFileReader(TabularDataReader): - """ - A class for reading Parquet files and retrieving data in tabular format. - - Attributes: - ----------- - file_name : Path - The path to the Parquet file. - """ - - def __init__(self, file_name: Path): - self.file_name = file_name - - def __str__(self): - return f"ParquetFileReader({self.file_name=})" - - def __repr__(self): - return f"ParquetFileReader({self.file_name=})" - - def get_column_names(self) -> list[str]: - return pq.ParquetFile(self.file_name).schema.names - - def get_column_types(self) -> list: - return pq.ParquetFile(self.file_name).schema.to_arrow_schema().types - - def read(self, columns: list[str] | None = None) -> pd.DataFrame: - result = pq.read_table(self.file_name, columns=columns).to_pandas() - return result - - def get_chunked_data_iterator( - self, chunk_size: int, columns: list[str] | None = None - ) -> Generator[pd.DataFrame, None, None]: - pf = pq.ParquetFile(self.file_name) - - for i, record_batch in enumerate( - pf.iter_batches(chunk_size, columns=columns) - ): - df = record_batch.to_pandas() - df.index = df.index + i * chunk_size - yield df - - -@typechecked -class TabularDataWriter(ABC): - """ - Abstract base class for writing tabular data to different file formats. - - Attributes: - ----------- - columns : list[str] - List of column names - column_types : list | None - List of column types (optional) - """ - def __init__( - self, - columns: list[str], - column_types: list | None = None, - ): - self.columns = columns - self.column_types = column_types - # todo: I think the TDW should have a field/option that says whether - # data should be appended or whether the file should be cleared first - # if it already contains data - - def get_column_names(self) -> list[str]: - return self.columns - - def get_column_types(self) -> list: - return self.column_types - - @abstractmethod - def append_data(self, data: pd.DataFrame): - raise NotImplementedError - - def check_valid_data(self, data: pd.DataFrame): - columns = data.columns.tolist() - if not columns == self.get_column_names(): - raise ValueError( - f"Column names {columns} do not " - f"match {self.get_column_names()}" - ) - - if self.column_types is not None: - pass - # todo: Commented out for a while till we have a better type - # compatibility check, or agreed on some "super type" of numpy - # dtype and pyarrow types (and what not...) - # column_types = data.dtypes.tolist() - # if not column_types == self.get_column_types(): - # raise ValueError( - # f"Column types {column_types} do " - # f"not match {self.get_column_types()}" - # ) - - def write(self, data: pd.DataFrame): - self.check_valid_data(data) - self.initialize() - self.append_data(data) - self.finalize() - - def initialize(self): - pass - - def finalize(self): - pass - - def __enter__(self): - self.initialize() - return self - - def __exit__(self, exc_type, exc_value, exc_traceback): - self.finalize() - - @abstractmethod - def get_associated_reader(self): - raise NotImplementedError - - @staticmethod - def from_suffix( - file_name: Path, - columns: list[str], - buffer_size: int = 0, - buffer_type: TableType = TableType.DataFrame, - **kwargs, - ) -> "TabularDataWriter": - suffix = file_name.suffix - if suffix in CSV_SUFFIXES: - writer = CSVFileWriter(file_name, columns, **kwargs) - elif suffix in PARQUET_SUFFIXES: - writer = ParquetFileWriter(file_name, columns, **kwargs) - elif suffix in SQLITE_SUFFIXES: - writer = SqliteWriter(file_name, columns, **kwargs) - else: # Fallback - warnings.warn( - f"Suffix '{suffix}' not recognized in file name '{file_name}'." - " Falling back to CSV..." - ) - writer = CSVFileWriter(file_name, columns, **kwargs) - - if buffer_size > 1: - writer = BufferedWriter(writer, buffer_size, buffer_type) - return writer - - -@contextmanager -# @typechecked -def auto_finalize(writers: list[TabularDataWriter]): - # todo: this method should actually (to be really secure), check which - # writers were correctly initialized and if some initialization throws an - # error, finalize all that already have been initialized. Similar with - # errors during finalization. - for writer in writers: - writer.__enter__() - try: - yield None - finally: - for writer in writers: - writer.__exit__(None, None, None) - - -@typechecked -class BufferedWriter(TabularDataWriter): - """ - This class represents a buffered writer for tabular data. It allows - writing data to a tabular data writer in batches, reducing the - number of write operations. - - Attributes: - ----------- - writer : TabularDataWriter - The tabular data writer to which the data will be written. - buffer_size : int - The number of records to buffer before writing to the writer. - buffer_type : TableType - The type of buffer being used. Can be one of TableType.DataFrame, - TableType.Dicts, or TableType.Records. - buffer : pd.DataFrame or list of dictionaries or np.recarray or None - The buffer containing the tabular data to be written. - The buffer type depends on the buffer_type attribute. - """ - writer: TabularDataWriter - buffer_size: int - buffer_type: TableType - buffer: pd.DataFrame | list[dict] | np.recarray | None - - def __init__( - self, - writer: TabularDataWriter, - buffer_size=1000, - buffer_type=TableType.DataFrame, - ): - super().__init__(writer.columns, writer.column_types) - self.writer = writer - self.buffer_size = buffer_size - self.buffer_type = buffer_type - self.buffer = None - - def _buffer_slice( - self, - start: int = 0, - end: int | None = None, - as_dataframe: bool = False, - ): - if self.buffer_type == TableType.DataFrame: - slice = self.buffer.iloc[start:end] - else: - slice = self.buffer[start:end] - if as_dataframe and not isinstance(slice, pd.DataFrame): - return pd.DataFrame(slice) - else: - return slice - - def _write_buffer(self, force=False): - if self.buffer is None: - return - while len(self.buffer) >= self.buffer_size: - self.writer.append_data( - self._buffer_slice(end=self.buffer_size, as_dataframe=True) - ) - self.buffer = self._buffer_slice( - start=self.buffer_size, - ) - if force and len(self.buffer) > 0: - self.writer.append_data(self._buffer_slice(as_dataframe=True)) - self.buffer = None - - def append_data(self, data: pd.DataFrame | dict | list[dict] | np.record): - if self.buffer_type == TableType.DataFrame: - if not isinstance(data, pd.DataFrame): - raise TypeError( - "Parameter data must be of type DataFrame," - f" not {type(data)}" - ) - - if self.buffer is None: - self.buffer = data.copy(deep=True) - else: - self.buffer = pd.concat( - [self.buffer, data], axis=0, ignore_index=True - ) - elif self.buffer_type == TableType.Dicts: - if isinstance(data, dict): - data = [data] - if self.buffer is None: - self.buffer = [] - self.buffer += data - elif self.buffer_type == TableType.Records: - if self.buffer is None: - self.buffer = np.recarray(shape=(0,), dtype=data.dtype) - self.buffer = np.append(self.buffer, data) - else: - raise RuntimeError("Not yet done...") - - self._write_buffer() - - def check_valid_data(self, data: pd.DataFrame): - return self.writer.check_valid_data(data) - - def write(self, data: pd.DataFrame): - self.writer.write(data) - - def initialize(self): - self.writer.initialize() - - def finalize(self): - self._write_buffer(force=True) - self.writer.finalize() - - def get_associated_reader(self): - return self.writer.get_associated_reader() - - -@typechecked -class CSVFileWriter(TabularDataWriter): - """ - CSVFileWriter class for writing tabular data to a CSV file. - - Attributes: - ----------- - file_name : Path - The file path where the CSV file will be written. - - sep : str, optional - The separator string used to separate fields in the CSV file. - Default is tab character ("\t"). - """ - file_name: Path - - def __init__( - self, - file_name: Path, - columns: list[str], - column_types: list | None = None, - sep: str = "\t", - ): - super().__init__(columns, column_types) - self.file_name = file_name - self.stdargs = {"sep": sep, "index": False} - - def __str__(self): - return f"CSVFileWriter({self.file_name=},{self.columns=})" - - def __repr__(self): - return ( - f"CSVFileWriter({self.file_name=},{self.columns=},{self.stdargs=})" - ) - - def get_schema(self, as_dict: bool = True): - schema = [] - for name, type in zip(self.columns, self.column_types): - schema.append((name, type)) - return {name: str(type) for name, type in schema} - - def initialize(self): - # Just write header information - df = pd.DataFrame(columns=self.columns) - df.to_csv(self.file_name, **self.stdargs) - - def finalize(self): - # no need to do anything here - pass - - def append_data(self, data: pd.DataFrame): - self.check_valid_data(data) - data.to_csv(self.file_name, mode="a", header=False, **self.stdargs) - - def get_associated_reader(self): - return CSVFileReader(self.file_name, sep=self.stdargs["sep"]) - - -@typechecked -class ParquetFileWriter(TabularDataWriter): - """ - This class is responsible for writing tabular data into Parquet files. - - - Attributes: - ----------- - file_name : Path - The path to the Parquet file being written. - """ - file_name: Path - - def __init__( - self, - file_name: Path, - columns: list[str], - column_types: list | None = None, - ): - super().__init__(columns, column_types) - self.file_name = file_name - - def __str__(self): - return f"ParquetFileWriter({self.file_name=},{self.columns=})" - - def __repr__(self): - return f"ParquetFileWriter({self.file_name=},{self.columns=})" - - def get_schema(self, as_dict: bool = False): - schema = [] - for name, type in zip(self.columns, self.column_types): - schema.append((name, type)) - if as_dict: - return {name: str(type) for name, type in schema} - return pa.schema(schema) - - def initialize(self): - self.writer = pq.ParquetWriter( - self.file_name, schema=self.get_schema() - ) - - def finalize(self): - self.writer.close() - - def append_data(self, data: pd.DataFrame): - table = pa.Table.from_pandas( - data, preserve_index=False, schema=self.get_schema() - ) - self.writer.write_table(table) - - def write(self, data: pd.DataFrame): - data.to_parquet(self.file_name, index=False) - - def get_associated_reader(self): - return ParquetFileReader(self.file_name) - - -@typechecked -class SqliteWriter(TabularDataWriter, ABC): - """ - SqliteWriter class for writing tabular data to SQLite database. - """ - connection: sqlite3.Connection - - def __init__( - self, - database: str | Path | sqlite3.Connection, - columns: list[str], - column_types: list | None = None, - ) -> None: - super().__init__(columns, column_types) - if isinstance(database, sqlite3.Connection): - self.file_name = None - self.connection = database - else: - self.file_name = database - self.connection = sqlite3.connect(self.file_name) - - def __str__(self): - return f"SqliteFileWriter({self.file_name=},{self.columns=})" - - def __repr__(self): - return f"SqliteFileWriter({self.file_name=},{self.columns=})" - - def initialize(self): - # Nothing to do here, we expect the table(s) to already exist - pass - - def finalize(self): - self.connection.commit() - self.connection.close() - - def append_data(self, data: pd.DataFrame): - # Must be implemented in derived class - # todo: maybe we can supply also a default implementation in this class - # given the table name and sql column names - raise NotImplementedError - - def get_associated_reader(self): - # todo: need an sqlite reader first... - raise NotImplementedError - - -@typechecked -def remove_columns( - column_names: list[str], - column_types: list, - columns_to_remove: list[str], -) -> tuple[list[str], list]: - temp_columns = [ - (column, type) - for column, type in zip(column_names, column_types) - if column not in columns_to_remove - ] - temp_column_names, temp_column_types = zip(*temp_columns) - return (list(temp_column_names), list(temp_column_types)) diff --git a/mokapot/tabular_data/__init__.py b/mokapot/tabular_data/__init__.py new file mode 100644 index 00000000..edc36e4b --- /dev/null +++ b/mokapot/tabular_data/__init__.py @@ -0,0 +1,21 @@ +from .base import ( + auto_finalize, + remove_columns, + BufferType, + ColumnMappedReader, + ColumnSelectReader, + DataFrameReader, + TabularDataReader, + TabularDataWriter, +) +from .csv import CSVFileReader, CSVFileWriter +from .parquet import ParquetFileWriter, ParquetFileReader +from .streaming import ( + join_readers, + merge_readers, + BufferedWriter, + ComputedTabularDataReader, + JoinedTabularDataReader, + MergedTabularDataReader, +) +from .sqlite import ConfidenceSqliteWriter, SqliteWriter diff --git a/mokapot/tabular_data/base.py b/mokapot/tabular_data/base.py new file mode 100644 index 00000000..20c09ff1 --- /dev/null +++ b/mokapot/tabular_data/base.py @@ -0,0 +1,367 @@ +""" +Classes for reading and writing data in tabular form. +""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from contextlib import contextmanager +from enum import Enum +from pathlib import Path +from typing import Generator + +import numpy as np +import pandas as pd +from typeguard import typechecked + + +class BufferType(Enum): + DataFrame = "DataFrame" + Records = "Records" + Dicts = "Dicts" + + +@typechecked +class TabularDataReader(ABC): + """ + An abstract class that represents a source for tabular data that can be + read in either completely or chunk-wise. + + Implementations can be classes that either read from files, from memory + (e.g. data frames), combine or modify other readers or represent computed + tabular results. + """ + + @abstractmethod + def get_column_names(self) -> list[str]: + raise NotImplementedError + + @abstractmethod + def get_column_types(self) -> list[np.dtype]: + raise NotImplementedError + + def get_schema( + self, as_dict: bool = False + ) -> dict[str, np.dtype] | list[tuple[str, np.dtype]]: + schema = list(zip(self.get_column_names(), self.get_column_types())) + if as_dict: + return {name: type for name, type in schema} + return schema + + @abstractmethod + def read(self, columns: list[str] | None = None) -> pd.DataFrame: + raise NotImplementedError + + @abstractmethod + def get_chunked_data_iterator( + self, chunk_size: int, columns: list[str] | None = None + ) -> Generator[pd.DataFrame, None, None]: + raise NotImplementedError + + def _returned_dataframe_is_mutable(self): + return True + + def get_default_extension(self) -> str: + raise NotImplementedError + + @staticmethod + def from_path( + file_name: Path, + column_map: dict[str, str] | None = None, + only_columns: list[str] | None = None, + **kwargs, + ) -> TabularDataReader: + from .format_chooser import reader_from_path + + return reader_from_path(file_name, column_map, only_columns, **kwargs) + + @staticmethod + def from_series(series: pd.Series, name=None) -> TabularDataReader: + if name is not None: + return DataFrameReader(series.to_frame(name=name)) + else: + return DataFrameReader(series.to_frame()) + + @staticmethod + def from_array(array: np.ndarray, name: str) -> TabularDataReader: + if array.ndim == 2 and array.shape[1] == 1: + array = array[:, 0] + elif array.ndim > 1: + raise ValueError("Array must be 1-dimensional") + + return DataFrameReader(pd.DataFrame({name: array})) + + +@typechecked +class ColumnSelectReader(TabularDataReader): + """ + A tabular data reader that returns only certain selected columns of another + reader. + + Attributes: + ----------- + reader : TabularDataReader + The underlying reader for the original data. + selected_columns : list[str] + A list that contains names of the selected columns. + """ + + def __init__(self, reader: TabularDataReader, selected_columns: list[str]): + self.reader = reader + self.selected_columns = selected_columns + + type_map = reader.get_schema(as_dict=True) + self.types = [type_map[column] for column in selected_columns] + + def get_column_names(self) -> list[str]: + return self.selected_columns + + def get_column_types(self) -> list[np.dtype]: + return self.types + + def _check_columns(self, columns: list[str]): + for column in columns or []: + if column not in self.selected_columns: + raise ValueError( + f"Columns ({columns}) are not a subset of " + f"the selected columns ({self.selected_columns})" + ) + + def read(self, columns: list[str] | None = None) -> pd.DataFrame: + self._check_columns(columns) + return self.reader.read(columns=self.selected_columns or columns) + + def get_chunked_data_iterator( + self, chunk_size: int, columns: list[str] | None = None + ) -> Generator[pd.DataFrame, None, None]: + self._check_columns(columns) + return self.reader.get_chunked_data_iterator( + columns=self.selected_columns or columns + ) + + +@typechecked +class ColumnMappedReader(TabularDataReader): + """ + A tabular data reader that renames the columns of another tabular data + reader to new names. + + Attributes: + ----------- + reader : TabularDataReader + The underlying reader for the original data. + column_map : dict[str, str] + A dictionary that maps the original column names to the new + column names. + """ + + def __init__(self, reader: TabularDataReader, column_map: dict[str, str]): + self.reader = reader + self.column_map = column_map + + def get_column_names(self) -> list[str]: + return [ + self.column_map.get(column, column) + for column in self.reader.get_column_names() + ] + + def get_column_types(self) -> list[np.dtype]: + return self.reader.get_column_types() + + def _get_orig_columns(self, columns: list[str] | None) -> list[str] | None: + if columns is None: + return None + + all_orig_columns = self.reader.get_column_names() + all_columns = self.get_column_names() + reverse_column_map = dict(zip(all_columns, all_orig_columns)) + orig_columns = [reverse_column_map[column] for column in columns] + return orig_columns + + def _get_mapped_dataframe(self, df: pd.DataFrame) -> pd.DataFrame: + # Note: the returned dataframe from here is always mutable, either + # because the original reader allows this, or because we made a copy + if self.reader._returned_dataframe_is_mutable(): + df.rename(columns=self.column_map, inplace=True) + else: + df = df.rename(columns=self.column_map, inplace=False, copy=False) + return df + + def read(self, columns: list[str] | None = None) -> pd.DataFrame: + df = self.reader.read(columns=self._get_orig_columns(columns)) + return self._get_mapped_dataframe(df) + + def get_chunked_data_iterator( + self, chunk_size: int, columns: list[str] | None = None + ) -> Generator[pd.DataFrame, None, None]: + orig_columns = self._get_orig_columns(columns) + for chunk in self.reader.get_chunked_data_iterator( + chunk_size, columns=orig_columns + ): + yield self._get_mapped_dataframe(chunk) + + +@typechecked +class DataFrameReader(TabularDataReader): + """ + This class allows reading pandas DataFrames in the context of tabular data + readers. + + Attributes: + ----------- + df : pd.DataFrame + The DataFrame being read from. + """ + + df: pd.DataFrame + + def __init__(self, df: pd.DataFrame): + self.df = df + + def __str__(self): + return f"DataFrameReader({self.df.columns=})" + + def __repr__(self): + return f"DataFrameReader({self.df=})" + + def get_column_names(self) -> list[str]: + return self.df.columns.tolist() + + def get_column_types(self) -> list[np.dtype]: + return self.df.dtypes.tolist() + + def read(self, columns: list[str] | None = None) -> pd.DataFrame: + return self.df if columns is None else self.df[columns] + + def get_chunked_data_iterator( + self, chunk_size: int, columns: list[str] | None = None + ) -> Generator[pd.DataFrame, None, None]: + for pos in range(0, len(self.df), chunk_size): + chunk = self.df.iloc[pos : pos + chunk_size] + yield chunk if columns is None else chunk[columns] + + def _returned_dataframe_is_mutable(self): + return False + + +@typechecked +class TabularDataWriter(ABC): + """ + Abstract base class for writing tabular data to different file formats. + + Attributes: + ----------- + columns : list[str] + List of column names + column_types : list | None + List of column types (optional) + """ + + def __init__( + self, + columns: list[str], + column_types: list[np.dtype], + ): + if len(column_types) not in [0, len(columns)]: + raise ValueError( + "`column_types` must have length 0 or same length as `columns`" + ) + self.columns = columns + self.column_types = column_types + + def get_column_names(self) -> list[str]: + return self.columns + + def get_column_types(self) -> list[np.dtype]: + return self.column_types + + @abstractmethod + def append_data(self, data: pd.DataFrame): + raise NotImplementedError + + def check_valid_data(self, data: pd.DataFrame): + columns = data.columns.tolist() + if not columns == self.get_column_names(): + raise ValueError( + f"Column names {columns} do not " + f"match {self.get_column_names()}" + ) + + if self.column_types is not None: + column_types = data.dtypes.tolist() + for own_type, other_type in zip(self.column_types, column_types): + if not np.can_cast(own_type, other_type, "same_kind"): + raise ValueError( + f"Column types {column_types} do not match " + f"{self.get_column_types()} ({own_type}!={other_type})" + ) + + def write(self, data: pd.DataFrame): + self.check_valid_data(data) + self.initialize() + self.append_data(data) + self.finalize() + + def initialize(self): + pass + + def finalize(self): + pass + + def __enter__(self): + self.initialize() + return self + + def __exit__(self, exc_type, exc_value, exc_traceback): + self.finalize() + + @abstractmethod + def get_associated_reader(self): + raise NotImplementedError + + @staticmethod + def from_suffix( + file_name: Path, + columns: list[str], + column_types: list[np.dtype], + buffer_size: int = 0, + buffer_type: BufferType = BufferType.DataFrame, + **kwargs, + ) -> TabularDataWriter: + # local import needed to avoid circular imports + from .format_chooser import writer_from_suffix + + return writer_from_suffix( + file_name, columns, column_types, buffer_size, buffer_type + ) + + +@contextmanager +# @typechecked +def auto_finalize(writers: list[TabularDataWriter]): + # todo: nice to have: this method should actually (to be really secure), + # check which writers were correctly initialized and if some + # initialization throws an error, finalize all that already have been + # initialized. Similar with errors during finalization. + for writer in writers: + writer.__enter__() + try: + yield None + finally: + for writer in writers: + writer.__exit__(None, None, None) + + +@typechecked +def remove_columns( + column_names: list[str], + column_types: list, + columns_to_remove: list[str], +) -> tuple[list[str], list]: + temp_columns = [ + (column, type) + for column, type in zip(column_names, column_types) + if column not in columns_to_remove + ] + temp_column_names, temp_column_types = zip(*temp_columns) + return (list(temp_column_names), list(temp_column_types)) diff --git a/mokapot/tabular_data/csv.py b/mokapot/tabular_data/csv.py new file mode 100644 index 00000000..0e817713 --- /dev/null +++ b/mokapot/tabular_data/csv.py @@ -0,0 +1,112 @@ +from pathlib import Path +from typing import Generator + +import numpy as np +import pandas as pd +from typeguard import typechecked + +from mokapot.tabular_data import TabularDataReader, TabularDataWriter + + +@typechecked +class CSVFileReader(TabularDataReader): + """ + A tabular data reader for reading CSV files. + + Attributes: + ----------- + file_name : Path + The path to the CSV file. + stdargs : dict + Arguments for reading CSV file passed on to the pandas + `read_csv` function. + """ + + def __init__(self, file_name: Path, sep: str = "\t"): + self.file_name = file_name + self.stdargs = {"sep": sep, "index_col": False} + + def __str__(self): + return f"CSVFileReader({self.file_name=})" + + def __repr__(self): + return f"CSVFileReader({self.file_name=},{self.stdargs=})" + + def get_column_names(self) -> list[str]: + df = pd.read_csv(self.file_name, **self.stdargs, nrows=0) + return df.columns.tolist() + + def get_column_types(self) -> list[np.dtype]: + df = pd.read_csv(self.file_name, **self.stdargs, nrows=2) + return df.dtypes.tolist() + + def read(self, columns: list[str] | None = None) -> pd.DataFrame: + result = pd.read_csv(self.file_name, usecols=columns, **self.stdargs) + return result if columns is None else result[columns] + + def get_chunked_data_iterator( + self, chunk_size: int, columns: list[str] | None = None + ) -> Generator[pd.DataFrame, None, None]: + for chunk in pd.read_csv( + self.file_name, + usecols=columns, + chunksize=chunk_size, + **self.stdargs, + ): + yield chunk if columns is None else chunk[columns] + + def get_default_extension(self) -> str: + return ".csv" + + +@typechecked +class CSVFileWriter(TabularDataWriter): + """ + CSVFileWriter class for writing tabular data to a CSV file. + + Attributes: + ----------- + file_name : Path + The file path where the CSV file will be written. + + sep : str, optional + The separator string used to separate fields in the CSV file. + Default is tab character ("\t"). + """ + + file_name: Path + + def __init__( + self, + file_name: Path, + columns: list[str], + column_types: list[np.dtype], + sep: str = "\t", + ): + super().__init__(columns, column_types) + self.file_name = file_name + self.stdargs = {"sep": sep, "index": False} + + def __str__(self): + return f"CSVFileWriter({self.file_name=},{self.columns=})" + + def __repr__(self): + return ( + f"CSVFileWriter({self.file_name=},{self.columns=},{self.stdargs=})" + ) + + def initialize(self): + # Just write header information + df = pd.DataFrame(columns=self.columns) + df.to_csv(self.file_name, **self.stdargs) + + def finalize(self): + # no need to do anything here + pass + + def append_data(self, data: pd.DataFrame): + self.check_valid_data(data) + data.to_csv(self.file_name, mode="a", header=False, **self.stdargs) + + def get_associated_reader(self): + return CSVFileReader(self.file_name, sep=self.stdargs["sep"]) diff --git a/mokapot/tabular_data/format_chooser.py b/mokapot/tabular_data/format_chooser.py new file mode 100644 index 00000000..d9a074a9 --- /dev/null +++ b/mokapot/tabular_data/format_chooser.py @@ -0,0 +1,83 @@ +import warnings +from pathlib import Path + +import numpy as np +from typeguard import typechecked + +from mokapot.tabular_data import ( + BufferType, + BufferedWriter, + ColumnMappedReader, + ColumnSelectReader, + CSVFileReader, + CSVFileWriter, + ParquetFileReader, + ParquetFileWriter, + SqliteWriter, + TabularDataReader, + TabularDataWriter, +) + +CSV_SUFFIXES = [".csv", ".pin", ".tab", ".csv"] +PARQUET_SUFFIXES = [".parquet"] +SQLITE_SUFFIXES = [".db"] + + +@typechecked +def reader_from_path( + file_name: Path, + column_map: dict[str, str] | None = None, + only_columns: list[str] | None = None, + **kwargs, +) -> TabularDataReader: + # Currently, we look only at the suffix, however, in the future we + # could also look into the file itself (is it ascii? does it have + # some "magic bytes"? ...) + + suffix = file_name.suffix + if suffix in CSV_SUFFIXES: + reader = CSVFileReader(file_name, **kwargs) + elif suffix in PARQUET_SUFFIXES: + reader = ParquetFileReader(file_name, **kwargs) + else: + # Fallback + warnings.warn( + f"Suffix '{suffix}' not recognized in file name '{file_name}'." + " Falling back to CSV..." + ) + reader = CSVFileReader(file_name, **kwargs) + + if only_columns is not None: + reader = ColumnSelectReader(reader, only_columns) + + if column_map is not None: + reader = ColumnMappedReader(reader, column_map) + + return reader + + +def writer_from_suffix( + file_name: Path, + columns: list[str], + column_types: list[np.dtype], + buffer_size: int = 0, + buffer_type: BufferType = BufferType.DataFrame, + **kwargs, +) -> TabularDataWriter: + suffix = file_name.suffix + if suffix in CSV_SUFFIXES: + writer = CSVFileWriter(file_name, columns, column_types, **kwargs) + elif suffix in PARQUET_SUFFIXES: + writer = ParquetFileWriter(file_name, columns, column_types, **kwargs) + elif suffix in SQLITE_SUFFIXES: + writer = SqliteWriter(file_name, columns, column_types, **kwargs) + else: # Fallback + warnings.warn( + f"Suffix '{suffix}' not recognized in file name '{file_name}'." + " Falling back to CSV..." + ) + writer = CSVFileWriter(file_name, columns, column_types, **kwargs) + + if buffer_size > 1: + writer = BufferedWriter(writer, buffer_size, buffer_type) + return writer diff --git a/mokapot/tabular_data/parquet.py b/mokapot/tabular_data/parquet.py new file mode 100644 index 00000000..9aaba48e --- /dev/null +++ b/mokapot/tabular_data/parquet.py @@ -0,0 +1,129 @@ +from pathlib import Path +from typing import Generator + +import numpy as np +import pandas as pd +import pyarrow as pa +from pyarrow import parquet as pq +from typeguard import typechecked + +from mokapot.tabular_data import TabularDataWriter, TabularDataReader + + +@typechecked +class ParquetFileWriter(TabularDataWriter): + """ + This class is responsible for writing tabular data into Parquet files. + + + Attributes: + ----------- + file_name : Path + The path to the Parquet file being written. + """ + + file_name: Path + + def __init__( + self, + file_name: Path, + columns: list[str], + column_types: list[np.dtype], + ): + super().__init__(columns, column_types) + self.file_name = file_name + self.writer = None + + def __str__(self): + return f"ParquetFileWriter({self.file_name=},{self.columns=})" + + def __repr__(self): + return f"ParquetFileWriter({self.file_name=},{self.columns=})" + + @staticmethod + def _from_numpy_dtype(type): + if type == "object": + return pa.string() + else: + return pa.from_numpy_dtype(type) + + def _get_schema(self): + schema = [ + (name, ParquetFileWriter._from_numpy_dtype(type)) + for name, type in zip(self.columns, self.column_types) + ] + return pa.schema(schema) + + def initialize(self): + if len(self.column_types) > 0: + self.writer = pq.ParquetWriter( + self.file_name, schema=self._get_schema() + ) + + def finalize(self): + self.writer.close() + + def append_data(self, data: pd.DataFrame): + if self.writer is None: + # Infer the schema from the first dataframe + if self.column_types is None or len(self.column_types) == 0: + self.column_types = data.dtypes.to_list() + self.initialize() + + schema = self._get_schema() + table = pa.Table.from_pandas(data, preserve_index=False, schema=schema) + self.writer.write_table(table) + + def write(self, data: pd.DataFrame): + data.to_parquet(self.file_name, index=False) + + def get_associated_reader(self): + return ParquetFileReader(self.file_name) + + +@typechecked +class ParquetFileReader(TabularDataReader): + """ + A class for reading Parquet files and retrieving data in tabular format. + + Attributes: + ----------- + file_name : Path + The path to the Parquet file. + """ + + def __init__(self, file_name: Path): + self.file_name = file_name + + def __str__(self): + return f"ParquetFileReader({self.file_name=})" + + def __repr__(self): + return f"ParquetFileReader({self.file_name=})" + + def get_column_names(self) -> list[str]: + return pq.ParquetFile(self.file_name).schema.names + + def get_column_types(self) -> list[np.dtype]: + schema = pq.ParquetFile(self.file_name).schema + pq_types = schema.to_arrow_schema().types + return [np.dtype(type.to_pandas_dtype()) for type in pq_types] + + def read(self, columns: list[str] | None = None) -> pd.DataFrame: + result = pq.read_table(self.file_name, columns=columns).to_pandas() + return result + + def get_chunked_data_iterator( + self, chunk_size: int, columns: list[str] | None = None + ) -> Generator[pd.DataFrame, None, None]: + pf = pq.ParquetFile(self.file_name) + + for i, record_batch in enumerate( + pf.iter_batches(chunk_size, columns=columns) + ): + df = record_batch.to_pandas() + df.index += i * chunk_size + yield df + + def get_default_extension(self) -> str: + return ".parquet" diff --git a/mokapot/tabular_data/sqlite.py b/mokapot/tabular_data/sqlite.py new file mode 100644 index 00000000..fc715f5f --- /dev/null +++ b/mokapot/tabular_data/sqlite.py @@ -0,0 +1,103 @@ +import sqlite3 +from abc import ABC +from pathlib import Path + +import numpy as np +import pandas as pd +from typeguard import typechecked + +from mokapot.tabular_data import TabularDataWriter + + +@typechecked +class SqliteWriter(TabularDataWriter, ABC): + """ + SqliteWriter class for writing tabular data to SQLite database. + """ + + connection: sqlite3.Connection + + def __init__( + self, + database: str | Path | sqlite3.Connection, + columns: list[str], + column_types: list | None = None, + ) -> None: + super().__init__(columns, column_types) + if isinstance(database, sqlite3.Connection): + self.file_name = None + self.connection = database + else: + self.file_name = database + self.connection = sqlite3.connect(self.file_name) + + def __str__(self): + return f"SqliteFileWriter({self.file_name=},{self.columns=})" + + def __repr__(self): + return f"SqliteFileWriter({self.file_name=},{self.columns=})" + + def initialize(self): + # Nothing to do here, we expect the table(s) to already exist + pass + + def finalize(self): + self.connection.commit() + self.connection.close() + + def append_data(self, data: pd.DataFrame): + # Must be implemented in derived class + # todo: discuss: maybe we can supply also a default implementation in + # this class given the table name and sql column names. + raise NotImplementedError + + def get_associated_reader(self): + # Currently there is no SqliteReader and also no need for it + raise NotImplementedError("SqliteWriter has no associated reader yet.") + + +@typechecked +class ConfidenceSqliteWriter(SqliteWriter): + def __init__( + self, + database: str | Path | sqlite3.Connection, + columns: list[str], + column_types: list[np.dtype], + level: str = "psms", + qvalue_column: str = "q-value", + pep_column: str = "posterior_error_prob", + ) -> None: + super().__init__(database, columns, column_types) + self.level_cols = { + "precursors": ["PRECURSOR_VALIDATION", "PCM_ID", "Precursor"], + "modifiedpeptides": [ + "MODIFIED_PEPTIDE_VALIDATION", + "MODIFIED_PEPTIDE_ID", + "ModifiedPeptide", + ], + "peptides": ["PEPTIDE_VALIDATION", "PEPTIDE_ID", "peptide"], + "peptidegroups": [ + "PEPTIDE_GROUP_VALIDATION", + "PEPTIDE_GROUP_ID", + "PeptideGroup", + ], + } + self.level = level + self.qvalue_column = qvalue_column + self.pep_column = pep_column + + def get_query(self, level): + if level == "psms": + query = "UPDATE CANDIDATE SET PSM_FDR = :q_value, SVM_SCORE = :score, POSTERIOR_ERROR_PROBABILITY = :posterior_error_prob WHERE CANDIDATE_ID = :PSMId;" # noqa: E501 + else: + table_name, table_id_col, mokapot_id_col = self.level_cols[level] + query = f"INSERT INTO {table_name}({table_id_col},FDR,PEP,SVM_SCORE) VALUES(:{mokapot_id_col},:q_value,:posterior_error_prob,:score)" # noqa: E501 + return query + + def append_data(self, data): + query = self.get_query(self.level) + data = data.to_dict("records") + for row in data: + row["q_value"] = row[self.qvalue_column] + row["posterior_error_prob"] = row[self.pep_column] + self.connection.executemany(query, data) diff --git a/mokapot/streaming.py b/mokapot/tabular_data/streaming.py similarity index 63% rename from mokapot/streaming.py rename to mokapot/tabular_data/streaming.py index b2fdcae8..315541f8 100644 --- a/mokapot/streaming.py +++ b/mokapot/tabular_data/streaming.py @@ -2,17 +2,20 @@ Helper classes and methods used for streaming of tabular data. """ - from __future__ import annotations +import warnings from typing import Generator, Callable, Iterator -import pandas as pd import numpy as np +import pandas as pd from typeguard import typechecked -import pyarrow as pa -from mokapot.tabular_data import TabularDataReader, TableType +from mokapot.tabular_data import ( + BufferType, + TabularDataReader, + TabularDataWriter, +) @typechecked @@ -26,6 +29,7 @@ class JoinedTabularDataReader(TabularDataReader): A list of 'TabularDataReader' objects representing the individual data sources. """ + readers: list[TabularDataReader] def __init__(self, readers: list[TabularDataReader]): @@ -108,7 +112,7 @@ def __init__( self, reader: TabularDataReader, column: str, - dtype: np.dtype | pa.DataType, + dtype: np.dtype, func: Callable, ): self.reader = reader @@ -122,12 +126,18 @@ def get_column_names(self) -> list[str]: def get_column_types(self) -> list: return self.reader.get_column_types() + [self.dtype] - def _reader_columns(self, columns: list[str]): - return [column for column in columns if column != self.column] + def _reader_columns(self, columns: list[str] | None): + # todo: performance: Currently, we need to read all columns, since we + # don't know what's needed in the computation. This could be made more + # efficient by letting the class know which columns those are. + return None def read(self, columns: list[str] | None = None) -> pd.DataFrame: df = self.reader.read(self._reader_columns(columns)) - if columns is not None or self.column in columns: + # We need to compute the result column only in two cases: + # a) all columns are requested (columns = None) + # b) the computed column is requested explicitly + if columns is None or self.column in columns: df[self.column] = self.func(df) return df if columns is None else df[columns] @@ -143,7 +153,8 @@ def get_chunked_data_iterator( df = next(iterator) except StopIteration: break - if columns is not None or self.column in columns: + # See comments in `read` for explanation + if columns is None or self.column in columns: df[self.column] = self.func(df) yield df if columns is None else df[columns] @@ -173,6 +184,7 @@ class MergedTabularDataReader(TabularDataReader): column_types : list List of column types for the merged data. """ + def __init__( self, readers: list[TabularDataReader], @@ -185,22 +197,21 @@ def __init__( self.descending = descending self.reader_chunk_size = reader_chunk_size - assert len(readers) > 0, "At least one data reader is required" + if len(readers) == 0: + raise ValueError("At least one data reader is required") + self.column_names = readers[0].get_column_names() self.column_types = readers[0].get_column_types() - # todo: all those asserts should raise an exception, - # could happen in production too for reader in readers: - assert ( - reader.get_column_names() == self.column_names - ), "Column names do not match" - assert ( - reader.get_column_types() == self.column_types - ), "Column types do not match" - assert ( - priority_column in self.column_names - ), "Priority column not found" + if not reader.get_column_names() == self.column_names: + raise ValueError("Column names do not match") + + if not reader.get_column_types() == self.column_types: + raise ValueError("Column types do not match") + + if priority_column not in self.column_names: + raise ValueError("Priority column not found") def get_column_names(self) -> list[str]: return self.column_names @@ -211,7 +222,7 @@ def get_column_types(self) -> list: def get_row_iterator( self, columns: list[str] | None = None, - row_type: TableType = TableType.DataFrame, + row_type: BufferType = BufferType.DataFrame, ) -> Iterator[pd.DataFrame | dict | np.record]: def iterate_over_df(df: pd.DataFrame) -> Iterator: for i in range(len(df)): @@ -233,13 +244,13 @@ def iterate_over_records(df: pd.DataFrame) -> Iterator: records = df.to_records(index=False) return iter(records) - if row_type == TableType.DataFrame: + if row_type == BufferType.DataFrame: iterate_over_chunk = iterate_over_df get_value = get_value_df - elif row_type == TableType.Dicts: + elif row_type == BufferType.Dicts: iterate_over_chunk = iterate_over_dicts get_value = get_value_dict - elif row_type == TableType.Records: + elif row_type == BufferType.Records: iterate_over_chunk = iterate_over_records get_value = get_value_dict else: @@ -343,3 +354,138 @@ def merge_readers( ) iterator = reader.get_chunked_data_iterator(chunk_size=1) return iterator + + +@typechecked +class BufferedWriter(TabularDataWriter): + """ + This class represents a buffered writer for tabular data. It allows + writing data to a tabular data writer in batches, reducing the + number of write operations. + + Attributes: + ----------- + writer : TabularDataWriter + The tabular data writer to which the data will be written. + buffer_size : int + The number of records to buffer before writing to the writer. + buffer_type : TableType + The type of buffer being used. Can be one of TableType.DataFrame, + TableType.Dicts, or TableType.Records. + buffer : pd.DataFrame or list of dictionaries or np.recarray or None + The buffer containing the tabular data to be written. + The buffer type depends on the buffer_type attribute. + """ + + writer: TabularDataWriter + buffer_size: int + buffer_type: BufferType + buffer: pd.DataFrame | list[dict] | np.recarray | None + + def __init__( + self, + writer: TabularDataWriter, + buffer_size=1000, + buffer_type=BufferType.DataFrame, + ): + super().__init__(writer.columns, writer.column_types) + self.writer = writer + self.buffer_size = buffer_size + self.buffer_type = buffer_type + self.buffer = None + # For BufferedWriters it is extremely important that they are + # correctly initialized and finalized, so we make sure + self.finalized = False + self.initialized = False + + def __del__(self): + if self.initialized and not self.finalized: + warnings.warn( + f"BufferedWriter not finalized (buffering: {self.writer})" + ) + + def _buffer_slice( + self, + start: int = 0, + end: int | None = None, + as_dataframe: bool = False, + ): + if self.buffer_type == BufferType.DataFrame: + slice = self.buffer.iloc[start:end] + else: + slice = self.buffer[start:end] + if as_dataframe and not isinstance(slice, pd.DataFrame): + return pd.DataFrame(slice) + else: + return slice + + def _write_buffer(self, force=False): + if self.buffer is None: + return + while len(self.buffer) >= self.buffer_size: + self.writer.append_data( + self._buffer_slice(end=self.buffer_size, as_dataframe=True) + ) + self.buffer = self._buffer_slice( + start=self.buffer_size, + ) + if force and len(self.buffer) > 0: + slice = self._buffer_slice(as_dataframe=True) + self.writer.append_data(slice) + self.buffer = None + + def append_data(self, data: pd.DataFrame | dict | list[dict] | np.record): + assert self.initialized and not self.finalized + + if self.buffer_type == BufferType.DataFrame: + if not isinstance(data, pd.DataFrame): + raise TypeError( + "Parameter `data` must be of type DataFrame," + f" not {type(data)}" + ) + + if self.buffer is None: + self.buffer = data.copy(deep=True) + else: + self.buffer = pd.concat( + [self.buffer, data], axis=0, ignore_index=True + ) + elif self.buffer_type == BufferType.Dicts: + if isinstance(data, dict): + data = [data] + if not (isinstance(data, list) and isinstance(data[0], dict)): + raise TypeError( + "Parameter `data` must be of type dict or list[dict]," + f" not {type(data)}" + ) + if self.buffer is None: + self.buffer = [] + self.buffer += data + elif self.buffer_type == BufferType.Records: + if self.buffer is None: + self.buffer = np.recarray(shape=(0,), dtype=data.dtype) + self.buffer = np.append(self.buffer, data) + else: + raise ValueError(f"Unknown buffer type {self.buffer_type}") + + self._write_buffer() + + def check_valid_data(self, data: pd.DataFrame): + return self.writer.check_valid_data(data) + + def write(self, data: pd.DataFrame): + self.writer.write(data) + + def initialize(self): + assert not self.initialized + self.initialized = True + self.writer.initialize() + + def finalize(self): + assert self.initialized + self.finalized = True # Only for checking whether this got called + self._write_buffer(force=True) + self.writer.finalize() + + def get_associated_reader(self): + return self.writer.get_associated_reader() diff --git a/mokapot/tabular_data/target_decoy_writer.py b/mokapot/tabular_data/target_decoy_writer.py new file mode 100644 index 00000000..eb9cea30 --- /dev/null +++ b/mokapot/tabular_data/target_decoy_writer.py @@ -0,0 +1,66 @@ +from typing import Sequence + +import pandas as pd +from typeguard import typechecked + +from mokapot.tabular_data import TabularDataWriter + + +@typechecked +class TargetDecoyWriter(TabularDataWriter): + def __init__( + self, + writers: Sequence[TabularDataWriter], + write_decoys: bool = True, + target_column: str | None = None, + decoy_column: str | None = None, + ): + super().__init__( + writers[0].get_column_names(), writers[0].get_column_types() + ) + self.writers = writers + self.write_decoys = write_decoys + self.target_column = target_column + self.decoy_column = decoy_column + self.output_columns = writers[0].get_column_names() + + assert (target_column is None) != ( + decoy_column is None + ), "Exactly one of `target_column` and `decoy_column` must be given" + + def initialize(self): + for writer in self.writers: + writer.initialize() + + def finalize(self): + for writer in self.writers: + writer.finalize() + + def check_valid_data(self, data): + # We let the `self.writers` to the validation + pass + + def append_data(self, data: pd.DataFrame): + out_columns = self.output_columns + writers = self.writers + write_combined = (len(writers) == 1) and self.write_decoys + + if write_combined: + targets = None + elif self.target_column is not None: + targets = data[self.target_column] + else: + targets = ~data[self.decoy_column] + + assert write_combined or targets.dtype == bool + + if write_combined: + # Write decoys and targets combined + writers[0].append_data(data.loc[:, out_columns]) + elif self.write_decoys: + # Write decoys and targets separately + writers[0].append_data(data.loc[targets, out_columns]) + writers[1].append_data(data.loc[~targets, out_columns]) + else: + # Write targets only + writers[0].append_data(data.loc[targets, out_columns]) diff --git a/mokapot/tdmodel.py b/mokapot/tdmodel.py new file mode 100644 index 00000000..b2fa6bab --- /dev/null +++ b/mokapot/tdmodel.py @@ -0,0 +1,270 @@ +from abc import abstractmethod, ABC + +import numpy as np +import scipy as sp +from typeguard import typechecked + + +# This file (class) is only for checking validity of TD modelling assumptions. + + +@typechecked +def set_mu_std(dist: sp.stats.rv_continuous, mu: float, std: float): + """Modifies distribution parameters to have specified mean and standard + deviation. + + Note: the input distribution needs to have finite mean and standard + deviation for this method to work. + + Parameters + ---------- + dist : sp.stats.rv_continuous + The continuous random variable distribution object. + mu : float + The desired mean value for the distribution. + std : float + The desired standard deviation value for the distribution. + + Returns + ------- + dist + The distribution object with updated mean and standard deviation. + """ + kwds = dist.kwds + kwds["loc"] = 0 + kwds["scale"] = 1 + rv0 = dist.dist(**kwds) + kwds["scale"] = std / rv0.std() + rv1 = dist.dist(**kwds) + kwds["loc"] = mu - rv1.mean() + return dist.dist(**kwds) + + +@typechecked +def set_support(dist, lower: float, upper: float): + """Modifies distribution object to have fixed support. + + Note: the input distribution must have finite support already. + + Parameters + ---------- + dist : sp.stats.rv_continuous + The continuous random variable distribution object. + lower : float + The new lower limit of the support. + upper : float + The new upper limit of the support. + + Returns + ------- + dist + The distribution object with updated support. + """ + kwds = dist.kwds + kwds["loc"] = 0 + kwds["scale"] = 1 + rv0 = dist.dist(**kwds) + kwds["scale"] = (upper - lower) / (rv0.support()[1] - rv0.support()[0]) + rv1 = dist.dist(**kwds) + kwds["loc"] = lower - rv1.support()[0] + return dist.dist(**kwds) + + +class TDModel(ABC): + """Abstract base class for target-decoy models. + + Attributes: + R0 (object): The distribution model for decoy scores. + R1 (object): The distribution model for true target scores. + pi0 (float): The fraction of foreign spectra (i.e. spectra for which + the generating spectrum is not in the database, see [Keich 2015]. + + Methods: + sample_decoys(N): + Generates N decoy scores from the (initial) decoy score + distribution. + + sample_true_targets(N): + Generates N true target scores from the (initial) target score + distribution. + + sample_targets(N, include_is_fd=True, shuffle_result=True): + Generates N target scores by sampling from both the target + and decoy score distributions. + + sample_scores(N): + Abstract method for generating N target and decoy scores. + + decoy_pdf(x): + Computes the probability density function of the decoy score + distribution at x. + + true_target_pdf(x): + Computes the probability density function of the target score + distribution at x. + + decoy_cdf(x): + Computes the cumulative distribution function of the decoy score + distribution at x. + + true_target_cdf(x): + Computes the cumulative distribution function of the target score + distribution at x. + + true_pep(x): + Computes the posterior error probability for a given score x. + + true_fdr(x): + Computes the false discovery rate for a given score x. + + get_sampling_pdfs(x): + Abstract method for getting the sampling PDFs for a given score x. + """ + + def __init__(self, R0, R1, pi0): + self.R0 = R0 + self.R1 = R1 + self.pi0 = pi0 + + def sample_decoys(self, N): + return self.R0.rvs(N) + + def sample_true_targets(self, N): + return self.R1.rvs(N) + + def sample_targets(self, N, include_is_fd=True, shuffle_result=True): + NT = N + NT0 = int(np.round(self.pi0 * NT)) + NT1 = NT - NT0 + R0 = self.R0 + R1 = self.R1 + + nat1 = R1.rvs(NT1) + nat0 = R0.rvs(NT1) + target_scores = np.concatenate((np.maximum(nat1, nat0), R0.rvs(NT0))) + is_fd = np.concatenate((nat1 < nat0, np.full(NT0, True))) + + if shuffle_result: + indices = np.arange(target_scores.shape[0]) + np.random.shuffle(indices) + target_scores = target_scores[indices] + is_fd = is_fd[indices] + + if include_is_fd: + return target_scores, is_fd + else: + return target_scores + + @abstractmethod + def sample_scores(self, N): + pass + + def _sample_both(self, NT, ND): + target_scores, is_fd = self.sample_targets(NT, include_is_fd=True) + decoy_scores = self.sample_decoys(ND) + return target_scores, decoy_scores, is_fd + + @staticmethod + def _sort_and_return(scores, is_target, is_fd): + sort_idx = np.argsort(-scores) + sorted_scores = scores[sort_idx] + is_target = is_target[sort_idx] + is_fd = is_fd[sort_idx] + return sorted_scores, is_target, is_fd + + def decoy_pdf(self, x): + return self.R0.pdf(x) + + def true_target_pdf(self, x): + return self.R1.pdf(x) + + def decoy_cdf(self, x): + return self.R0.cdf(x) + + def true_target_cdf(self, x): + return self.R1.cdf(x) + + def true_pep(self, x): + T_pdf, TT_pdf, FT_pdf, D_pdf, pi0 = self.get_sampling_pdfs(x) + return pi0 * FT_pdf / T_pdf + + def true_fdr(self, x): + if any(np.diff(x) < 0): + raise ValueError("x must be non-decreasing, but wasn't'") + + # This pi0 is in both cases the Storey pi0, not the Keich pi0 + T_pdf, TT_pdf, FT_pdf, D_pdf, FDR = self.get_sampling_pdfs(x) + + fdr = FDR * np.flip( + np.cumsum(np.flip(FT_pdf)) / np.cumsum(np.flip(T_pdf)) + ) + + return fdr + + @staticmethod + def _integrate(func_values, x): + # Note: this is a bit primitive but does it's job here. Nicer would be + # an adapted higher order integration rule that integrates over the + # decoy probability density (which is all we need this method for), but + # this one does the job + return sp.integrate.trapz(func_values, x) + + def _get_input_pdfs_and_cdfs(self, x): + pi0 = self.pi0 + X0_pdf = self.true_target_pdf(x) + X0_cdf = self.true_target_cdf(x) + X_pdf = (1 - pi0) * X0_pdf + X_cdf = pi0 + (1 - pi0) * X0_cdf + Y_pdf = self.decoy_pdf(x) + Y_cdf = self.decoy_cdf(x) + return X_pdf, X_cdf, Y_pdf, Y_cdf + + @abstractmethod + def get_sampling_pdfs(self, x): + pass + + +class TDCModel(TDModel): + """A TDModel class for target decoy competition or concatenated search""" + + def sample_scores(self, N): + target_scores, decoy_scores, is_fd = self._sample_both(N, N) + is_target = target_scores >= decoy_scores + all_scores = np.where(is_target, target_scores, decoy_scores) + return self._sort_and_return(all_scores, is_target, is_fd) + + def get_sampling_pdfs(self, x): + X_pdf, X_cdf, Y_pdf, Y_cdf = self._get_input_pdfs_and_cdfs(x) + + DP = TDModel._integrate(X_cdf * Y_cdf * Y_pdf, x) + FDR = DP / (1 - DP) + T_pdf = (X_pdf * Y_cdf**2 + X_cdf * Y_cdf * Y_pdf) / (1 - DP) + TT_pdf = X_pdf * Y_cdf**2 / (1 - 2 * DP) + FT_pdf = (X_cdf * Y_cdf * Y_pdf) / DP + D_pdf = FT_pdf + return T_pdf, TT_pdf, FT_pdf, D_pdf, FDR + + +class STDSModel(TDModel): + """A TDModel class for separate search""" + + def sample_scores(self, NT, ND=None): + ND = NT if ND is None else ND + target_scores, decoy_scores, is_fd = self._sample_both(NT, ND) + all_scores = np.concatenate((target_scores, decoy_scores)) + is_target = np.concatenate((np.full(NT, True), np.full(ND, False))) + is_fd = np.concatenate(( + is_fd, + np.full(ND, False), + )) # is_fd value for decoys is irrelevant + return self._sort_and_return(all_scores, is_target, is_fd) + + def get_sampling_pdfs(self, x): + X_pdf, X_cdf, Y_pdf, Y_cdf = self._get_input_pdfs_and_cdfs(x) + + FDR = TDModel._integrate(Y_pdf * X_cdf, x) + T_pdf = X_pdf * Y_cdf + X_cdf * Y_pdf + TT_pdf = X_pdf * Y_cdf / (1 - FDR) + FT_pdf = X_cdf * Y_pdf / FDR + D_pdf = Y_pdf + return T_pdf, TT_pdf, FT_pdf, D_pdf, FDR diff --git a/mokapot/utils.py b/mokapot/utils.py index 49ae368c..b195b3d1 100644 --- a/mokapot/utils.py +++ b/mokapot/utils.py @@ -1,20 +1,13 @@ """ Utility functions """ - -from __future__ import annotations - -import itertools import gzip +import itertools from pathlib import Path -from typing import Union, List, Iterator, Any, NewType, Dict +from typing import Union, Iterator, Any, NewType, Dict import numpy as np import pandas as pd -from mokapot.tabular_data import TabularDataReader - -from .constants import MERGE_SORT_CHUNK_SIZE -import pyarrow.parquet as pq from typeguard import typechecked @@ -35,7 +28,6 @@ def groupby_max(df, by_cols, max_col, rng): .drop_duplicates(list(by_cols), keep="last") .index ) - return idx @@ -128,58 +120,6 @@ def get_next_row( return max_row -@typechecked -def csv_row_iterator(path: Path) -> Iterator[DataRow]: - chunk_iterator = TabularDataReader.from_path( - path - ).get_chunked_data_iterator(chunk_size=MERGE_SORT_CHUNK_SIZE) - for chunk in chunk_iterator: - records = chunk.to_dict(orient="records") - yield from records - - -@typechecked -def parquet_row_iterator(path: Path) -> Iterator[DataRow]: - batch_iterator = pq.ParquetFile(path).iter_batches(MERGE_SORT_CHUNK_SIZE) - for record_batch in batch_iterator: - batch = record_batch.to_pylist() - yield from batch - - -@typechecked -def merge_sort(paths: list[Path], score_column: str): - if paths[0].suffix == ".parquet": - row_iterator_func = parquet_row_iterator - else: - row_iterator_func = csv_row_iterator - - row_iterator_dict = { - i: row_iterator_func(path) for i, path in enumerate(paths) - } - current_row_dict = { - i: next(row_iter) for i, row_iter in row_iterator_dict.items() - } - - while row_iterator_dict != {}: - row = get_next_row(row_iterator_dict, current_row_dict, score_column) - if row is not None: - yield row - - -def get_dataframe_from_records( - records: List[dict], - in_columns: List, - column_mapping: dict, - target_column: str = None, -): - df = pd.DataFrame.from_records(records, columns=in_columns) - if target_column: - if df[target_column].dtype == "object": - df[target_column] = df[target_column] == "True" - df = df.rename(columns=column_mapping) - return df - - @typechecked def convert_targets_column( data: pd.DataFrame, target_column: str diff --git a/mokapot/writers/__init__.py b/mokapot/writers/__init__.py deleted file mode 100644 index ad0073a6..00000000 --- a/mokapot/writers/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -"""Define the public functions for the writers""" - -from .txt import to_txt -from .flashlfq import to_flashlfq diff --git a/mokapot/writers/flashlfq.py b/mokapot/writers/flashlfq.py index 3e8b6d14..4edbe9fd 100644 --- a/mokapot/writers/flashlfq.py +++ b/mokapot/writers/flashlfq.py @@ -117,9 +117,7 @@ def _format_flashlfq(conf): if isinstance(proteins, str): # TODO: Add delimiter sniffing. - prots = peptides.loc[passing, proteins].str.replace( - "\t", "; ", regex=False - ) + prots = peptides.loc[passing, proteins].str.replace("\t", "; ", regex=False) elif proteins is None: prots = "" else: @@ -132,8 +130,7 @@ def _format_flashlfq(conf): num_missing = missing.sum() if num_missing: LOGGER.warning( - "- Discarding %i peptides that could not be mapped to protein " - "groups", + "- Discarding %i peptides that could not be mapped to protein " "groups", num_missing, ) out_df = out_df.loc[~missing, :] diff --git a/mokapot/writers/txt.py b/mokapot/writers/txt.py deleted file mode 100644 index 9170ab9a..00000000 --- a/mokapot/writers/txt.py +++ /dev/null @@ -1,101 +0,0 @@ -"""Writer to save results in a tab-delmited format""" - -from pathlib import Path -from collections import defaultdict - -import pandas as pd - - -def to_txt(conf, dest_dir=None, file_root=None, sep="\t", decoys=False): - """Save confidence estimates to delimited text files. - - Write the confidence estimates for each of the available levels - (i.e. PSMs, peptides, proteins) to separate flat text files using the - specified delimiter. If more than one collection of confidence estimates - is provided, they will be combined, yielding a single file for each level - specified by either dataset. - - Parameters - ---------- - conf : Confidence object or tuple of Confidence objects - One or more :py:class:`~mokapot.confidence.LinearConfidence` objects. - dest_dir : str or None, optional - The directory in which to save the files. :code:`None` will use the - current working directory. - file_root : str or None, optional - An optional prefix for the confidence estimate files. The suffix will - always be "mokapot.{level}.txt" where "{level}" indicates the level at - which confidence estimation was performed (i.e. PSMs, peptides, - proteins). - sep : str, optional - The delimiter to use. - decoys : bool, optional - Save decoys confidence estimates as well? - - Returns - ------- - list of str - The paths to the saved files. - - """ - - # todo: I think this function is only referenced in test_writer_txt and can - # be safely removed in the future (if the test is also removed of course) - try: - assert not isinstance(conf, str) - iter(conf) - except TypeError: - conf = [conf] - except AssertionError: - raise ValueError("'conf' should be a Confidence object, not a string.") - - file_base = "mokapot" - if file_root is not None: - file_base = file_root + "." + file_base - if dest_dir is not None: - file_base = Path(dest_dir, file_base) - - results = defaultdict(list) - for res in conf: - for level, qval_list in _get_level_data(res, decoys).items(): - results[level] += qval_list - - out_files = [] - for level, qval_list in results.items(): - out_file = str(file_base) + f".{level}.txt" - pd.concat(qval_list).to_csv(out_file, sep=sep, index=False) - out_files.append(out_file) - - return out_files - - -def _get_level_data(conf, decoys): - """Return the dataframes for each level. - - Parameters - ---------- - conf : a Confidence object - A LinearConfidence object. - decoys : bool - Should decoys be included? - - Returns - ------- - Dict - Each entry contains a level, dataframe pair. - """ - results = defaultdict(list) - for level, qvals in conf.confidence_estimates.items(): - if qvals is None: - continue - - results[level].append(qvals) - - if decoys: - for level, qvals in conf.decoy_confidence_estimates.items(): - if qvals is None: - continue - - results[f"decoy.{level}"].append(qvals) - - return results diff --git a/pyproject.toml b/pyproject.toml index d9721137..78c5441f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ dependencies = [ "typeguard>=4.1.5", "pyarrow>=15.0.0", "scipy>=1.13.0", + "filelock>=3.16.1", ] dynamic = ["version"] @@ -49,10 +50,15 @@ docs = [ "ipykernel>=5.3.0", "recommonmark>=0.5.0", ] -dev = [ + +[tool.uv] +dev-dependencies = [ "pre-commit>=2.7.1", "ruff>=0.4.4", "pytest>=8.2.2", + "flake8>=7.1.1", + "wheel>=0.44.0", + "pytest-cov>=5.0.0" ] [project.scripts] @@ -66,10 +72,14 @@ find = {namespaces = false} [tool.setuptools_scm] +[tool.pytest.ini_options] +testpaths = ["tests",] +norecursedirs = ["extra", ] + [tool.ruff] extend-exclude = ["docs/source/conf.py"] -line-length = 79 target-version = "py39" +exclude = ["docs", "notebooks", "*.ipynb"] [tool.ruff.lint] select = ["E", "F", "T20"] # T20 is for print() statements. @@ -80,3 +90,4 @@ select = ["E", "F", "T20"] # T20 is for print() statements. [tool.ruff.format] docstring-code-format = true +preview = true diff --git a/tests/conftest.py b/tests/conftest.py index de74f4ce..48b2c096 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,14 +6,15 @@ import sys from pathlib import Path -import pytest import numpy as np import pandas as pd -from mokapot import LinearPsmDataset, OnDiskPsmDataset +import pyarrow as pa +import pyarrow.parquet as pq +import pytest from triqler.qvality import getQvaluesFromScores + +from mokapot import LinearPsmDataset, OnDiskPsmDataset from mokapot.qvalues import tdc -import pyarrow.parquet as pq -import pyarrow as pa from mokapot.utils import convert_targets_column @@ -44,7 +45,7 @@ def psm_df_100(tmp_path): targets = { "specid": np.arange(50), "target": [True] * 50, - "scannr": np.random.randint(0, 100, 50), + "scannr": rng.integers(0, 100, 50), "calcmass": rng.uniform(500, 2000, size=50), "expmass": rng.uniform(500, 2000, size=50), "peptide": [_random_peptide(5, rng) for _ in range(50)], @@ -58,7 +59,7 @@ def psm_df_100(tmp_path): decoys = { "specid": np.arange(50, 100), "target": [False] * 50, - "scannr": np.random.randint(0, 100, 50), + "scannr": rng.integers(0, 100, 50), "calcmass": rng.uniform(500, 2000, size=50), "expmass": rng.uniform(500, 2000, size=50), "peptide": [_random_peptide(5, rng) for _ in range(50)], @@ -82,7 +83,7 @@ def psm_df_100_parquet(tmp_path): targets = { "specid": np.arange(50), "target": [True] * 50, - "scannr": np.random.randint(0, 100, 50), + "scannr": rng.integers(0, 100, 50), "calcmass": rng.uniform(500, 2000, size=50), "expmass": rng.uniform(500, 2000, size=50), "peptide": [_random_peptide(5, rng) for _ in range(50)], @@ -96,7 +97,7 @@ def psm_df_100_parquet(tmp_path): decoys = { "specid": np.arange(50, 100), "target": [False] * 50, - "scannr": np.random.randint(0, 100, 50), + "scannr": rng.integers(0, 100, 50), "calcmass": rng.uniform(500, 2000, size=50), "expmass": rng.uniform(500, 2000, size=50), "peptide": [_random_peptide(5, rng) for _ in range(50)], @@ -120,7 +121,8 @@ def psm_df_1000(tmp_path): targets = { "specid": np.arange(500), "target": [True] * 500, - "scannr": np.random.randint(0, 1000, 500), + "spectrum": np.arange(500), + "scannr": rng.integers(0, 1000, 500), "calcmass": rng.uniform(500, 2000, size=500), "expmass": rng.uniform(500, 2000, size=500), "peptide": [_random_peptide(5, rng) for _ in range(500)], @@ -142,13 +144,13 @@ def psm_df_1000(tmp_path): "specid": np.arange(500, 1000), "target": [False] * 500, "spectrum": np.arange(500), - "score2": rng.normal(size=500), - "scannr": np.random.randint(0, 1000, 500), + "scannr": rng.integers(0, 1000, 500), "calcmass": rng.uniform(500, 2000, size=500), "expmass": rng.uniform(500, 2000, size=500), "peptide": [_random_peptide(5, rng) for _ in range(500)], "proteins": ["_dummy" for _ in range(500)], "score": rng.normal(size=500), + "score2": rng.normal(size=500), "filename": "test.mzML", "ret_time": rng.uniform(0, 60 * 120, size=500), "charge": rng.choice([2, 3, 4], size=500), @@ -160,11 +162,13 @@ def psm_df_1000(tmp_path): ) fasta = tmp_path / "test_1000.fasta" - pin = tmp_path / "test.pin" with open(fasta, "w+") as fasta_ref: fasta_ref.write(fasta_data) + df = pd.concat([pd.DataFrame(targets), pd.DataFrame(decoys)]) - df.to_csv(pin, sep="\t", index=False) + + pin = tmp_path / "test.pin" + df.drop(columns=["score", "score2"]).to_csv(pin, sep="\t", index=False) return pin, df, fasta @@ -175,7 +179,7 @@ def psm_df_1000_parquet(tmp_path): targets = { "specid": np.arange(500), "target": [True] * 500, - "scannr": np.random.randint(0, 1000, 500), + "scannr": rng.integers(0, 1000, 500), "calcmass": rng.uniform(500, 2000, size=500), "expmass": rng.uniform(500, 2000, size=500), "peptide": [_random_peptide(5, rng) for _ in range(500)], @@ -198,7 +202,7 @@ def psm_df_1000_parquet(tmp_path): "target": [False] * 500, "spectrum": np.arange(500), "score2": rng.normal(size=500), - "scannr": np.random.randint(0, 1000, 500), + "scannr": rng.integers(0, 1000, 500), "calcmass": rng.uniform(500, 2000, size=500), "expmass": rng.uniform(500, 2000, size=500), "peptide": [_random_peptide(5, rng) for _ in range(500)], @@ -215,16 +219,18 @@ def psm_df_1000_parquet(tmp_path): ) fasta = tmp_path / "test_1000.fasta" - pf = tmp_path / "test.parquet" with open(fasta, "w+") as fasta_ref: fasta_ref.write(fasta_data) + df = pd.concat([pd.DataFrame(targets), pd.DataFrame(decoys)]) - df.to_parquet(pf, index=False) + + pf = tmp_path / "test.parquet" + df.drop(columns=["score", "score2"]).to_parquet(pf, index=False) return pf, df, fasta @pytest.fixture -def psms(psm_df_1000): +def psms_dataset(psm_df_1000): """A small LinearPsmDataset""" _, df, _ = psm_df_1000 psms = LinearPsmDataset( @@ -248,13 +254,11 @@ def psms(psm_df_1000): def psms_ondisk(): """A small OnDiskPsmDataset""" filename = Path("data", "scope2_FP97AA.pin") - df_spectra = pd.read_csv( - filename, sep="\t", usecols=["ScanNr", "ExpMass", "Label"] - ) + df_spectra = pd.read_csv(filename, sep="\t", usecols=["ScanNr", "ExpMass", "Label"]) with open(filename) as perc: columns = perc.readline().rstrip().split("\t") psms = OnDiskPsmDataset( - filename=filename, + filename, target_column="Label", spectrum_columns=["ScanNr", "ExpMass"], peptide_column="Peptide", @@ -302,14 +306,14 @@ def psms_ondisk(): @pytest.fixture def psms_ondisk_from_parquet(): """A small OnDiskPsmDataset""" - filename = Path("data/10k_psms_test.parquet") + filename = Path("data") / "10k_psms_test.parquet" df_spectra = pq.read_table( filename, columns=["ScanNr", "ExpMass", "Label"] ).to_pandas() df_spectra = convert_targets_column(df_spectra, "Label") columns = pq.ParquetFile(filename).schema.names psms = OnDiskPsmDataset( - filename=filename, + filename, target_column="Label", spectrum_columns=["ScanNr", "ExpMass"], peptide_column="Peptide", @@ -445,9 +449,7 @@ def targets_decoys_psms_scored(tmp_path): scores = scores[idx] label = label[idx] qval = tdc(scores, label) - pep = getQvaluesFromScores( - target_scores, decoy_scores, includeDecoys=True - )[1] + pep = getQvaluesFromScores(target_scores, decoy_scores, includeDecoys=True)[1] peptides = np.hstack([np.arange(1, n + 1), np.arange(1, n + 1)]) peptides.sort() df = pd.DataFrame( @@ -462,19 +464,13 @@ def targets_decoys_psms_scored(tmp_path): ], ) df["proteinIds"] = "dummy" - df[df["Label"] == 1].drop("Label", axis=1).to_csv( - psms_t, sep="\t", index=False - ) - df[df["Label"] == -1].drop("Label", axis=1).to_csv( - psms_d, sep="\t", index=False - ) + df[df["Label"] == 1].drop("Label", axis=1).to_csv(psms_t, sep="\t", index=False) + df[df["Label"] == -1].drop("Label", axis=1).to_csv(psms_d, sep="\t", index=False) return [psms_t, psms_d] -def _make_fasta( - num_proteins, peptides, peptides_per_protein, random_state, prefix="" -): +def _make_fasta(num_proteins, peptides, peptides_per_protein, random_state, prefix=""): """Create a FASTA string from a set of peptides Parameters @@ -498,9 +494,7 @@ def _make_fasta( lines = [] for protein in range(num_proteins): lines.append(f">{prefix}sp|test|test_{protein}") - lines.append( - "".join(list(random_state.choice(peptides, peptides_per_protein))) - ) + lines.append("".join(list(random_state.choice(peptides, peptides_per_protein)))) return lines @@ -508,8 +502,7 @@ def _make_fasta( def _random_peptide(length, random_state): """Generate a random peptide""" return "".join( - list(random_state.choice(list("ACDEFGHILMNPQSTVWY"), length - 1)) - + ["K"] + list(random_state.choice(list("ACDEFGHILMNPQSTVWY"), length - 1)) + ["K"] ) @@ -543,7 +536,7 @@ def __init__(self): self._has_proteins = False self.peptides = pd.DataFrame({ - "filename": "a/b/c.mzML", + "filename": Path("a") / "b" / "c.mzML", "calcmass": [1, 2], "ret_time": [60, 120], "charge": [2, 3], @@ -552,43 +545,12 @@ def __init__(self): "protein": ["A|B|C\tB|C|A", "A|B|C"], }) - self.confidence_estimates = {"peptides": self.peptides} - self.decoy_confidence_estimates = {"peptides": self.peptides} - return conf() -@pytest.fixture -def merge_sort_data(tmp_path): - filenames_csv = [tmp_path / f"merge_sort_{i}.csv" for i in range(3)] - filenames_parquet = [ - tmp_path / f"merge_sort_{i}.parquet" for i in range(3) - ] - df = pd.read_csv( - "data/10k_psms_test.pin", - sep="\t", - usecols=[ - "SpecId", - "Label", - "ScanNr", - "ExpMass", - "Peptide", - "Proteins", - ], - ) - df = df[:15] - df["score"] = np.arange(0.16, 0.01, -0.01) - for i, (file_csv, file_parquet) in enumerate( - zip(filenames_csv, filenames_parquet) - ): - df[i::3].to_csv(file_csv, sep="\t", index=False) - df[i::3].to_parquet(file_parquet, index=False) - return filenames_csv, filenames_parquet - - @pytest.fixture def confidence_write_data(): - filename = Path("data/confidence_results_test.tsv") + filename = Path("data") / "confidence_results_test.tsv" psm_df = pd.read_csv(filename, sep="\t") precursor_df = psm_df.drop_duplicates(subset=["Precursor"]) mod_pep_df = psm_df.drop_duplicates(subset=["ModifiedPeptide"]) @@ -692,7 +654,7 @@ def pytest_sessionstart(session): # pd.set_option("display.max_colwidth", None) #default 50 # Set max width for output of the whole data frame - pd.set_option("display.width", None) # default 80, None means auto-detect + pd.set_option("display.width", 1000) # default 80, None means auto-detect # Also set full precision # (see https://pandas.pydata.org/docs/user_guide/options.html) diff --git a/tests/helpers/cli.py b/tests/helpers/cli.py index 6aeef118..50ce696c 100644 --- a/tests/helpers/cli.py +++ b/tests/helpers/cli.py @@ -1,7 +1,7 @@ import contextlib +import io import logging import os -import io import subprocess from contextlib import redirect_stdout, redirect_stderr from typing import List, Any, Optional, Dict, Callable @@ -29,7 +29,10 @@ def catch_type_check(): except TypeCheckError as e: import traceback - logging.error("\n\nTypeCheckError: ", end="") + # This method is used internally, and thus the output should be printed + # directly to the console and not logged via the logging framework + print("\n\nTypeCheckError: ", end="") # noqa: T201 + # Print the exception with the stack trace excluding the last # entries which stem from the typeguard module frames = traceback.extract_tb(e.__traceback__) @@ -40,6 +43,27 @@ def catch_type_check(): raise +def flatten(lst): + """ + Parameters + ---------- + lst: + The list or tuple to be flattened. + + Returns + ------- + list + A flattened list containing all the elements from the input collection. + """ + result = [] + for i in lst: + if isinstance(i, list | tuple): + result.extend(flatten(i)) + else: + result.append(i) + return result + + def _run_cli( module: str, main_func: Callable, @@ -75,9 +99,10 @@ def _run_cli( else: run_in_subprocess = bool(run_in_subprocess) + params = flatten(params) params = [str(param) for param in params] if run_in_subprocess: - cmd = ["python", "-m", module] + params + cmd = ["uv", "run", "python", "-m", module] + params try: res = subprocess.run( cmd, check=True, capture_output=capture_output @@ -95,6 +120,14 @@ def _run_cli( "stdout": res.stdout.decode(), "stderr": res.stderr.decode(), } + + if res.returncode == 250: + raise ValueError(f"Mokapot returned error status {res.returncode}") + elif res.returncode != 0: + raise RuntimeError( + f"Mokapot returned error status {res.returncode}" + ) + elif capture_output: stdout_sink = io.StringIO() stderr_sink = io.StringIO() @@ -108,6 +141,7 @@ def _run_cli( with redirect_stderr(stderr_sink), redirect_stdout(stdout_sink): with catch_type_check(): main_func(params) + return { "stdout": stdout_sink.getvalue(), "stderr": stderr_sink.getvalue(), diff --git a/tests/helpers/math.py b/tests/helpers/math.py new file mode 100644 index 00000000..8bef0bd7 --- /dev/null +++ b/tests/helpers/math.py @@ -0,0 +1,74 @@ +import warnings + +import numpy as np +import pandas as pd +import scipy + + +def reduce_linear(x, y, rtol=1e-10): + """ + Removes unnecessary points from point set w.r.t. to linear interpolation. + + Can be used to test whether two sets of points returned from function + estimators (e.g. to estimate q-values from scores) and fed into linear + interpolators are equivalent, i.e. will lead to the same interpolated + function. + + Parameters + ---------- + x : array-like + The x-coordinates of the points. + y : array-like + The y-coordinates of the points. + rtol : float, optional + The relative tolerance to determine when points can be reduced + (default is 1e-10). + + Returns + ------- + x : array-like + Reduced x-coordinates of the points. + y : array-like + Reduced y-coordinates of the points. + """ + tol = rtol * (y.max() - y.min()) + # Compute linearly interpolated values ym_i at points x_i from the pairs + # (x_i-1, y_i-1) and (x_i+1, y_i+1) + ddx = x[:-2] - x[2:] + ddy = y[:-2] - y[2:] + ym = y[:-2] + ddy / ddx * (x[1:-1] - x[:-2]) + # Value of y_i at point x_i for comparison. If the difference to ym_i is + # near zero it can be eliminated. + yi = y[1:-1] + # Keep points with near zero diff plus the endpoints + keep = np.abs(ym - yi) > tol + keep = np.append(np.insert(keep, 0, True), True) + x = x[keep] + y = y[keep] + return x, y + + +def estimate_abs_int(x, y, mode=1): + """Estimate the normalized absolute difference between two curves""" + if isinstance(x, pd.Series): + x = x.values + if isinstance(y, pd.Series): + y = y.values + + if all(np.diff(x) <= 0): + x = x[::-1] + y = y[::-1] + assert all(np.diff(x) >= 0) + + a = np.abs(y) + # Determine possible 0 crossings of y (which means kinks in abs(y)) + with warnings.catch_warnings(): + # we sort out nans later + warnings.simplefilter("ignore") + x2 = (a[1:] * x[:-1] + a[:-1] * x[1:]) / (a[1:] + a[:-1]) + x2 = x2[~np.isnan(x2)] + # Add zero-crossing points to integral eval points (+ sort and make unique) + xn = np.unique(np.sort(np.concatenate((x2, x)))) + an = np.abs(np.interp(xn, x, y)) + # Trapezoidal rule is exact for piecewise linear functions + return scipy.integrate.trapezoid(an, xn) / (xn[-1] - xn[0]) diff --git a/tests/helpers/utils.py b/tests/helpers/utils.py new file mode 100644 index 00000000..df0c04c0 --- /dev/null +++ b/tests/helpers/utils.py @@ -0,0 +1,139 @@ +from pathlib import Path + + +class Interval: + def __init__(self, start, end): + self.start = start + self.end = end + + def __str__(self): + return f"[{self.start}, {self.end}]" + + def __repr__(self): + return f"[{self.start}, {self.end}]" + + def __contains__(self, value): + try: + return self.start <= value <= self.end + except RuntimeError: + return False + + +def count_lines(path: Path, *args): + """Count the number of lines in a file. + + Parameters + ---------- + path : Path + The path to the file. + + Returns + ------- + int + The number of lines in the file. + """ + path = Path(path, *args) + if not path.is_file(): + return None + with open(path, "r") as file: + lines = file.readlines() + return len(lines) + + +class FileCheck: + def __init__(self, dir_path: Path, ext_path, min, max): + self.passed, self.message = FileCheck._check( + dir_path, ext_path, min, max + ) + + @staticmethod + def _check(dir_path, ext_path, min, max): + """Check whether a file exists and has the correct length. + + Note: if `min` is set to zero or less, it is checked that the file + does *not* exist. + + Parameters + ---------- + dir_path : str + The directory path where the file is located. + ext_path : str + The file extension path. + min : int or None + The minimum number of lines expected in the file. If set to None, + there is no minimum limit. If smaller or equal to zero, the file + is required to NOT exist. + max : int or None + The maximum number of lines expected in the file. If set to None, + there is no maximum limit. + + Returns + ---------- + tuple + A tuple containing a boolean value and an error message string or + success message string. + """ + path = Path(dir_path, ext_path) + if min is not None and min <= 0: + if path.is_file(): + return False, f"File `{ext_path}` does not exist (but it did)" + else: + return True, f"File `{ext_path}` does not exist as it should" + + if not path.is_file(): + return False, f"File `{ext_path}` exists (but it didn't)" + + if min is not None: + line_count = count_lines(path) + if max is not None: + if line_count < min or line_count > max: + return ( + False, + f"Line count of `{ext_path}` in [{min}, {max}] (but was {line_count})", # noqa: E501 + ) + elif line_count < min: + return ( + False, + f"Line count of `{ext_path}` at least {min} (but was {line_count})", # noqa: E501 + ) + + return True, f"File `{ext_path}` exists and is ok." + + def __bool__(self): + return self.passed + + def __repr__(self): + return self.message + + def __str__(self): + return self.message + + +def file_check( + file_path, ext_path, expected_lines=None, min=None, max=None, diff=100 +): + if expected_lines is not None: + min, max = expected_lines - diff, expected_lines + diff + if min < 1 and expected_lines > 0: + min = 1 + return FileCheck(file_path, ext_path, min, max) + + +def file_exist(file_path, ext_path): + return file_check(file_path, ext_path) + + +def file_missing(file_path, ext_path): + return file_check(file_path, ext_path, min=0) + + +def file_min_len(file_path, ext_path, length): + return file_check(file_path, ext_path, min=length) + + +def file_exact_len(file_path, ext_path, length): + return file_check(file_path, ext_path, min=length, max=length) + + +def file_approx_len(file_path, ext_path, length, diff=100): + return file_check(file_path, ext_path, expected_lines=length, diff=diff) diff --git a/tests/system_tests/test_brew_rollup.py b/tests/system_tests/test_brew_rollup.py index f4b5099b..85bbf7b5 100644 --- a/tests/system_tests/test_brew_rollup.py +++ b/tests/system_tests/test_brew_rollup.py @@ -5,13 +5,22 @@ output, just that the expect outputs are created. """ -from __future__ import annotations - +import shutil from pathlib import Path from typing import List, Any -from mokapot.brew_rollup import compute_rollup_levels +import pytest +from filelock import FileLock +from pandas.testing import assert_series_equal + +from mokapot.rollup import compute_rollup_levels +from mokapot.tabular_data import ( + TabularDataReader, + CSVFileReader, + ParquetFileWriter, +) from ..helpers.cli import run_mokapot_cli, _run_cli +from ..helpers.math import estimate_abs_int def run_brew_rollup( @@ -24,86 +33,134 @@ def run_brew_rollup( ) -def test_rollup_10000(tmp_path): - """Test that basic cli works.""" - # path = tmp_path - path = Path("scratch", "testing") - path.mkdir(parents=True, exist_ok=True) +@pytest.fixture(scope="session") +def rollup_src_dirs(tmp_path_factory): + dest_dir = Path("scratch", "testing_rollup") + dest_dir.mkdir(parents=True, exist_ok=True) + pq_dest_dir = dest_dir / "parquet" + pq_dest_dir.mkdir(parents=True, exist_ok=True) retrain = False - recompute = False + recompute = retrain or False common_params = [ - "--dest_dir", - path, - "--max_workers", - 8, - "--test_fdr", - 0.10, - "--train_fdr", - 0.9, - "--verbosity", - 2, - "--subset_max_train", - 4000, - "--max_iter", - 10, + ("--dest_dir", dest_dir), + ("--max_workers", 8), + ("--test_fdr", 0.10), + ("--train_fdr", 0.05), + ("--verbosity", 2), + ("--subset_max_train", 4000), + ("--max_iter", 10), "--ensemble", "--keep_decoys", ] - if retrain or not Path.exists(path / "mokapot.model_fold-1.pkl"): - params = [ - Path("data", "percolator-noSplit-extended-10000.tab"), - *common_params, - "--save_models", - ] - run_mokapot_cli(params) - - if recompute or not Path.exists(path / "a.targets.precursors"): - params = [ - Path("data", "percolator-noSplit-extended-1000.tab"), - *common_params, - "--load_models", - *path.glob("mokapot.model_fold-*.pkl"), - "--file_root", - "a", - ] - run_mokapot_cli(params) - - if recompute or not Path.exists(path / "b.targets.precursors"): - params = [ - Path("data", "percolator-noSplit-extended-1000b.tab"), - *common_params, - "--load_models", - *path.glob("mokapot.model_fold-*.pkl"), - "--file_root", - "b", - ] - run_mokapot_cli(params) - - if recompute or not Path.exists(path / "c.targets.precursors"): - params = [ - Path("data", "percolator-noSplit-extended-1000c.tab"), - *common_params, - "--load_models", - *path.glob("mokapot.model_fold-*.pkl"), - "--file_root", - "c", - ] - run_mokapot_cli(params) + # In case we run the tests parallel with xdist, we may run into race + with FileLock(dest_dir / "rollup.lock"): + # Train mokapot on larger input file + if retrain or not Path.exists(dest_dir / "mokapot.model_fold-1.pkl"): + params = [ + Path("data", "percolator-noSplit-extended-10000.tab"), + *common_params, + "--save_models", + ] + run_mokapot_cli(params) + + parts = { + "part-a": "percolator-noSplit-extended-1000.tab", + "part-b": "percolator-noSplit-extended-1000b.tab", + "part-c": "percolator-noSplit-extended-1000c.tab", + } + + for root, input_file in parts.items(): + # Run mokapot for the smaller data files + if recompute or not Path.exists( + dest_dir / f"{root}.targets.precursors.csv" + ): + params = [ + Path("data", input_file), + *common_params, + ("--load_models", *dest_dir.glob("mokapot.model*.pkl")), + ("--file_root", root), + ] + run_mokapot_cli(params) + + # Convert csv output to parquet + for file in Path(dest_dir).glob(f"{root}.*.csv"): + outfile = pq_dest_dir / file.with_suffix(".parquet").name + if outfile.exists(): + continue + reader = CSVFileReader(file) + data = reader.read() + writer = ParquetFileWriter( + outfile, + reader.get_column_names(), + reader.get_column_types(), + ) + writer.write(data) + + yield dest_dir, pq_dest_dir + + # Cleanup files here + # Note: If you want to keep the files, create a file or directory name + # "dont_remove_me" in the dest_dir e.g. by the command + # mkdir -p scratch/testing/dont_remove_me + if not Path.exists(dest_dir / "dont_remove_me"): + shutil.rmtree(dest_dir) + + +@pytest.mark.parametrize( + "suffix", + [".csv", ".parquet"], +) +def test_rollup_10000(rollup_src_dirs, suffix, tmp_path): + """Test that basic cli works.""" + # rollup_dest_dir = tmp_path / suffix + rollup_src_dir, rollup_src_dir_parquet = rollup_src_dirs + + rollup_dest_dir = tmp_path / suffix + rollup_dest_dir.mkdir(parents=True, exist_ok=True) + + if suffix == ".parquet": + src_dir = rollup_src_dir_parquet + else: + src_dir = rollup_src_dir rollup_params = [ - "--level", - "precursor", - "--src_dir", - path, - "--dest_dir", - path, - "--verbosity", - 2, + ("--level", "precursor"), + ("--src_dir", src_dir), + ("--qvalue_algorithm", "from_counts"), + ("--verbosity", 2), ] - _ = run_brew_rollup(rollup_params, capture_output=True) + run_brew_rollup( + rollup_params + ["--dest_dir", rollup_dest_dir / "rollup0"], + capture_output=False, + ) + run_brew_rollup( + rollup_params + + ["--dest_dir", rollup_dest_dir / "rollup1", "--stream_confidence"], + capture_output=False, + ) + + assert rollup_dest_dir / "rollup0" / f"rollup.targets.peptides{suffix}" + + file0 = rollup_dest_dir / "rollup0" / f"rollup.targets.peptides{suffix}" + file1 = rollup_dest_dir / "rollup1" / f"rollup.targets.peptides{suffix}" + df0 = TabularDataReader.from_path(file0).read() + df1 = TabularDataReader.from_path(file1).read() + + qval_column = "q-value" + assert_series_equal(df0[qval_column], df1[qval_column], atol=0.02) + assert ( + estimate_abs_int(df0.score, df1[qval_column] - df0[qval_column]) + < 0.002 + ) + assert ( + estimate_abs_int( + df0.score, df1.posterior_error_prob - df0.posterior_error_prob + ) + < 0.03 + ) def test_compute_rollup_levels(): diff --git a/tests/system_tests/test_cli.py b/tests/system_tests/test_cli.py index 9be78681..eaa11b06 100644 --- a/tests/system_tests/test_cli.py +++ b/tests/system_tests/test_cli.py @@ -7,16 +7,17 @@ from pathlib import Path -from ..helpers.cli import run_mokapot_cli - -import pytest import pandas as pd +import pytest + +from ..helpers.cli import run_mokapot_cli +from ..helpers.utils import file_approx_len, file_missing, file_exist @pytest.fixture def scope_files(): """Get the scope-ms files""" - return sorted(list(Path("data").glob("scope*"))) + return sorted(list(Path("data").glob("scope*.pin"))) @pytest.fixture @@ -27,33 +28,15 @@ def phospho_files(): return pin, fasta -def count_lines(path: Path): - """Count the number of lines in a file. - - Parameters - ---------- - path : Path - The path to the file. - - Returns - ------- - int - The number of lines in the file. - """ - with open(path, "r") as file: - lines = file.readlines() - return len(lines) - - def test_basic_cli(tmp_path, scope_files): """Test that basic cli works.""" - params = [scope_files[0], "--dest_dir", tmp_path] + params = [scope_files[0], "--dest_dir", tmp_path, "--verbosity", 3] run_mokapot_cli(params) - assert Path(tmp_path, "targets.psms").exists() - assert Path(tmp_path, "targets.peptides").exists() + assert file_approx_len(tmp_path, "targets.psms.csv", 5487) + assert file_approx_len(tmp_path, "targets.peptides.csv", 5183) targets_psms_df = pd.read_csv( - Path(tmp_path, "targets.psms"), sep="\t", index_col=None + Path(tmp_path, "targets.psms.csv"), sep="\t", index_col=None ) assert targets_psms_df.columns.values.tolist() == [ "PSMId", @@ -69,50 +52,46 @@ def test_basic_cli(tmp_path, scope_files): assert targets_psms_df.iloc[0, 5] == "sp|P10809|CH60_HUMAN" - def test_cli_options(tmp_path, scope_files): """Test non-defaults""" params = [ scope_files[0], scope_files[1], - "--dest_dir", - tmp_path, - "--file_root", - "blah", - "--train_fdr", - "0.2", - "--test_fdr", - "0.1", - "--seed", - "100", - "--direction", - "RefactoredXCorr", - "--folds", - "2", - "-v", - "1", - "--max_iter", - "1", + ("--dest_dir", tmp_path), + ("--file_root", "blah"), + ("--train_fdr", "0.2"), + ("--test_fdr", "0.1"), + ("--seed", "100"), + ("--direction", "RefactoredXCorr"), + ("--folds", "2"), + ("-v", "1"), + ("--max_iter", "1"), "--keep_decoys", - "--subset_max_train", - "50000", - "--max_workers", - "3", + ("--subset_max_train", "50000"), + ("--max_workers", "3"), ] run_mokapot_cli(params) - file_bases = [f.name.split(".")[0] for f in scope_files[0:2]] + filebase = ["blah." + f.name.split(".")[0] for f in scope_files[0:2]] - assert Path(tmp_path, f"blah.{file_bases[0]}.targets.psms").exists() - assert Path(tmp_path, f"blah.{file_bases[0]}.targets.peptides").exists() - assert Path(tmp_path, f"blah.{file_bases[1]}.targets.psms").exists() - assert Path(tmp_path, f"blah.{file_bases[1]}.targets.peptides").exists() + assert file_approx_len(tmp_path, f"{filebase[0]}.targets.psms.csv", 5490) + assert file_approx_len( + tmp_path, f"{filebase[0]}.targets.peptides.csv", 5194 + ) + assert file_approx_len(tmp_path, f"{filebase[1]}.targets.psms.csv", 4659) + assert file_approx_len( + tmp_path, f"{filebase[1]}.targets.peptides.csv", 4406 + ) # Test keep_decoys: - assert Path(tmp_path, f"blah.{file_bases[0]}.decoys.psms").exists() - assert Path(tmp_path, f"blah.{file_bases[0]}.decoys.peptides").exists() - assert Path(tmp_path, f"blah.{file_bases[1]}.decoys.psms").exists() - assert Path(tmp_path, f"blah.{file_bases[1]}.decoys.peptides").exists() + assert file_approx_len(tmp_path, f"{filebase[0]}.decoys.psms.csv", 2090) + assert file_approx_len( + tmp_path, f"{filebase[0]}.decoys.peptides.csv", 2037 + ) + assert file_approx_len(tmp_path, f"{filebase[1]}.decoys.psms.csv", 1806) + assert file_approx_len( + tmp_path, f"{filebase[1]}.decoys.peptides.csv", 1755 + ) def test_cli_aggregate(tmp_path, scope_files): @@ -120,126 +99,196 @@ def test_cli_aggregate(tmp_path, scope_files): params = [ scope_files[0], scope_files[1], - "--dest_dir", - tmp_path, - "--file_root", - "blah", + ("--dest_dir", tmp_path), + ("--file_root", "blah"), "--aggregate", - "--max_iter", - "1", + ("--max_iter", "1"), ] run_mokapot_cli(params) - assert Path(tmp_path, "blah.targets.psms").exists() - assert Path(tmp_path, "blah.targets.peptides").exists() - assert not Path(tmp_path, "blah.targets.decoy.psms").exists() - assert not Path(tmp_path, "blah.targets.decoy.peptides").exists() - - # Line counts were determined by one hopefully correct test run - # GT counts: 10256, 9663 - assert count_lines(Path(tmp_path, "blah.targets.psms")) in range( - 10256 - 100, 10256 + 100 - ) - assert count_lines(Path(tmp_path, "blah.targets.peptides")) in range( - 9663 - 50, 9663 + 50 - ) + + # Line counts were determined by one (hopefully correct) test run + assert file_approx_len(tmp_path, "blah.targets.psms.csv", 10256) + assert file_approx_len(tmp_path, "blah.targets.peptides.csv", 9663) + assert file_missing(tmp_path, "blah.decoys.psms.csv") + assert file_missing(tmp_path, "blah.decoys.peptides.csv") # Test that decoys are also in the output when --keep_decoys is used params += ["--keep_decoys"] run_mokapot_cli(params) - assert Path(tmp_path, "blah.decoys.psms").exists() - assert Path(tmp_path, "blah.decoys.peptides").exists() - - assert count_lines(Path(tmp_path, "blah.decoys.psms")) in range( - 3787 - 50, 3787 + 50 - ) - assert count_lines(Path(tmp_path, "blah.decoys.peptides")) in range( - 3694 - 50, 3694 + 50 - ) + assert file_approx_len(tmp_path, "blah.targets.psms.csv", 10256) + assert file_approx_len(tmp_path, "blah.targets.peptides.csv", 9663) + assert file_approx_len(tmp_path, "blah.decoys.psms.csv", 3787) + assert file_approx_len(tmp_path, "blah.decoys.peptides.csv", 3694) def test_cli_fasta(tmp_path, phospho_files): """Test that proteins happen""" params = [ phospho_files[0], - "--dest_dir", - tmp_path, - "--proteins", - phospho_files[1], - "--max_iter", - "1", + ("--dest_dir", tmp_path), + ("--proteins", phospho_files[1]), + ("--max_iter", "1"), ] run_mokapot_cli(params) - assert Path(tmp_path, "targets.psms").exists() - assert Path(tmp_path, "targets.peptides").exists() - assert Path(tmp_path, "targets.proteins").exists() + + assert file_approx_len(tmp_path, "targets.psms.csv", 42331) + assert file_approx_len(tmp_path, "targets.peptides.csv", 33538) + assert file_approx_len(tmp_path, "targets.proteins.csv", 7827) def test_cli_saved_models(tmp_path, phospho_files): """Test that saved_models works""" params = [ phospho_files[0], - "--dest_dir", - tmp_path, - "--test_fdr", - "0.01", + ("--dest_dir", tmp_path), + ("--test_fdr", "0.01"), ] run_mokapot_cli(params + ["--save_models"]) params += ["--load_models", *list(Path(tmp_path).glob("*.pkl"))] run_mokapot_cli(params) - assert Path(tmp_path, "targets.psms").exists() - assert Path(tmp_path, "targets.peptides").exists() + assert file_approx_len(tmp_path, "targets.psms.csv", 42331) + assert file_approx_len(tmp_path, "targets.peptides.csv", 33538) def test_cli_skip_rollup(tmp_path, phospho_files): """Test that peptides file results is skipped when using skip_rollup""" params = [ phospho_files[0], - "--dest_dir", - tmp_path, - "--test_fdr", - "0.01", + ("--dest_dir", tmp_path), + ("--test_fdr", "0.01"), "--skip_rollup", ] run_mokapot_cli(params) - assert Path(tmp_path, "targets.psms").exists() - assert not Path(tmp_path, "targets.peptides").exists() + assert file_approx_len(tmp_path, "targets.psms.csv", 42331) + assert file_missing(tmp_path, "targets.peptides.csv") def test_cli_ensemble(tmp_path, phospho_files): """Test ensemble flag""" params = [ phospho_files[0], - "--dest_dir", - tmp_path, - "--test_fdr", - "0.01", + ("--dest_dir", tmp_path), + ("--test_fdr", "0.01"), "--ensemble", ] run_mokapot_cli(params) - assert Path(tmp_path, "targets.psms").exists() - assert Path(tmp_path, "targets.peptides").exists() - # fixme: should also test the *contents* of the files + assert file_approx_len(tmp_path, "targets.psms.csv", 42331) + assert file_approx_len(tmp_path, "targets.peptides.csv", 33538) + # todo: nice to have: we should also test the *contents* of the files def test_cli_bad_input(tmp_path): - """Test ensemble flag""" + """Test with problematic input files""" + + # The input file contains "integers" of the form `6d05`, which caused + # problems with certain readers + params = [ Path("data") / "percolator-noSplit-extended-201-bad.tab", - "--dest_dir", - tmp_path, - "--train_fdr", - "0.05", + ("--dest_dir", tmp_path), + ("--train_fdr", "0.05"), "--ensemble", ] run_mokapot_cli(params) - assert Path(tmp_path, "targets.psms").exists() - assert Path(tmp_path, "targets.peptides").exists() - # fixme: should also test the *contents* of the files + assert file_exist(tmp_path, "targets.psms.csv") + assert file_exist(tmp_path, "targets.peptides.csv") + + +def test_negative_features(tmp_path, psm_df_1000): + """Test that best feature selection works.""" + + def make_pin_file(filename, desc, seed=None): + import numpy as np + + df = psm_df_1000[1].copy() + if seed is not None: + np.random.seed(seed) + scores = df["score"] + targets = df["target"] + df.drop(columns=["score", "score2", "target"], inplace=True) + df["Label"] = targets * 1 + df["feat"] = scores * (1 if desc else -1) + df["scannr"] = np.random.randint(0, 1000, 1000) + file = tmp_path / filename + df.to_csv(file, sep="\t", index=False) + return file, df + + file1bad, df1b = make_pin_file("test1bad.pin", desc=True, seed=123) + file2bad, df2b = make_pin_file("test2bad.pin", desc=False, seed=123) + file1, df1 = make_pin_file("test1.pin", desc=True, seed=126) + file2, df2 = make_pin_file("test2.pin", desc=False, seed=126) + + def read_result(filename): + df = pd.read_csv(tmp_path / filename, sep="\t", index_col=False) + return df.sort_values(by="PSMId").reset_index(drop=True) + + def mean_scores(str): + def mean_score(file): + psms_df = read_result(file) + return psms_df.score.values.mean() + + target_mean = mean_score(f"{str}.targets.psms.csv") + decoy_mean = mean_score(f"{str}.decoys.psms.csv") + return (target_mean, decoy_mean, target_mean > decoy_mean) + + common_params = [ + ("--dest_dir", tmp_path), + ("--train_fdr", 0.05), + ("--test_fdr", 0.05), + ("--peps_algorithm", "hist_nnls"), + "--keep_decoys", + ] + + # Test with data where a "good" model can be trained. Once with the normal + # feat column, once with the feat column negated. + params = [file1, "--file_root", "test1"] + run_mokapot_cli(params + common_params) + + params = [file2, "--file_root", "test2"] + run_mokapot_cli(params + common_params) + + psms_df1 = read_result("test1.targets.psms.csv") + psms_df2 = read_result("test2.targets.psms.csv") + pd.testing.assert_frame_equal(psms_df1, psms_df2) + + # In the case below, the trained model performs worse than just using the + # feat column, so the score is just the same as the feature. + + params = [file1bad, "--file_root", "test1b"] + run_mokapot_cli(params + common_params) + + params = [file2bad, "--file_root", "test2b"] + run_mokapot_cli(params + common_params) + + psms_df1b = read_result("test1b.targets.psms.csv") + psms_df2b = read_result("test2b.targets.psms.csv") + pd.testing.assert_frame_equal(psms_df1b, psms_df2b) + + # Let's check now that the score columns are indeed equal to the + # normal/negated feature column + + feature_col1 = df1b[df1b.Label == 1].sort_values(by="specid").feat + score_col1 = psms_df1b.sort_values(by="PSMId").score + pd.testing.assert_series_equal( + score_col1, feature_col1, check_index=False, check_names=False + ) + + feature_col2 = df2b[df2b.Label == 1].sort_values(by="specid").feat + score_col2 = psms_df2b.sort_values(by="PSMId").score + pd.testing.assert_series_equal( + score_col2, -feature_col2, check_index=False, check_names=False + ) + + # Lastly, test that the targets have a higher mean score than the decoys + assert mean_scores("test1")[2] + assert mean_scores("test2")[2] + assert mean_scores("test1b")[2] + assert mean_scores("test2b")[2] # This one is the most likely to fail diff --git a/tests/system_tests/test_determinism.py b/tests/system_tests/test_determinism.py index 4bdbd549..5a573c44 100644 --- a/tests/system_tests/test_determinism.py +++ b/tests/system_tests/test_determinism.py @@ -2,13 +2,12 @@ These tests verify that the aggregatePsmsToPeptides executable works as expected. """ # noqa: E501 -from pathlib import Path - -from ..helpers.cli import run_mokapot_cli - import pandas as pd import pytest +from ..helpers.cli import run_mokapot_cli +from ..helpers.utils import file_exist + # Warnings are errors for these tests pytestmark = pytest.mark.filterwarnings("error") @@ -17,52 +16,43 @@ def test_determinism_same_file(tmp_path, psm_files_4000): """Test that two identical mokapot runs produce same results.""" params = [ - "--dest_dir", - tmp_path, - "--subset_max_train", - "2500", + ("--dest_dir", tmp_path), + ("--subset_max_train", "2500"), + ("--max_workers", "8"), + ("--max_iter", "2"), "--keep_decoys", - "--max_workers", - "8", "--ensemble", - "--max_iter", - "2", ] run_mokapot_cli(params + [psm_files_4000[0], "--file_root", "run1"]) run_mokapot_cli(params + [psm_files_4000[0], "--file_root", "run2"]) - assert Path(tmp_path, "run1.targets.peptides").exists() - assert Path(tmp_path, "run1.decoys.peptides").exists() - assert Path(tmp_path, "run1.targets.psms").exists() - assert Path(tmp_path, "run1.decoys.psms").exists() + assert file_exist(tmp_path, "run1.targets.peptides.csv") + assert file_exist(tmp_path, "run1.decoys.peptides.csv") + assert file_exist(tmp_path, "run1.targets.psms.csv") + assert file_exist(tmp_path, "run1.decoys.psms.csv") - assert Path(tmp_path, "run2.targets.peptides").exists() - assert Path(tmp_path, "run2.decoys.peptides").exists() - assert Path(tmp_path, "run2.targets.psms").exists() - assert Path(tmp_path, "run2.decoys.psms").exists() + assert file_exist(tmp_path, "run2.targets.peptides.csv") + assert file_exist(tmp_path, "run2.decoys.peptides.csv") + assert file_exist(tmp_path, "run2.targets.psms.csv") + assert file_exist(tmp_path, "run2.decoys.psms.csv") - df_run1_t_psms = pd.read_csv(tmp_path / "run1.targets.psms", sep="\t") - df_run2_t_psms = pd.read_csv(tmp_path / "run2.targets.psms", sep="\t") + def read_tsv(filename): + return pd.read_csv(tmp_path / filename, sep="\t") + + df_run1_t_psms = read_tsv("run1.targets.psms.csv") + df_run2_t_psms = read_tsv("run2.targets.psms.csv") pd.testing.assert_frame_equal(df_run1_t_psms, df_run2_t_psms) - df_run1_t_peptides = pd.read_csv( - tmp_path / "run1.targets.peptides", sep="\t" - ) - df_run2_t_peptides = pd.read_csv( - tmp_path / "run2.targets.peptides", sep="\t" - ) + df_run1_t_peptides = read_tsv("run1.targets.peptides.csv") + df_run2_t_peptides = read_tsv("run2.targets.peptides.csv") pd.testing.assert_frame_equal(df_run1_t_peptides, df_run2_t_peptides) - df_run1_d_psms = pd.read_csv(tmp_path / "run1.decoys.psms", sep="\t") - df_run2_d_psms = pd.read_csv(tmp_path / "run2.decoys.psms", sep="\t") + df_run1_d_psms = read_tsv("run1.decoys.psms.csv") + df_run2_d_psms = read_tsv("run2.decoys.psms.csv") pd.testing.assert_frame_equal(df_run1_d_psms, df_run2_d_psms) - df_run1_d_peptides = pd.read_csv( - tmp_path / "run1.decoys.peptides", sep="\t" - ) - df_run2_d_peptides = pd.read_csv( - tmp_path / "run2.decoys.peptides", sep="\t" - ) + df_run1_d_peptides = read_tsv("run1.decoys.peptides.csv") + df_run2_d_peptides = read_tsv("run2.decoys.peptides.csv") pd.testing.assert_frame_equal(df_run1_d_peptides, df_run2_d_peptides) @@ -73,59 +63,43 @@ def test_determinism_different_psmid(tmp_path, psm_files_4000): """ cmd = [ - "--dest_dir", - tmp_path, - "--subset_max_train", - "2500", + ("--dest_dir", tmp_path), + ("--subset_max_train", "2500"), + ("--max_workers", "8"), + ("--max_iter", "2"), "--keep_decoys", - "--max_workers", - "8", "--ensemble", - "--max_iter", - "2", ] run_mokapot_cli(cmd + [psm_files_4000[0], "--file_root", "run1"]) run_mokapot_cli(cmd + [psm_files_4000[1], "--file_root", "run2"]) - assert Path(tmp_path, "run1.targets.peptides").exists() - assert Path(tmp_path, "run1.decoys.peptides").exists() - assert Path(tmp_path, "run1.targets.psms").exists() - assert Path(tmp_path, "run1.decoys.psms").exists() - - assert Path(tmp_path, "run2.targets.peptides").exists() - assert Path(tmp_path, "run2.decoys.peptides").exists() - assert Path(tmp_path, "run2.targets.psms").exists() - assert Path(tmp_path, "run2.decoys.psms").exists() - - df_run1_t_psms = pd.read_csv( - tmp_path / "run1.targets.psms", sep="\t" - ).drop("PSMId", axis=1) - df_run2_t_psms = pd.read_csv( - tmp_path / "run2.targets.psms", sep="\t" - ).drop("PSMId", axis=1) + assert file_exist(tmp_path, "run1.targets.peptides.csv") + assert file_exist(tmp_path, "run1.decoys.peptides.csv") + assert file_exist(tmp_path, "run1.targets.psms.csv") + assert file_exist(tmp_path, "run1.decoys.psms.csv") + + assert file_exist(tmp_path, "run2.targets.peptides.csv") + assert file_exist(tmp_path, "run2.decoys.peptides.csv") + assert file_exist(tmp_path, "run2.targets.psms.csv") + assert file_exist(tmp_path, "run2.decoys.psms.csv") + + def read_tsv(filename): + df = pd.read_csv(tmp_path / filename, sep="\t") + return df.drop("PSMId", axis=1) + + df_run1_t_psms = read_tsv("run1.targets.psms.csv") + df_run2_t_psms = read_tsv("run2.targets.psms.csv") pd.testing.assert_frame_equal(df_run1_t_psms, df_run2_t_psms) - df_run1_t_peptides = pd.read_csv( - tmp_path / "run1.targets.peptides", sep="\t" - ).drop("PSMId", axis=1) - df_run2_t_peptides = pd.read_csv( - tmp_path / "run2.targets.peptides", sep="\t" - ).drop("PSMId", axis=1) + df_run1_t_peptides = read_tsv("run1.targets.peptides.csv") + df_run2_t_peptides = read_tsv("run2.targets.peptides.csv") pd.testing.assert_frame_equal(df_run1_t_peptides, df_run2_t_peptides) - df_run1_d_psms = pd.read_csv(tmp_path / "run1.decoys.psms", sep="\t").drop( - "PSMId", axis=1 - ) - df_run2_d_psms = pd.read_csv(tmp_path / "run2.decoys.psms", sep="\t").drop( - "PSMId", axis=1 - ) + df_run1_d_psms = read_tsv("run1.decoys.psms.csv") + df_run2_d_psms = read_tsv("run2.decoys.psms.csv") pd.testing.assert_frame_equal(df_run1_d_psms, df_run2_d_psms) - df_run1_d_peptides = pd.read_csv( - tmp_path / "run1.decoys.peptides", sep="\t" - ).drop("PSMId", axis=1) - df_run2_d_peptides = pd.read_csv( - tmp_path / "run2.decoys.peptides", sep="\t" - ).drop("PSMId", axis=1) + df_run1_d_peptides = read_tsv("run1.decoys.peptides.csv") + df_run2_d_peptides = read_tsv("run2.decoys.peptides.csv") pd.testing.assert_frame_equal(df_run1_d_peptides, df_run2_d_peptides) diff --git a/tests/system_tests/test_parquet.py b/tests/system_tests/test_parquet.py new file mode 100644 index 00000000..dca44079 --- /dev/null +++ b/tests/system_tests/test_parquet.py @@ -0,0 +1,35 @@ +""" +These tests verify that the CLI works as expected. + +At least for now, they do not check the correctness of the +output, just that the expect outputs are created. +""" + +from pathlib import Path + +import pandas as pd + +from ..helpers.cli import run_mokapot_cli +from ..helpers.utils import file_exist + + +def test_parquet_output(tmp_path): + """Test that parquet input/output works.""" + params = [Path("data") / "10k_psms_test.parquet", "--dest_dir", tmp_path] + run_mokapot_cli(params) + assert file_exist(tmp_path, "targets.psms.parquet") + assert file_exist(tmp_path, "targets.peptides.parquet") + + targets_psms_df = pd.read_parquet(Path(tmp_path, "targets.psms.parquet")) + assert targets_psms_df.columns.values.tolist() == [ + "PSMId", + "peptide", + "score", + "q-value", + "posterior_error_prob", + "proteinIds", + ] + assert len(targets_psms_df.index) >= 5000 + + assert targets_psms_df.iloc[0, 0] == 6991 + assert targets_psms_df.iloc[0, 5] == "_.dummy._" diff --git a/tests/system_tests/test_rollup.py b/tests/system_tests/test_rollup.py index c6ba9d05..841391cb 100644 --- a/tests/system_tests/test_rollup.py +++ b/tests/system_tests/test_rollup.py @@ -5,12 +5,26 @@ output, just that the expect outputs are created. """ +import contextlib from pathlib import Path + import pandas as pd +import pytest +import mokapot from ..helpers.cli import run_mokapot_cli +from ..helpers.utils import file_exist, file_approx_len, count_lines -import pytest + +@contextlib.contextmanager +def run_with_chunk_size(chunk_size): + old_chunk_size = mokapot.confidence.CONFIDENCE_CHUNK_SIZE + try: + # We need to fully qualified path name to modify the constants + mokapot.confidence.CONFIDENCE_CHUNK_SIZE = chunk_size + yield + finally: + mokapot.confidence.CONFIDENCE_CHUNK_SIZE = old_chunk_size @pytest.fixture @@ -25,39 +39,20 @@ def percolator_extended_file_big(): return Path("data", "percolator-noSplit-extended-10000.tab") -# @pytest.fixture -# def percolator_extended_file_huge(): -# """Get the full extended percolator tab file""" -# return Path("scratch", "percolator-noSplit-extended.tab") - - def test_rollup_10000( tmp_path, percolator_extended_file_small, percolator_extended_file_big ): - """Test that basic cli works.""" + """Test that rollup for intermediate levels works.""" path = tmp_path - import shutil - - shutil.rmtree(path, ignore_errors=True) - - retrain = False - common_params = [ - "--dest_dir", - path, - "--max_workers", - 8, - "--test_fdr", - 0.10, - "--train_fdr", - 0.9, - "--verbosity", - 2, - "--subset_max_train", - 4000, - "--max_iter", - 10, + ("--dest_dir", path), + ("--max_workers", 8), + ("--test_fdr", 0.10), + ("--train_fdr", 0.9), + ("--verbosity", 2), + ("--subset_max_train", 4000), + ("--max_iter", 10), "--ensemble", "--keep_decoys", ] @@ -67,76 +62,177 @@ def test_rollup_10000( fasta = Path("data", "human_sp_td.fasta") common_params += ["--proteins", fasta] - # common_params += ["--skip_rollup"] - - if retrain or not Path.exists(path / "mokapot.model_fold-1.pkl"): - # params = [percolator_extended_file_big, - params = [ - percolator_extended_file_small, - *common_params, - "--save_models", - ] - else: - params = [ - percolator_extended_file_small, - *common_params, - "--load_models", - *path.glob("mokapot.model_fold-*.pkl"), - ] - + params = [ + percolator_extended_file_small, + *common_params, + "--save_models", + ] run_mokapot_cli(params) - assert Path(path, "targets.psms").exists() - assert Path(path, "targets.peptides").exists() - assert Path(path, "targets.precursors").exists() - assert Path(path, "targets.modifiedpeptides").exists() - assert Path(path, "targets.peptidegroups").exists() + assert file_exist(path, "targets.psms.csv") + assert file_exist(path, "targets.peptides.csv") + assert file_exist(path, "targets.precursors.csv") + assert file_exist(path, "targets.modifiedpeptides.csv") + assert file_exist(path, "targets.peptidegroups.csv") def test_extra_cols(tmp_path): """Test that two identical mokapot runs produce same results.""" extended_file = Path("data", "percolator-noSplit-extended-10000.tab") - non_extended_file = Path( - "data", "percolator-noSplit-non-extended-10000.tab" - ) + non_extended_file = Path("data", "percolator-noSplit-non-extended-10000.tab") params = [ - "--dest_dir", - tmp_path, - "--subset_max_train", - "2500", + ("--dest_dir", tmp_path), + ("--subset_max_train", "2500"), + ("--max_workers", "8"), + ("--max_iter", "2"), "--keep_decoys", - "--max_workers", - "8", "--ensemble", - "--max_iter", - "2", ] run_mokapot_cli([extended_file] + params + ["--file_root", "run1"]) run_mokapot_cli([non_extended_file] + params + ["--file_root", "run2"]) - assert Path(tmp_path, "run1.targets.peptides").exists() - assert Path(tmp_path, "run1.decoys.peptides").exists() - assert Path(tmp_path, "run1.targets.psms").exists() - assert Path(tmp_path, "run1.decoys.psms").exists() + assert file_exist(tmp_path, "run1.targets.peptides.csv") + assert file_exist(tmp_path, "run1.decoys.peptides.csv") + assert file_exist(tmp_path, "run1.targets.psms.csv") + assert file_exist(tmp_path, "run1.decoys.psms.csv") - assert Path(tmp_path, "run2.targets.peptides").exists() - assert Path(tmp_path, "run2.decoys.peptides").exists() - assert Path(tmp_path, "run2.targets.psms").exists() - assert Path(tmp_path, "run2.decoys.psms").exists() + assert file_exist(tmp_path, "run2.targets.peptides.csv") + assert file_exist(tmp_path, "run2.decoys.peptides.csv") + assert file_exist(tmp_path, "run2.targets.psms.csv") + assert file_exist(tmp_path, "run2.decoys.psms.csv") - df_run1_t_psms = pd.read_csv(tmp_path / "run1.targets.psms", sep="\t") - df_run2_t_psms = pd.read_csv(tmp_path / "run2.targets.psms", sep="\t") + df_run1_t_psms = pd.read_csv(tmp_path / "run1.targets.psms.csv", sep="\t") + df_run2_t_psms = pd.read_csv(tmp_path / "run2.targets.psms.csv", sep="\t") pd.testing.assert_frame_equal( df_run1_t_psms[df_run2_t_psms.columns], df_run2_t_psms ) - df_run1_t_peptides = pd.read_csv( - tmp_path / "run1.targets.peptides", sep="\t" - ) - df_run2_t_peptides = pd.read_csv( - tmp_path / "run2.targets.peptides", sep="\t" - ) + df_run1_t_peptides = pd.read_csv(tmp_path / "run1.targets.peptides.csv", sep="\t") + df_run2_t_peptides = pd.read_csv(tmp_path / "run2.targets.peptides.csv", sep="\t") pd.testing.assert_frame_equal( df_run1_t_peptides[df_run2_t_peptides.columns], df_run2_t_peptides ) + + +def test_deduplication(tmp_path): + """Test that deduplication of psms works.""" + path = tmp_path + file = Path("data") / "scope2_FP97AA.pin" + + params = [ + file, + ("--dest_dir", path), + "--ensemble", + "--keep_decoys", + "--skip_rollup", + ("--peps_algorithm", "hist_nnls"), + ] + + dedup_params = params + ["--file_root", "dedup"] + run_mokapot_cli(dedup_params) + + no_dedup_params = params + [ + "--file_root", + "nodedup", + "--skip_deduplication", + ] + run_mokapot_cli(no_dedup_params) + + assert file_exist(path, "dedup.targets.psms.csv") + assert file_exist(path, "nodedup.targets.psms.csv") + assert file_approx_len(path, "dedup.targets.psms.csv", 5549) + assert file_approx_len(path, "nodedup.targets.psms.csv", 37814) + + # Check that we have really the right number of results: + # Without deduplication, all original targets have to be in the + # targets.psms output file. With deduplication, either the target or the + # decoy may survive the deduplication process, and thus we can only check + # for the sum of targets and decoys in both output files. + df = pd.read_csv(file, sep="\t", usecols=["Label", "ScanNr", "ExpMass"]) + nodedup_count = len(df[df.Label == 1]) + dedup_count = len(df.drop_duplicates(subset=["ScanNr", "ExpMass"])) + + lines1a = count_lines(path, "dedup.targets.psms.csv") - 1 + lines1b = count_lines(path, "dedup.decoys.psms.csv") - 1 + lines2 = count_lines(path, "nodedup.targets.psms.csv") - 1 + + assert lines1a + lines1b == dedup_count + assert lines2 == nodedup_count + + assert lines1a < lines2 + + +def test_streaming(tmp_path, percolator_extended_file_big): + """Test that streaming of confidence assignments works.""" + path = tmp_path + file = Path("data") / "scope2_FP97AA.pin" + + base_params = [ + file, + ("--dest_dir", path), + "--ensemble", + "--keep_decoys", + "--skip_rollup", + "--skip_deduplication", + ] + + # Check that correct peps algorithm is used (need to run in the same + # process, so that we can catch the exception) + params = base_params + ["--file_root", "stream", "--stream_confidence"] + with pytest.raises(ValueError, match="hist_nnls"): + run_mokapot_cli(params, run_in_subprocess=False) + + # todo: discuss: with deduplication there are sometimes differences in + # the results, which can happen when different psms with the same + # spectrum id and exp_mass happen to have the same score - often one + # target and the corresponding decoy. Then one or the other may + # survive deduplication, depending on which chunk it landed in, leading + # to different psms lists. + + # Run mokapot with streaming + with run_with_chunk_size(100000): + base_params += ["--peps_algorithm", "hist_nnls"] + params = base_params + [ + "--file_root", + "stream1", + "--stream_confidence", + ] + run_mokapot_cli(params) + assert file_exist(path, "stream1.targets.psms.csv") + + # Set chunk size so low, that chunking really kicks in + with run_with_chunk_size(123): + base_params += ["--peps_algorithm", "hist_nnls"] + params = base_params + [ + "--file_root", + "stream2", + "--stream_confidence", + ] + run_mokapot_cli(params) + assert file_exist(path, "stream2.targets.psms.csv") + + # Run mokapot without streaming + params = base_params + ["--file_root", "base"] + run_mokapot_cli(params) + assert file_exist(path, "base.targets.psms.csv") + + # compare results + def read_tsv(filename): + return pd.read_csv(path / filename, sep="\t", index_col=False) + + df_streamed = read_tsv("stream1.targets.psms.csv") + df_streamed2 = read_tsv("stream2.targets.psms.csv") + + pd.testing.assert_frame_equal(df_streamed, df_streamed2) + + df_base = read_tsv("base.targets.psms.csv") + + qvc = "q-value" + pvc = "posterior_error_prob" + pd.testing.assert_frame_equal( + df_streamed.drop(columns=[qvc, pvc]), df_base.drop(columns=[qvc, pvc]) + ) + + pd.testing.assert_series_equal(df_streamed[qvc], df_base[qvc], atol=0.01) + pd.testing.assert_series_equal(df_streamed[pvc], df_base[pvc], atol=0.06) diff --git a/tests/system_tests/test_sqlite.py b/tests/system_tests/test_sqlite.py new file mode 100644 index 00000000..da46eeb8 --- /dev/null +++ b/tests/system_tests/test_sqlite.py @@ -0,0 +1,77 @@ +""" +These tests verify that the CLI works as expected. + +At least for now, they do not check the correctness of the +output, just that the expect outputs are created. +""" + +import sqlite3 +import pytest +from pathlib import Path + +from mokapot.tabular_data import CSVFileReader +from ..helpers.cli import run_mokapot_cli + + +@pytest.fixture +def pin_file(): + """Get the scope-ms files""" + # return Path("data", "10k_psms_test.pin") + return Path("data", "percolator-noSplit-extended-1000.tab") + + +@pytest.fixture +def sqlite_db_file(pin_file, tmp_path): + db_file = Path(tmp_path, "sqlite.db") + db_file.unlink(missing_ok=True) + connection = sqlite3.connect(db_file) + create_table_queries = [ + "CREATE TABLE CANDIDATE (CANDIDATE_ID INTEGER NOT NULL, PSM_FDR REAL, SVM_SCORE REAL, POSTERIOR_ERROR_PROBABILITY REAL, PRIMARY KEY (CANDIDATE_ID));", # noqa: E501 + "CREATE TABLE PRECURSOR_VALIDATION (PCM_ID INTEGER NOT NULL, FDR REAL, PEP REAL, SVM_SCORE REAL , PRIMARY KEY (PCM_ID));", # noqa: E501 + "CREATE TABLE MODIFIED_PEPTIDE_VALIDATION (MODIFIED_PEPTIDE_ID INTEGER NOT NULL, FDR REAL, PEP REAL, SVM_SCORE REAL, PRIMARY KEY (MODIFIED_PEPTIDE_ID))", # noqa: E501 + "CREATE TABLE PEPTIDE_VALIDATION (PEPTIDE_ID INTEGER NOT NULL, FDR REAL, PEP REAL, SVM_SCORE REAL , PRIMARY KEY (PEPTIDE_ID))", # noqa: E501 + "CREATE TABLE PEPTIDE_GROUP_VALIDATION (PEPTIDE_GROUP_ID INTEGER NOT NULL, FDR REAL, PEP REAL, SVM_SCORE REAL , PRIMARY KEY (PEPTIDE_GROUP_ID))", # noqa: E501 + ] + for query in create_table_queries: + connection.execute(query) + + reader = CSVFileReader(pin_file) + df = reader.read() + candidate_ids = df["SpecId"].values + for i, c_id in enumerate(candidate_ids): + connection.execute( + "INSERT INTO CANDIDATE (CANDIDATE_ID) VALUES(:id);", + {"id": int(c_id)}, + ) + connection.commit() + return db_file + + +def test_sqlite_output(tmp_path, pin_file, sqlite_db_file): + """Test that basic cli works.""" + params = [ + pin_file, + ("--dest_dir", tmp_path), + ("--sqlite_db_path", sqlite_db_file), + ("--train_fdr", "0.1"), + ("--test_fdr", "0.2"), + ("--peps_algorithm", "hist_nnls"), + ] + run_mokapot_cli(params) + + # Basic check that there are sufficiently many rows in the result tables + tables_and_cols = [ + ("CANDIDATE", "PSM_FDR", 600), + ("PRECURSOR_VALIDATION", "FDR", 300), + ("MODIFIED_PEPTIDE_VALIDATION", "FDR", 300), + ("PEPTIDE_VALIDATION", "FDR", 300), + ("PEPTIDE_GROUP_VALIDATION", "FDR", 300), + ] + + connection = sqlite3.connect(sqlite_db_file) + for table_name, column_name, min_rows in tables_and_cols: + cursor = connection.execute( + f"SELECT * FROM {table_name} WHERE {column_name} IS NOT NULL;" + ) + rows = cursor.fetchall() + assert len(rows) > min_rows diff --git a/tests/system_tests/test_system.py b/tests/system_tests/test_system.py index f2b09c00..809fce1a 100644 --- a/tests/system_tests/test_system.py +++ b/tests/system_tests/test_system.py @@ -11,6 +11,7 @@ from pathlib import Path import pandas as pd + import mokapot from mokapot.tabular_data import CSVFileReader @@ -21,13 +22,14 @@ def test_compare_to_percolator(tmp_path): """Test that mokapot get almost the same answer as Percolator""" with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=pd.errors.ParserWarning) - dat = mokapot.read_pin(Path("data", "phospho_rep1.pin"), max_workers=3) + datasets = mokapot.read_pin( + Path("data", "phospho_rep1.pin"), max_workers=3 + ) proteins = mokapot.read_fasta(Path("data", "human_sp_td.fasta")) - psms, models, scores, desc = mokapot.brew(dat) + models, scores = mokapot.brew(datasets) mokapot.assign_confidence( - psms=psms, - scores=scores, - descs=desc, + datasets=datasets, + scores_list=scores, dest_dir=tmp_path, proteins=proteins, prefixes=[None], @@ -35,7 +37,7 @@ def test_compare_to_percolator(tmp_path): ) perc_path = Path("data", "percolator.{p}.txt") - moka_path = tmp_path / "targets.{p}" + moka_path = tmp_path / "targets.{p}.csv" def format_name(path, **kwargs): return path.with_name(path.name.format(**kwargs)) diff --git a/tests/unit_tests/test_brew.py b/tests/unit_tests/test_brew.py index 93554fac..a74c107e 100644 --- a/tests/unit_tests/test_brew.py +++ b/tests/unit_tests/test_brew.py @@ -2,11 +2,12 @@ import copy -import pytest import numpy as np +import pytest +from sklearn.ensemble import RandomForestClassifier + import mokapot from mokapot import PercolatorModel, Model -from sklearn.ensemble import RandomForestClassifier np.random.seed(42) @@ -19,16 +20,14 @@ def svm(): def test_brew_simple(psms_ondisk, svm): """Test with mostly default parameters of brew""" - psms, models, scores, desc = mokapot.brew(psms_ondisk, svm, test_fdr=0.05) + models, scores = mokapot.brew([psms_ondisk], svm, test_fdr=0.05) assert len(models) == 3 assert isinstance(models[0], PercolatorModel) def test_brew_simple_parquet(psms_ondisk_from_parquet, svm): """Test with mostly default parameters of brew""" - psms, models, scores, desc = mokapot.brew( - psms_ondisk_from_parquet, svm, test_fdr=0.05 - ) + models, scores = mokapot.brew([psms_ondisk_from_parquet], svm, test_fdr=0.05) assert len(models) == 3 assert isinstance(models[0], PercolatorModel) @@ -39,9 +38,7 @@ def test_brew_random_forest(psms_ondisk): RandomForestClassifier(), train_fdr=0.1, ) - psms, models, scores, desc = mokapot.brew( - psms_ondisk, model=rfm, test_fdr=0.1 - ) + models, scores = mokapot.brew([psms_ondisk], model=rfm, test_fdr=0.1) assert len(models) == 3 assert isinstance(models[0], Model) @@ -49,11 +46,9 @@ def test_brew_random_forest(psms_ondisk): def test_brew_joint(psms_ondisk, svm): """Test that the multiple input PSM collections yield multiple out""" collections = [psms_ondisk, copy.copy(psms_ondisk), copy.copy(psms_ondisk)] - psms, models, scores, desc = mokapot.brew(collections, svm, test_fdr=0.05) + models, scores = mokapot.brew(collections, svm, test_fdr=0.05) assert len(scores) == 3 - assert len(psms) == 3 assert len(models) == 3 - assert len(desc) == 3 def test_brew_joint_parquet(psms_ondisk_from_parquet, svm): @@ -63,20 +58,15 @@ def test_brew_joint_parquet(psms_ondisk_from_parquet, svm): copy.copy(psms_ondisk_from_parquet), copy.copy(psms_ondisk_from_parquet), ] - psms, models, scores, desc = mokapot.brew(collections, svm, test_fdr=0.05) + models, scores = mokapot.brew(collections, svm, test_fdr=0.05) assert len(scores) == 3 - assert len(psms) == 3 assert len(models) == 3 - assert len(desc) == 3 def test_brew_folds(psms_ondisk, svm): """Test that changing the number of folds works""" - psms, models, scores, desc = mokapot.brew( - psms_ondisk, svm, test_fdr=0.05, folds=4 - ) + models, scores = mokapot.brew([psms_ondisk], svm, test_fdr=0.05, folds=4) assert len(scores) == 1 - assert len(psms) == 1 assert len(models) == 4 @@ -86,25 +76,23 @@ def test_brew_seed(psms_ondisk, svm): seed = 0 psms_ondisk_b = copy.copy(psms_ondisk) psms_ondisk_c = copy.copy(psms_ondisk) - psms_a, models_a, scores_a, desc_a = mokapot.brew( - psms_ondisk, svm, test_fdr=0.05, folds=folds, rng=seed + models_a, scores_a = mokapot.brew( + [psms_ondisk], svm, test_fdr=0.05, folds=folds, rng=seed ) assert len(models_a) == folds - psms_b, models_b, scores_b, desc_b = mokapot.brew( - psms_ondisk_b, svm, test_fdr=0.05, folds=folds, rng=seed + models_b, scores_b = mokapot.brew( + [psms_ondisk_b], svm, test_fdr=0.05, folds=folds, rng=seed ) assert len(models_b) == folds - assert np.array_equal( - scores_a[0], scores_b[0] - ), "Results differed with same seed" + assert np.array_equal(scores_a[0], scores_b[0]), "Results differed with same seed" - psms_c, models_c, scores_c, desc_c = mokapot.brew( - psms_ondisk_c, svm, test_fdr=0.05, folds=folds, rng=seed + 2 + models_c, scores_c = mokapot.brew( + [psms_ondisk_c], svm, test_fdr=0.05, folds=folds, rng=seed + 2 ) assert len(models_c) == folds - assert ~( + assert not ( np.array_equal(scores_a[0], scores_c[0]) ), "Results were identical with different seed!" @@ -115,8 +103,8 @@ def test_brew_seed_parquet(psms_ondisk_from_parquet, svm): seed = 0 psms_ondisk_b = copy.copy(psms_ondisk_from_parquet) psms_ondisk_c = copy.copy(psms_ondisk_from_parquet) - psms_a, models_a, scores_a, desc_a = mokapot.brew( - psms_ondisk_from_parquet, + models_a, scores_a = mokapot.brew( + [psms_ondisk_from_parquet], svm, test_fdr=0.05, folds=folds, @@ -124,8 +112,8 @@ def test_brew_seed_parquet(psms_ondisk_from_parquet, svm): ) assert len(models_a) == folds - psms_b, models_b, scores_b, desc_b = mokapot.brew( - psms_ondisk_b, + models_b, scores_b = mokapot.brew( + [psms_ondisk_b], svm, test_fdr=0.05, folds=folds, @@ -133,19 +121,17 @@ def test_brew_seed_parquet(psms_ondisk_from_parquet, svm): ) assert len(models_b) == folds - assert np.array_equal( - scores_a[0], scores_b[0] - ), "Results differed with same seed" + assert np.array_equal(scores_a[0], scores_b[0]), "Results differed with same seed" - psms_c, models_c, scores_c, desc_c = mokapot.brew( - psms_ondisk_c, + models_c, scores_c = mokapot.brew( + [psms_ondisk_c], svm, test_fdr=0.05, folds=folds, rng=seed + 2, ) assert len(models_c) == folds - assert ~( + assert not ( np.array_equal(scores_a[0], scores_c[0]) ), "Results were identical with different seed!" @@ -153,7 +139,7 @@ def test_brew_seed_parquet(psms_ondisk_from_parquet, svm): def test_brew_test_fdr_error(psms_ondisk, svm): """Test that we get a sensible error message""" with pytest.raises(RuntimeError) as err: - mokapot.brew(psms_ondisk, svm, test_fdr=0.001, rng=2) + mokapot.brew([psms_ondisk], svm, test_fdr=0.001, rng=2) assert "Failed to calibrate" in str(err) @@ -161,7 +147,7 @@ def test_brew_test_fdr_error_parquet(psms_ondisk_from_parquet, svm): """Test that we get a sensible error message""" with pytest.raises(RuntimeError) as err: mokapot.brew( - psms_ondisk_from_parquet, + [psms_ondisk_from_parquet], svm, test_fdr=0.001, rng=2, @@ -171,9 +157,7 @@ def test_brew_test_fdr_error_parquet(psms_ondisk_from_parquet, svm): def test_brew_multiprocess(psms_ondisk, svm): """Test that multiprocessing doesn't yield an error""" - _, models, _, _ = mokapot.brew( - psms_ondisk, svm, test_fdr=0.05, max_workers=2 - ) + models, _ = mokapot.brew([psms_ondisk], svm, test_fdr=0.05, max_workers=2) # The models should not be the same: assert_not_close(models[0].estimator.coef_, models[1].estimator.coef_) assert_not_close(models[1].estimator.coef_, models[2].estimator.coef_) @@ -182,8 +166,8 @@ def test_brew_multiprocess(psms_ondisk, svm): def test_brew_multiprocess_parquet(psms_ondisk_from_parquet, svm): """Test that multiprocessing doesn't yield an error""" - _, models, _, _ = mokapot.brew( - psms_ondisk_from_parquet, + models, _ = mokapot.brew( + [psms_ondisk_from_parquet], svm, test_fdr=0.05, max_workers=2, @@ -198,19 +182,15 @@ def test_brew_trained_models(psms_ondisk, svm): """Test that using trained models reproduces same results""" # fix a seed to have the same random split for each run ( - psms_with_training, models_with_training, scores_with_training, - desc_with_training, - ) = mokapot.brew(copy.copy(psms_ondisk), svm, test_fdr=0.05, rng=2) + ) = mokapot.brew([copy.copy(psms_ondisk)], svm, test_fdr=0.05, rng=2) models = list(models_with_training) models.reverse() # Change the model order ( - psms_without_training, models_without_training, scores_without_training, - desc_without_training, - ) = mokapot.brew(psms_ondisk, models, test_fdr=0.05, rng=2) + ) = mokapot.brew([psms_ondisk], models, test_fdr=0.05, rng=2) assert models_with_training == models_without_training assert np.array_equal(scores_with_training[0], scores_without_training[0]) @@ -219,23 +199,29 @@ def test_brew_using_few_models_error(psms_ondisk, svm): """Test that if the number of trained models less than the number of folds we get the expected error message. """ + with pytest.raises(ValueError) as err: - mokapot.brew(psms_ondisk, [svm, svm], test_fdr=0.05) + mokapot.brew([psms_ondisk], [svm, svm], test_fdr=0.05) assert ( "The number of trained models (2) must match the number of folds (3)." in str(err) ) +def test_brew_using_untrained_models_error(psms_ondisk, svm): + """Test that all models all trained.""" + + with pytest.raises(RuntimeError) as err: + mokapot.brew([psms_ondisk], [svm, svm, svm], test_fdr=0.05) + assert "not previously trained" in str(err) + + def test_brew_using_non_trained_models_error(psms_ondisk, svm): """Test that using non trained models gives the expected error message""" svm.is_trained = False with pytest.raises(RuntimeError) as err: - mokapot.brew(psms_ondisk, [svm, svm, svm], test_fdr=0.05) - assert ( - "One or more of the provided models was not previously trained" - in str(err) - ) + mokapot.brew([psms_ondisk], [svm, svm, svm], test_fdr=0.05) + assert "One or more of the provided models was not previously trained" in str(err) def assert_not_close(x, y): diff --git a/tests/unit_tests/test_confidence.py b/tests/unit_tests/test_confidence.py index df3ef7c9..55669fa9 100644 --- a/tests/unit_tests/test_confidence.py +++ b/tests/unit_tests/test_confidence.py @@ -1,19 +1,16 @@ """Test that Confidence classes are working correctly""" import contextlib +import copy import numpy as np -import pyarrow.parquet as pq import pandas as pd -import copy - +import pyarrow.parquet as pq from pandas.testing import assert_frame_equal from pytest import approx -import pyarrow as pa import mokapot from mokapot import OnDiskPsmDataset, assign_confidence -from mokapot.confidence import get_unique_peptides_from_psms @contextlib.contextmanager @@ -37,17 +34,18 @@ def test_chunked_assign_confidence(psm_df_1000, tmp_path): # NB: with the old bug there would *always* be targets labelled as 1 # incorrectly (namely the last and before last) - pin_file, _, _ = psm_df_1000 + pin_file, df, _ = psm_df_1000 columns = list(pd.read_csv(pin_file, sep="\t").columns) df_spectra = pd.read_csv( pin_file, sep="\t", usecols=["scannr", "expmass", "target"] ) + score = df["score"].values psms_disk = OnDiskPsmDataset( - filename=pin_file, + pin_file, target_column="target", spectrum_columns=["scannr", "expmass"], peptide_column="peptide", - feature_columns=["score"], + feature_columns=[], filename_column="filename", scan_column="scannr", calcmass_column="calcmass", @@ -77,18 +75,18 @@ def test_chunked_assign_confidence(psm_df_1000, tmp_path): spectra_dataframe=df_spectra, ) with run_with_chunk_size(100): - np.random.seed(42) assign_confidence( [copy.copy(psms_disk)], + scores_list=[score], prefixes=[None], - descs=[True], dest_dir=tmp_path, max_workers=4, eval_fdr=0.02, ) - df_results_group = pd.read_csv(tmp_path / "targets.peptides", sep="\t") - assert len(df_results_group) == 500 + df_results_group = pd.read_csv(tmp_path / "targets.peptides.csv", sep="\t") + # assert len(df_results_group) == 500 + assert len(df_results_group) > 400 assert df_results_group.columns.tolist() == [ "PSMId", "peptide", @@ -98,39 +96,36 @@ def test_chunked_assign_confidence(psm_df_1000, tmp_path): "proteinIds", ] df = df_results_group.head(3) - assert df["PSMId"].tolist() == [98, 187, 176] - assert df["peptide"].tolist() == ["PELPK", "IYFCK", "CGQGK"] - assert df["score"].tolist() == approx([5.857438, 5.703985, 5.337845]) - assert df["q-value"].tolist() == approx( - [ - 0.01020408, - 0.01020408, - 0.01020408, - ] - ) - assert df["posterior_error_prob"].tolist() == approx( - [ - 1.635110e-05, - 2.496682e-05, - 6.854064e-05, - ] - ) + assert df["PSMId"].tolist() == [136, 96, 164] + assert df["peptide"].tolist() == ["EVSSK", "HDWCK", "SYQVK"] + assert df["score"].tolist() == approx([5.767435, 5.572517, 5.531904]) + assert df["q-value"].tolist() == approx([ + 0.0103092780336737, + 0.0103092780336737, + 0.0103092780336737, + ]) + assert df["posterior_error_prob"].tolist() == approx([ + 3.315389846699129e-05, + 5.558992546200682e-05, + 6.191049743361808e-05, + ]) def test_assign_confidence_parquet(psm_df_1000_parquet, tmp_path): """Test that assign_confidence() works with parquet files.""" - parquet_file, _, _ = psm_df_1000_parquet + parquet_file, df, _ = psm_df_1000_parquet columns = pq.ParquetFile(parquet_file).schema.names df_spectra = pq.read_table( parquet_file, columns=["scannr", "expmass", "target"] ).to_pandas() + scores = [df["score"].values] psms_disk = OnDiskPsmDataset( - filename=parquet_file, + parquet_file, target_column="target", spectrum_columns=["scannr", "expmass"], peptide_column="peptide", - feature_columns=["score"], + feature_columns=[], filename_column="filename", scan_column="scannr", calcmass_column="calcmass", @@ -148,12 +143,12 @@ def test_assign_confidence_parquet(psm_df_1000_parquet, tmp_path): "target", ], metadata_column_types=[ - pa.int64(), - pa.int64(), - pa.int64(), - pa.string(), - pa.string(), - pa.int64(), + np.dtype("int64"), + np.dtype("int64"), + np.dtype("float64"), + np.dtype("O"), + np.dtype("O"), + np.dtype("int64"), ], level_columns=["peptide"], specId_column="specid", @@ -163,52 +158,28 @@ def test_assign_confidence_parquet(psm_df_1000_parquet, tmp_path): np.random.seed(42) assign_confidence( [copy.copy(psms_disk)], + scores_list=scores, prefixes=[None], - descs=[True], dest_dir=tmp_path, max_workers=4, eval_fdr=0.02, ) - df_results_group1 = pd.read_csv( - tmp_path / "targets.peptides", sep="\t" + df_results_group1 = pd.read_parquet( + tmp_path / "targets.peptides.parquet" ) with run_with_chunk_size(10000): np.random.seed(42) assign_confidence( [copy.copy(psms_disk)], + scores_list=scores, prefixes=[None], - descs=None, # should default to [True] as in the first case dest_dir=tmp_path, max_workers=4, eval_fdr=0.02, ) - df_results_group2 = pd.read_csv( - tmp_path / "targets.peptides", sep="\t" + df_results_group2 = pd.read_parquet( + tmp_path / "targets.peptides.parquet" ) assert_frame_equal(df_results_group1, df_results_group2) - - -def test_get_unique_psms_and_peptides(peptide_csv_file, psms_iterator): - psms_iterator = psms_iterator - get_unique_peptides_from_psms( - iterable=psms_iterator, - peptide_col_name="Peptide", - write_columns=["PSMId", "Label", "Peptide", "score", "proteinIds"], - out_peptides=peptide_csv_file, - sep="\t", - ) - - expected_output = pd.DataFrame( - [ - [1, 1, "HLAQLLR", -5.75, "_.dummy._"], - [3, 0, "NVPTSLLK", -5.83, "_.dummy._"], - [4, 1, "QILVQLR", -5.92, "_.dummy._"], - [7, 1, "SRTSVIPGPK", -6.12, "_.dummy._"], - ], - columns=["PSMId", "Label", "Peptide", "score", "proteinIds"], - ) - - output = pd.read_csv(peptide_csv_file, sep="\t") - pd.testing.assert_frame_equal(expected_output, output) diff --git a/tests/unit_tests/test_dataset.py b/tests/unit_tests/test_dataset.py index 1caec566..f25648ca 100644 --- a/tests/unit_tests/test_dataset.py +++ b/tests/unit_tests/test_dataset.py @@ -4,7 +4,8 @@ import numpy as np import pandas as pd -from mokapot import LinearPsmDataset + +from mokapot import LinearPsmDataset, OnDiskPsmDataset def test_linear_init(psm_df_6): @@ -56,3 +57,19 @@ def test_update_labels(psm_df_6): real_labs = np.array([1, 1, 0, -1, -1, -1]) new_labs = dset._update_labels(scores, eval_fdr=0.5) assert np.array_equal(real_labs, new_labs) + + +def test_hash_row(): + x = np.array(["test.mzML", 870, 5902.639978936955, 890.522815122875], dtype=object) + assert OnDiskPsmDataset._hash_row(x) == 4196757312 + + x = np.array( + [ + "test.mzML", + np.int64(870), + np.float64(5902.639978936955), + np.float64(890.522815122875), + ], + dtype=object, + ) + assert OnDiskPsmDataset._hash_row(x) == 4196757312 diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index 22ddb2ec..a56d1caa 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -3,7 +3,6 @@ import pytest import mokapot import numpy as np -import pandas as pd from sklearn.svm import LinearSVC from sklearn.linear_model import LogisticRegression, LogisticRegressionCV from sklearn.preprocessing import MinMaxScaler, StandardScaler @@ -57,58 +56,58 @@ def test_perc_init(): assert model.override -def test_model_fit(psms): +def test_model_fit(psms_dataset): """Test that model fitting works""" model = mokapot.Model(LogisticRegression(), train_fdr=0.05, max_iter=1) - model.fit(psms) + model.fit(psms_dataset) assert model.is_trained model = mokapot.Model(LogisticRegressionCV(), train_fdr=0.05, max_iter=1) - model.fit(psms) + model.fit(psms_dataset) assert isinstance(model.estimator, LogisticRegression) assert model.is_trained - no_targets = pd.DataFrame({"targets": [False] * 100}) + psms_dataset._data[psms_dataset._target_column] = False with pytest.raises(ValueError): - model.fit(no_targets) + model.fit(psms_dataset) # no targets - no_decoys = pd.DataFrame({"targets": [True] * 100}) + psms_dataset._data[psms_dataset._target_column] = True with pytest.raises(ValueError): - model.fit(no_decoys) + model.fit(psms_dataset) # no decoys -def test_model_fit_large_subset(psms): +def test_model_fit_large_subset(psms_dataset): model = mokapot.Model( LogisticRegression(), train_fdr=0.05, max_iter=1, ) - model.fit(psms) + model.fit(psms_dataset) assert model.is_trained -def test_model_predict(psms): +def test_model_predict(psms_dataset): """Test predictions""" model = mokapot.Model(LogisticRegression(), train_fdr=0.05, max_iter=1) try: - model.predict(psms) + model.predict(psms_dataset) except NotFittedError: pass # The normal case - model.fit(psms) - scores = model.predict(psms) - assert len(scores) == len(psms) + model.fit(psms_dataset) + scores = model.predict(psms_dataset) + assert len(scores) == len(psms_dataset) # The case where a model is trained on a dataset with different features: - psms._data["blah"] = np.random.randn(len(psms)) - psms._feature_columns = ("score", "blah") + psms_dataset._data["blah"] = np.random.randn(len(psms_dataset)) + psms_dataset._feature_columns = ("score", "blah") with pytest.raises(ValueError): - model.predict(psms) + model.predict(psms_dataset) def test_model_persistance(tmp_path): diff --git a/tests/unit_tests/test_parser_parquet.py b/tests/unit_tests/test_parser_parquet.py index 98bbcd14..cfaf8645 100644 --- a/tests/unit_tests/test_parser_parquet.py +++ b/tests/unit_tests/test_parser_parquet.py @@ -1,9 +1,10 @@ """Test that parsing Percolator input files (parquet) works correctly""" -import pytest -import mokapot import pandas as pd import pyarrow.parquet as pq +import pytest + +import mokapot @pytest.fixture @@ -42,22 +43,22 @@ def std_parquet(tmp_path): def test_parquet_parsing(std_parquet): """Test pin parsing""" - dat = mokapot.read_pin( + datasets = mokapot.read_pin( std_parquet, max_workers=4, ) df = pq.read_table(std_parquet).to_pandas() - assert len(dat) == 1 - assert dat[0].filename == std_parquet + assert len(datasets) == 1 + pd.testing.assert_frame_equal( - df.loc[:, ("sCore",)], df.loc[:, dat[0].feature_columns] + df.loc[:, ("sCore",)], df.loc[:, datasets[0].feature_columns] ) pd.testing.assert_series_equal( - df.loc[:, "sPeCid"], df.loc[:, dat[0].specId_column] + df.loc[:, "sPeCid"], df.loc[:, datasets[0].specId_column] ) pd.testing.assert_series_equal( - df.loc[:, "pRoteins"], df.loc[:, dat[0].protein_column] + df.loc[:, "pRoteins"], df.loc[:, datasets[0].protein_column] ) pd.testing.assert_frame_equal( - df.loc[:, ("scanNR",)], df.loc[:, dat[0].spectrum_columns] + df.loc[:, ("scanNR",)], df.loc[:, datasets[0].spectrum_columns] ) diff --git a/tests/unit_tests/test_parser_pin.py b/tests/unit_tests/test_parser_pin.py index d01e251f..1281303d 100644 --- a/tests/unit_tests/test_parser_pin.py +++ b/tests/unit_tests/test_parser_pin.py @@ -2,11 +2,10 @@ from pathlib import Path -import pytest import pandas as pd +import pytest import mokapot -from mokapot.parsers import pin @pytest.fixture @@ -16,9 +15,8 @@ def std_pin(tmp_path): with open(str(out_file), "w+") as pin: dat = ( "sPeCid\tLaBel\tpepTide\tsCore\tscanNR\tpRoteins\n" - "DefaultDirection\t0\t-\t-\t1\t-\t-\n" - "a\t1\tABC\t5\t2\tprotein1\tprotein2\n" - "b\t-1\tCBA\t10\t3\tdecoy_protein1\tdecoy_protein2" + "a\t1\tABC\t5\t2\tprotein1:protein2\n" + "b\t-1\tCBA\t10\t3\tdecoy_protein1:decoy_protein2" ) pin.write(dat) @@ -27,49 +25,27 @@ def std_pin(tmp_path): def test_pin_parsing(std_pin): """Test pin parsing""" - dat = mokapot.read_pin( + datasets = mokapot.read_pin( std_pin, max_workers=4, ) df = pd.read_csv(std_pin, sep="\t") - assert len(dat) == 1 - assert dat[0].filename == std_pin + assert len(datasets) == 1 + pd.testing.assert_frame_equal( - df.loc[:, ("sCore",)], df.loc[:, dat[0].feature_columns] + df.loc[:, ("sCore",)], df.loc[:, datasets[0].feature_columns] ) pd.testing.assert_series_equal( - df.loc[:, "sPeCid"], df.loc[:, dat[0].specId_column] + df.loc[:, "sPeCid"], df.loc[:, datasets[0].specId_column] ) pd.testing.assert_series_equal( - df.loc[:, "pRoteins"], df.loc[:, dat[0].protein_column] + df.loc[:, "pRoteins"], df.loc[:, datasets[0].protein_column] ) pd.testing.assert_frame_equal( - df.loc[:, ("scanNR",)], df.loc[:, dat[0].spectrum_columns] + df.loc[:, ("scanNR",)], df.loc[:, datasets[0].spectrum_columns] ) def test_pin_wo_dir(): """Test a PIN file without a DefaultDirection line""" mokapot.read_pin(Path("data", "scope2_FP97AA.pin"), max_workers=4) - - -def test_read_file_in_chunks(): - """Test reading files in chungs""" - columns = ["SpecId", "Label", "ScanNr", "ExpMass"] - iterator = pin.read_file_in_chunks( - Path("data", "scope2_FP97AA.pin"), 100, use_cols=columns - ) - df = next(iterator) - assert len(df) == 100 - assert df.iloc[0, 0] == "target_0_9674_2_-1" - assert df.iloc[0, 2] == 9674 - - # Read in different column order than given in file - columns = ["ExpMass", "SpecId", "Label", "ScanNr"] - iterator = pin.read_file_in_chunks( - Path("data", "scope2_FP97AA.pin"), 100, use_cols=columns - ) - df = next(iterator) - assert len(df) == 100 - assert df.iloc[0, 1] == "target_0_9674_2_-1" - assert df.iloc[0, 3] == 9674 diff --git a/tests/unit_tests/test_peps.py b/tests/unit_tests/test_peps.py index c9197ed3..1d2217dc 100644 --- a/tests/unit_tests/test_peps.py +++ b/tests/unit_tests/test_peps.py @@ -1,10 +1,10 @@ +import logging import time -import pytest -import logging -from pytest import approx import numpy as np import numpy.testing as testing +import pytest +from pytest import approx from scipy import stats import mokapot.peps as peps @@ -17,20 +17,16 @@ def get_target_decoy_data(): R1 = stats.norm(loc=3, scale=2) NT0 = int(np.round(pi0 * N)) NT1 = N - NT0 - target_scores = np.concatenate( - ( - np.maximum(R1.rvs(NT1), R0.rvs(NT1)), - R0.rvs(NT0), - ) - ) + target_scores = np.concatenate(( + np.maximum(R1.rvs(NT1), R0.rvs(NT1)), + R0.rvs(NT0), + )) decoy_scores = R0.rvs(N) all_scores = np.concatenate((target_scores, decoy_scores)) - is_target = np.concatenate( - ( - np.full(len(target_scores), True), - np.full(len(decoy_scores), False), - ) - ) + is_target = np.concatenate(( + np.full(len(target_scores), True), + np.full(len(decoy_scores), False), + )) sortIdx = np.argsort(-all_scores) return [all_scores[sortIdx], is_target[sortIdx]] @@ -105,9 +101,15 @@ def test_monotonize(): def test_fit_nnls0(): # n = np.array([3, 2, 0, 0, 1, 1, 3]) # k = np.array([0, 0, 1, 1, 1, 1, 3]) - n = np.array([2, 1, 0, 1, 0, 2]) - k = np.array([0, 0, 1, 1, 1, 2]) - _ = peps.fit_nnls(n, k, ascending=True) + n = np.array([0, 2, 1, 0, 1, 0, 2, 0]) + k = np.array([1, 0, 0, 1, 1, 1, 2, 1]) + p_asc = np.array([0, 0, 0, 1 / 2, 1, 1, 1, 1]) + + p1 = peps.fit_nnls(n, k, ascending=True, erase_zeros=True) + np.testing.assert_allclose(p1, p_asc, atol=1e-15) + p2 = peps.fit_nnls(n, k, ascending=True, erase_zeros=False) + np.testing.assert_allclose(p2, p_asc, atol=1e-15) + _ = peps.fit_nnls(n, k, ascending=False) @@ -120,7 +122,7 @@ def test_fit_nnls_zeros(): p = peps.fit_nnls(n, k, ascending=True) assert p == approx([0, 1 / 2, 1]) p = peps.fit_nnls(n, k, ascending=True, erase_zeros=True) - assert p == approx([0, 1, 1]) + assert p == approx([0, 1 / 2, 1]) # Two zeros after each other n = np.array([1, 1, 0, 0, 1, 1]) @@ -128,7 +130,7 @@ def test_fit_nnls_zeros(): p = peps.fit_nnls(n, k, ascending=True) assert p == approx([0, 0, 1 / 3, 2 / 3, 1, 1]) p = peps.fit_nnls(n, k, ascending=True, erase_zeros=True) - assert p == approx([0, 0, 1, 1, 1, 1]) + assert p == approx([0, 0, 1 / 3, 2 / 3, 1, 1]) # Tricky: n==0 at the end n = np.array([1, 1, 0, 0, 1, 0]) @@ -136,7 +138,7 @@ def test_fit_nnls_zeros(): p = peps.fit_nnls(n, k, ascending=True) assert p == approx([0, 0, 1 / 3, 2 / 3, 1, 1]) p = peps.fit_nnls(n, k, ascending=True, erase_zeros=True) - assert p == approx([0, 0, 1, 1, 1, 1]) + assert p == approx([0, 0, 1 / 3, 2 / 3, 1, 1]) # Descending n = np.array([1, 1, 0, 0, 1, 1]) @@ -164,146 +166,28 @@ def test_fit_nnls_peps(): # This is from a real test case that failed due to a problem in # the scipy._nnls implementation n0 = np.array( - [ - 3, - 2, - 0, - 1, - 1, - 1, - 3, - 8, - 14, - 16, - 29, - 23, - 41, - 47, - 53, - 57, - 67, - 76, - 103, - 89, - 97, - 94, - 85, - 95, - 78, - 78, - 78, - 77, - 73, - 50, - 50, - 56, - 68, - 98, - 95, - 112, - 134, - 145, - 158, - 172, - 213, - 234, - 222, - 215, - 216, - 216, - 206, - 183, - 135, - 156, - 110, - 92, - 63, - 60, - 52, - 29, - 20, - 16, - 12, - 5, - 5, - 5, - 1, - 2, - 3, - 0, - 2, - ] + [3, 2, 0, 1, 1, 1, 3, 8, 14, 16, 29, 23, 41, 47, 53, 57, 67, 76, 103] + + [89, 97, 94, 85, 95, 78, 78, 78, 77, 73, 50, 50, 56, 68, 98, 95, 112] + + [134, 145, 158, 172, 213, 234, 222, 215, 216, 216, 206, 183, 135] + + [156, 110, 92, 63, 60, 52, 29, 20, 16, 12, 5, 5, 5, 1, 2, 3, 0, 2] ) k0 = np.array( - [ - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.7205812007860187, - 0.0, - 1.4411624015720375, - 0.7205812007860187, - 2.882324803144075, - 5.76464960628815, - 5.76464960628815, - 12.249880413362318, - 15.132205216506394, - 20.176273622008523, - 27.382085629868712, - 48.27894045266326, - 47.558359251877235, - 68.45521407467177, - 97.99904330689854, - 108.0871801179028, - 135.46926574777152, - 140.51333415327366, - 184.4687874012208, - 171.49832578707245, - 205.36564222401535, - 244.27702706646033, - 214.01261663344755, - 228.42424064916793, - 232.02714665309804, - 205.36564222401535, - 172.9394881886445, - 191.67459940908097, - 162.1307701768542, - 153.48379576742198, - 110.96950492104689, - 103.04311171240067, - 86.46974409432225, - 60.528820866025576, - 43.234872047161126, - 23.779179625938617, - 24.499760826724636, - 17.29394881886445, - 11.5292992125763, - 5.76464960628815, - 5.044068405502131, - 3.6029060039300935, - 0.0, - 2.882324803144075, - 0.0, - 0.0, - 0.0, - ] + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.7205812007860187, 0.0] + + [1.4411624015720375, 0.7205812007860187, 2.882324803144075] + + [5.76464960628815, 5.76464960628815, 12.249880413362318] + + [15.132205216506394, 20.176273622008523, 27.382085629868712] + + [48.27894045266326, 47.558359251877235, 68.45521407467177] + + [97.99904330689854, 108.0871801179028, 135.46926574777152] + + [140.51333415327366, 184.4687874012208, 171.49832578707245] + + [205.36564222401535, 244.27702706646033, 214.01261663344755] + + [228.42424064916793, 232.02714665309804, 205.36564222401535] + + [172.9394881886445, 191.67459940908097, 162.1307701768542] + + [153.48379576742198, 110.96950492104689, 103.04311171240067] + + [86.46974409432225, 60.528820866025576, 43.234872047161126] + + [23.779179625938617, 24.499760826724636, 17.29394881886445] + + [11.5292992125763, 5.76464960628815, 5.044068405502131] + + [3.6029060039300935, 0.0, 2.882324803144075, 0.0, 0.0, 0.0] ) p = peps.fit_nnls(n0, k0, ascending=True, erase_zeros=True) assert np.all(np.diff(p) >= 0) @@ -313,20 +197,22 @@ def test_fit_nnls_peps(): assert np.linalg.norm(k0 - n0 * p, np.inf) < 40 -def test_peps_qvality(): +@pytest.mark.parametrize("is_tdc", [True, False]) +def test_peps_qvality(is_tdc): scores, targets = get_target_decoy_data() - peps_values = peps.peps_from_scores_qvality(scores, targets) + peps_values = peps.peps_from_scores_qvality(scores, targets, is_tdc) assert np.all(peps_values >= 0) assert np.all(peps_values <= 1) assert np.all(np.diff(peps_values) * np.diff(scores) <= 0) -def test_peps_kde_nnls(): +@pytest.mark.parametrize("is_tdc", [True, False]) +def test_peps_kde_nnls(is_tdc): np.random.seed( 1253 ) # this produced an error with failing iterations in nnls scores, targets = get_target_decoy_data() - peps_values = peps.peps_from_scores_kde_nnls(scores, targets) + peps_values = peps.peps_from_scores_kde_nnls(scores, targets, is_tdc) assert np.all(peps_values >= 0) assert np.all(peps_values <= 1) assert np.all(np.diff(peps_values) * np.diff(scores) <= 0) @@ -335,7 +221,7 @@ def test_peps_kde_nnls(): 1245 ) # this produced an assertion error due to peps over 1.0 scores, targets = get_target_decoy_data() - peps_values = peps.peps_from_scores_kde_nnls(scores, targets) + peps_values = peps.peps_from_scores_kde_nnls(scores, targets, is_tdc) assert np.all(peps_values >= 0) assert np.all(peps_values <= 1) assert np.all(np.diff(peps_values) * np.diff(scores) <= 0) @@ -343,57 +229,76 @@ def test_peps_kde_nnls(): @pytest.mark.parametrize( "seed", - [ - # Those were collected seeds from random experiments where nnls failed - 1253, - 41908, - 39831, - 21706, - 38306, - 23020, - 46079, - 96127, - 23472, - 21706, - 38306, - 23020, - 46079, - 96127, - 23472, - 21706, - 38306, - 23020, - 46079, - 96127, - 23472, - 21706, - 38306, - 23020, - 46079, - 96127, - 23472, - 21706, - 38306, - 23020, - 46079, - 96127, - 23472, - 21706, - 38306, - 23020, - 46079, - 96127, - 23472, - 21706, - ], + # Those were collected seeds from random experiments where nnls failed + [1253, 41908, 39831, 21706, 38306, 23020, 46079, 96127, 23472, 21706] + + [38306, 23020, 46079, 96127, 23472, 21706, 38306, 23020, 46079, 96127] + + [23472, 21706, 38306, 23020, 46079, 96127, 23472, 21706, 38306, 23020] + + [46079, 96127, 23472, 21706, 38306, 23020, 46079, 96127, 23472, 21706], ) def test_peps_hist_nnls(seed): np.random.seed(seed) scores, targets = get_target_decoy_data() - try: - peps_values = peps.peps_from_scores_hist_nnls(scores, targets) - assert np.all(peps_values >= 0) - assert np.all(peps_values <= 1) - assert np.all(np.diff(peps_values) * np.diff(scores) <= 0) - except Exception as e: - pytest.fail(f"nnls failed on seed {seed}: {str(e)}") + + peps_values = peps.peps_from_scores_hist_nnls(scores, targets, False) + assert np.all(peps_values >= 0) + assert np.all(peps_values <= 1) + assert np.all(np.diff(peps_values) * np.diff(scores) <= 0) + + +def test_hist_data_from_iterator(): + scores = np.random.uniform(-3, 10, 1127) + targets = (np.random.uniform(0, 1, len(scores))) > 0.7 + + def score_iterator(scores, targets, chunksize=5): + for i in range(0, len(scores), chunksize): + yield scores[i : i + chunksize], targets[i : i + chunksize] + + bins = np.histogram_bin_edges(scores, bins=31) + e0, t0, d0 = peps.hist_data_from_scores( + scores, targets, bins=bins + ).as_counts() + e1, t1, d1 = peps.hist_data_from_iterator( + score_iterator(scores, targets), bin_edges=bins + ).as_counts() + assert e0 == approx(e1) + assert t0 == approx(t1) + assert d0 == approx(d1) + + bins = np.histogram_bin_edges(scores, bins=17) + e0, t0, d0 = peps.hist_data_from_scores( + scores, targets, bins=bins + ).as_densities() + e1, t1, d1 = peps.hist_data_from_iterator( + score_iterator(scores, targets, chunksize=7), bin_edges=bins + ).as_densities() + assert e0 == approx(e1) + assert t0 == approx(t1) + assert d0 == approx(d1) + + +def test_peps_from_qvality_sorting(): + # Check that qvality works with differently sorted scores + rng = np.random.Generator(np.random.PCG64(42)) + + N = 1126 + targets = (rng.uniform(0, 1, N)) > 0.7 + scores = targets * rng.normal(1, 1, N) + (1 - targets) * rng.normal( + -1, 1, N + ) + + peps0 = peps.peps_from_scores_qvality( + scores, targets, True, use_binary=False + ) + + index = np.argsort(scores)[::-1] + scores_sorted, targets_sorted = scores[index], targets[index] + + peps1 = peps.peps_from_scores_qvality( + scores_sorted, targets_sorted, True, use_binary=False + ) + + # import matplotlib.pyplot as plt + # plt.plot(scores, peps0, 'x'); plt.show() + # plt.plot(scores_sorted, peps1, 'x'); plt.show() + + assert np.all(peps0[index] == peps1) diff --git a/tests/unit_tests/test_qvalues.py b/tests/unit_tests/test_qvalues.py index e85ca395..de11c19f 100644 --- a/tests/unit_tests/test_qvalues.py +++ b/tests/unit_tests/test_qvalues.py @@ -4,9 +4,15 @@ import pytest import numpy as np +from mokapot.peps import TDHistData, hist_data_from_scores from scipy import stats -from mokapot.qvalues import tdc, qvalues_from_peps, qvalues_from_counts +from mokapot.qvalues import ( + tdc, + qvalues_from_peps, + qvalues_from_counts, + qvalues_func_from_hist, +) @pytest.fixture @@ -14,26 +20,24 @@ def desc_scores(): """Create a series of descending scores and their q-values""" scores = np.array([10, 10, 9, 8, 7, 7, 6, 5, 4, 3, 2, 2, 1, 1, 1, 1]) target = np.array([1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0]) - qvals = np.array( - [ - 1 / 4, - 1 / 4, - 1 / 4, - 1 / 4, - 2 / 6, - 2 / 6, - 2 / 6, - 3 / 7, - 3 / 7, - 4 / 7, - 5 / 8, - 5 / 8, - 1, - 1, - 1, - 1, - ] - ) + qvals = np.array([ + 1 / 4, + 1 / 4, + 1 / 4, + 1 / 4, + 2 / 6, + 2 / 6, + 2 / 6, + 3 / 7, + 3 / 7, + 4 / 7, + 5 / 8, + 5 / 8, + 1, + 1, + 1, + 1, + ]) return scores, target, qvals @@ -42,26 +46,24 @@ def asc_scores(): """Create a series of ascending scores and their q-values""" scores = np.array([1, 1, 2, 3, 4, 4, 5, 6, 7, 8, 9, 9, 10, 10, 10, 10]) target = np.array([1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0]) - qvals = np.array( - [ - 1 / 4, - 1 / 4, - 1 / 4, - 1 / 4, - 2 / 6, - 2 / 6, - 2 / 6, - 3 / 7, - 3 / 7, - 4 / 7, - 5 / 8, - 5 / 8, - 1, - 1, - 1, - 1, - ] - ) + qvals = np.array([ + 1 / 4, + 1 / 4, + 1 / 4, + 1 / 4, + 2 / 6, + 2 / 6, + 2 / 6, + 3 / 7, + 3 / 7, + 4 / 7, + 5 / 8, + 5 / 8, + 1, + 1, + 1, + 1, + ]) return scores, target, qvals @@ -123,39 +125,71 @@ def rand_scores(): R1 = stats.norm(loc=3, scale=2) NT0 = int(np.round(pi0 * N)) NT1 = N - NT0 - target_scores = np.concatenate( - ( - np.maximum(R1.rvs(NT1), R0.rvs(NT1)), - R0.rvs(NT0), - ) - ) + target_scores = np.concatenate(( + np.maximum(R1.rvs(NT1), R0.rvs(NT1)), + R0.rvs(NT0), + )) decoy_scores = R0.rvs(N) all_scores = np.concatenate((target_scores, decoy_scores)) - is_target = np.concatenate( - ( - np.full(len(target_scores), True), - np.full(len(decoy_scores), False), - ) - ) + is_target = np.concatenate(( + np.full(len(target_scores), True), + np.full(len(decoy_scores), False), + )) sortIdx = np.argsort(-all_scores) return [all_scores[sortIdx], is_target[sortIdx]] -def test_qvalues_from_peps(rand_scores): +@pytest.mark.parametrize("is_tdc", [True, False]) +def test_qvalues_from_peps(rand_scores, is_tdc): # Note: we should also test against some known truth # (of course, up to some error margin and fixing the random seed), # and also against shuffeling of the target/decoy sequences. scores, targets = rand_scores - qvalues = qvalues_from_peps(scores, targets) + qvalues = qvalues_from_peps(scores, targets, is_tdc) assert np.all(qvalues >= 0) assert np.all(qvalues <= 1) assert np.all(np.diff(qvalues) * np.diff(scores) <= 0) -def test_qvalues_from_counts(rand_scores): +@pytest.mark.parametrize("is_tdc", [True, False]) +def test_qvalues_from_counts(rand_scores, is_tdc): scores, targets = rand_scores - qvalues = qvalues_from_counts(scores, targets) + qvalues = qvalues_from_counts(scores, targets, is_tdc) assert np.all(qvalues >= 0) assert np.all(qvalues <= 1) assert np.all(np.diff(qvalues) * np.diff(scores) <= 0) + + +def test_qvalues_from_counts_descending(desc_scores): + """Test that q-values are correct for descending scores""" + scores, target, true_qvals = desc_scores + targets = target == 1 + qvals = qvalues_from_counts(scores, targets, is_tdc=True) + np.testing.assert_allclose(qvals, true_qvals, atol=1e-7) + + +def test_qvalues_from_hist_desc(desc_scores): + scores, target, true_qvals = desc_scores + targets = target == 1 + # Use small bins covering every interval + the scores as bin edges + bin_edges = np.linspace(0, 11, num=371) + bin_edges = np.unique(np.sort(np.concatenate((bin_edges, scores)))) + + hist_data = TDHistData.from_scores_targets(bin_edges, scores, targets) + qvalue_func = qvalues_func_from_hist(hist_data, is_tdc=True) + qvals = qvalue_func(scores) + + np.testing.assert_allclose(qvals, true_qvals, atol=1e-7) + + +def test_compare_rand_qvalues_from_hist_vs_count(rand_scores): + # Compare the q-values computed via counts with those computed via + # histogram on a dataset of a few thousand random scores + scores, targets = rand_scores + hist_data = hist_data_from_scores(scores, targets) + qvalue_func = qvalues_func_from_hist(hist_data, is_tdc=True) + qvals_hist = qvalue_func(scores) + qvals_counts = qvalues_from_counts(scores, targets, is_tdc=True) + + np.testing.assert_allclose(qvals_hist, qvals_counts, atol=0.02) diff --git a/tests/unit_tests/test_statistics.py b/tests/unit_tests/test_statistics.py new file mode 100644 index 00000000..4b7cf7ee --- /dev/null +++ b/tests/unit_tests/test_statistics.py @@ -0,0 +1,136 @@ +import math +import numpy as np +import pytest +import scipy as sp +from pytest import approx + +from mokapot.statistics import OnlineStatistics, HistData + + +def test_init(): + stats = OnlineStatistics() + assert stats.min == math.inf + assert stats.max == -math.inf + assert stats.n == 0 + assert stats.sum == 0.0 + assert stats.mean == 0.0 + assert stats.var == 0.0 + assert stats.sd == 0.0 + + +def test_update(): + stats = OnlineStatistics() + vals = np.array([1, 2, 3, 4, 5]) + stats.update(vals) + + assert stats.min == 1.0 + assert stats.max == 5.0 + assert stats.n == 5 + assert stats.sum == 15.0 + assert stats.mean == 3.0 + assert math.isclose(stats.var, 2.5) + assert math.isclose(stats.sd, 1.5811388300841898) + + +def test_update_multiple(): + stats = OnlineStatistics() + vals1 = np.array([1, 2, 3, 4, 5]) + stats.update(vals1) + vals2 = np.array([6, 7, 8, 9, 10]) + stats.update(vals2) + + desc = sp.stats.describe(np.concatenate((vals1, vals2))) + assert (stats.min, stats.max) == desc.minmax + assert stats.n == desc.nobs + assert stats.mean == desc.mean + assert math.isclose(stats.var, desc.variance) + + +def test_update_multiple2(): + stats = OnlineStatistics() + vals = np.array([]) + for _ in range(1000): + n = np.random.randint(10, 50) + new_vals = 100 * np.random.random_sample(n) + stats.update(new_vals) + vals = np.concatenate((vals, new_vals)) + + assert stats.min == vals.min() + assert stats.max == vals.max() + assert stats.n == len(vals) + assert stats.mean == approx(np.mean(vals)) + assert stats.var == approx(np.var(vals, ddof=1), rel=1e-14) + assert stats.sd == approx(np.std(vals, ddof=1), rel=1e-14) + + +def test_update_single(): + stats = OnlineStatistics() + vals = np.arange(10) + for val in vals: + stats.update_single(val) + + assert stats.min == vals.min() + assert stats.max == vals.max() + assert stats.n == len(vals) + assert stats.mean == np.mean(vals) + assert stats.var == approx(np.var(vals, ddof=1)) + assert stats.sd == approx(np.std(vals, ddof=1)) + + +def test_max_likelihood_variance(): + stats = OnlineStatistics(unbiased=False) + vals = np.arange(10) + stats.update(vals) + assert stats.var == approx(np.var(vals, ddof=0)) + assert stats.sd == approx(np.std(vals, ddof=0)) + + +def test_hist_data(): + N = 1000 + x = np.concatenate([np.random.normal(size=N), np.random.normal(2, size=N)]) + counts, bin_edges = np.histogram(x, bins="scott") + hist = HistData(bin_edges, counts) + + density, _ = np.histogram(x, bins=bin_edges, density=True) + assert density == approx(hist.density) + + bin_centers = hist.bin_centers + assert len(bin_centers) == len(counts) + assert bin_centers[0] == approx((bin_edges[0] + bin_edges[1]) / 2) + assert bin_centers[-1] == approx((bin_edges[-2] + bin_edges[-1]) / 2) + + +def test_binning(): + N = 10000 + x = np.random.normal(2, 3, size=N) + + stats = OnlineStatistics() + stats.update(x) + + assert HistData.get_bin_edges(stats, "scott") == approx( + np.histogram_bin_edges(x, bins="scott") + ) + assert HistData.get_bin_edges(stats, "sturges") == approx( + np.histogram_bin_edges(x, bins="sturges") + ) + assert HistData.get_bin_edges(stats, "auto") == approx( + np.histogram_bin_edges(x, bins="scott") + ) + + # If we extend the bins, the new bins should have the same center, it + # should be one bin more, and they should all have the same size + edges0 = HistData.get_bin_edges(stats, "scott") + edges1 = HistData.get_bin_edges(stats, "scott", extend=True) + assert len(edges1) == len(edges0) + 1 + assert np.diff(edges1).mean() == approx(np.diff(edges0).mean()) + assert edges1.mean() == approx(edges0.mean()) + + assert HistData.get_bin_edges(stats, clip=(2, 3)) == approx( + np.histogram_bin_edges(x, bins=3) + ) + assert HistData.get_bin_edges(stats, clip=(200, 202)) == approx( + np.histogram_bin_edges(x, bins=200) + ) + + with pytest.raises(ValueError): + HistData.get_bin_edges(stats, name="xyz") diff --git a/tests/unit_tests/test_streaming.py b/tests/unit_tests/test_streaming.py index 21bebe04..9bddb719 100644 --- a/tests/unit_tests/test_streaming.py +++ b/tests/unit_tests/test_streaming.py @@ -1,10 +1,6 @@ -from __future__ import annotations - import pandas as pd from mokapot.tabular_data import ( DataFrameReader, -) -from mokapot.streaming import ( merge_readers, MergedTabularDataReader, join_readers, diff --git a/tests/unit_tests/test_tabular_data.py b/tests/unit_tests/test_tabular_data.py index c8b806ab..8732852e 100644 --- a/tests/unit_tests/test_tabular_data.py +++ b/tests/unit_tests/test_tabular_data.py @@ -1,18 +1,18 @@ -import numpy as np -import pytest from pathlib import Path + +import numpy as np import pandas as pd +import pytest from numpy import dtype -import pyarrow as pa from mokapot.tabular_data import ( TabularDataReader, - CSVFileReader, - ParquetFileReader, DataFrameReader, ColumnMappedReader, - CSVFileWriter, auto_finalize, + CSVFileReader, + ParquetFileReader, + CSVFileWriter, ) @@ -76,6 +76,7 @@ def test_csv_file_reader(): df = reader.read(["ScanNr", "SpecId"]) assert df.columns.tolist() == ["ScanNr", "SpecId"] assert len(df) == 55398 + assert all(df.index == range(len(df))) chunk_iterator = reader.get_chunked_data_iterator( chunk_size=20000, columns=["ScanNr", "SpecId"] @@ -83,6 +84,8 @@ def test_csv_file_reader(): chunks = [chunk for chunk in chunk_iterator] sizes = [len(chunk) for chunk in chunks] assert sizes == [20000, 20000, 15398] + df_from_chunks = pd.concat(chunks) + assert all(df_from_chunks.index == range(len(df_from_chunks))) def test_parquet_file_reader(): @@ -93,24 +96,24 @@ def test_parquet_file_reader(): column_to_types = dict(zip(names, types)) expected_column_to_types = { - "SpecId": pa.int64(), - "Label": pa.int64(), - "ScanNr": pa.int64(), - "ExpMass": pa.float64(), - "Mass": pa.float64(), - "MS8_feature_5": pa.int64(), - "missedCleavages": pa.int64(), - "MS8_feature_7": pa.float64(), - "MS8_feature_13": pa.float64(), - "MS8_feature_20": pa.float64(), - "MS8_feature_21": pa.float64(), - "MS8_feature_22": pa.float64(), - "MS8_feature_24": pa.int64(), - "MS8_feature_29": pa.float64(), - "MS8_feature_30": pa.float64(), - "MS8_feature_32": pa.float64(), - "Peptide": pa.string(), - "Proteins": pa.string(), + "SpecId": dtype("int64"), + "Label": dtype("int64"), + "ScanNr": dtype("int64"), + "ExpMass": dtype("float64"), + "Mass": dtype("float64"), + "MS8_feature_5": dtype("int64"), + "missedCleavages": dtype("int64"), + "MS8_feature_7": dtype("float64"), + "MS8_feature_13": dtype("float64"), + "MS8_feature_20": dtype("float64"), + "MS8_feature_21": dtype("float64"), + "MS8_feature_22": dtype("float64"), + "MS8_feature_24": dtype("int64"), + "MS8_feature_29": dtype("float64"), + "MS8_feature_30": dtype("float64"), + "MS8_feature_32": dtype("float64"), + "Peptide": dtype("O"), + "Proteins": dtype("O"), } for name, type in expected_column_to_types.items(): @@ -119,13 +122,16 @@ def test_parquet_file_reader(): df = reader.read(["ScanNr", "SpecId"]) assert df.columns.tolist() == ["ScanNr", "SpecId"] assert len(df) == 10000 + assert all(df.index == range(len(df))) chunk_iterator = reader.get_chunked_data_iterator( chunk_size=3300, columns=["ScanNr", "SpecId"] ) - chunks = [chunk for chunk in chunk_iterator] + chunks = list(chunk_iterator) sizes = [len(chunk) for chunk in chunks] assert sizes == [3300, 3300, 3300, 100] + df_from_chunks = pd.concat(chunks) + assert all(df_from_chunks.index == range(len(df_from_chunks))) def test_dataframe_reader(psm_df_6): @@ -180,19 +186,11 @@ def test_dataframe_reader(psm_df_6): reader.read(), pd.DataFrame({"test": [1, 2, 3]}) ) - # Test whether we can create a reader from an array - reader = DataFrameReader.from_array([1, 2, 3], name="test") - pd.testing.assert_frame_equal( - reader.read(), pd.DataFrame({"test": [1, 2, 3]}) - ) - reader = DataFrameReader.from_array( - np.array([1, 2, 3], - dtype=np.int32), name="test" + np.array([1, 2, 3], dtype=np.int32), name="test" ) pd.testing.assert_frame_equal( - reader.read(), - pd.DataFrame({"test": [1, 2, 3]}, dtype=np.int32) + reader.read(), pd.DataFrame({"test": [1, 2, 3]}, dtype=np.int32) ) @@ -219,13 +217,13 @@ def test_column_renaming(psm_df_6): assert (reader.read().values == orig_reader.read().values).all() assert ( - reader.read(["Pep", "protein", "T", "feature_1"]).values - == orig_reader.read([ - "peptide", - "protein", - "target", - "feature_1", - ]).values + reader.read(["Pep", "protein", "T", "feature_1"]).values + == orig_reader.read([ + "peptide", + "protein", + "target", + "feature_1", + ]).values ).all() renamed_chunk = next( @@ -241,7 +239,10 @@ def test_column_renaming(psm_df_6): assert (renamed_chunk.values == orig_chunk.values).all() -# todo: tests for writers are still missing +# todo: nice to have: tests for writers (CSV, Parquet, buffering) are still +# missing + + def test_tabular_writer_context_manager(tmp_path): # Create a mock class that checks whether it will be correctly initialized # and finalized @@ -249,6 +250,11 @@ class MockWriter(CSVFileWriter): initialized = False finalized = False + def __init__(self): + super().__init__( + tmp_path / "test.csv", columns=["a", "b"], column_types=[] + ) + def initialize(self): super().initialize() self.initialized = True @@ -258,14 +264,14 @@ def finalize(self): self.finalized = True # Check that context manager works for one file - with MockWriter(tmp_path / "test.csv", columns=["a", "b"]) as writer: + with MockWriter() as writer: assert writer.initialized assert not writer.finalized assert writer.finalized # Check that it works when an exception is thrown try: - with MockWriter(tmp_path / "test.csv", columns=["a", "b"]) as writer: + with MockWriter() as writer: assert writer.initialized assert not writer.finalized raise RuntimeError("Just testing") @@ -277,8 +283,8 @@ def finalize(self): # Check that context manager convenience method (auto_finalize) works for # multiple files writers = [ - MockWriter(tmp_path / "test1.csv", columns=["a", "b"]), - MockWriter(tmp_path / "test2.csv", columns=["a", "b"]), + MockWriter(), + MockWriter(), ] assert not writers[0].initialized @@ -293,8 +299,8 @@ def finalize(self): # Now with an exception writers = [ - MockWriter(tmp_path / "test1.csv", columns=["a", "b"]), - MockWriter(tmp_path / "test2.csv", columns=["a", "b"]), + MockWriter(), + MockWriter(), ] try: diff --git a/tests/unit_tests/test_utils.py b/tests/unit_tests/test_utils.py index 3b7bf1e4..4dc60248 100644 --- a/tests/unit_tests/test_utils.py +++ b/tests/unit_tests/test_utils.py @@ -1,8 +1,8 @@ """Test the utility functions""" -import pytest import numpy as np import pandas as pd +import pytest from pandas.testing import assert_series_equal from mokapot import utils @@ -90,25 +90,6 @@ def test_tuplize(): assert utils.tuplize(tuple_in) == tuple_out -def test_merge_sort(merge_sort_data): - files_csv, files_parquet = merge_sort_data - iterable_csv = utils.merge_sort(files_csv, "score") - iterable_parquet = utils.merge_sort(files_parquet, "score") - a = list(iterable_csv) - b = list(iterable_parquet) - - a_ids = [x["SpecId"] for x in a] - b_ids = [x["SpecId"] for x in b] - - assert a_ids == b_ids, "Merge sort ids vary between parquet and csv" - - # This only tests whether tho empty lists are the same, since the iterator - # is consumed when calling list on it. - # assert list(iterable_csv) == list( - # iterable_parquet - # ), "Merge sort ids vary between parquet and csv" - - def test_create_chunks(): # Case 1: Chunk size is less than data length assert utils.create_chunks([1, 2, 3, 4, 5], 2) == [[1, 2], [3, 4], [5]] diff --git a/tests/unit_tests/test_writer_flashlfq.py b/tests/unit_tests/test_writer_flashlfq.py deleted file mode 100644 index 37663005..00000000 --- a/tests/unit_tests/test_writer_flashlfq.py +++ /dev/null @@ -1,78 +0,0 @@ -"""Test that FlashLFQ export is working""" - -import pytest -import mokapot -import numpy as np -import pandas as pd - - -def test_sanity(psms, tmp_path): - """Run simple sanity checks""" - - # conf = psms.assign_confidence(eval_fdr=0.05) - # test1 = conf.to_flashlfq(tmp_path / "test1.txt") - # mokapot.to_flashlfq(conf, tmp_path / "test2.txt") - # test3 = mokapot.to_flashlfq([conf, conf], tmp_path / "test3.txt") - # with pytest.raises(ValueError): - # mokapot.to_flashlfq("blah", tmp_path / "test4.txt") - # - # df1 = pd.read_table(test1) - # df3 = pd.read_table(test3) - # assert 2 * len(df1) == len(df3) - # assert len(df1.columns) == 7 - - # TODO needs to be adapted to OnDisk confidence assignment - pass - - -def test_basic(mock_conf, tmp_path): - """Test that the basic output works""" - conf = mock_conf - df = pd.read_table(mokapot.to_flashlfq(conf, tmp_path / "test.txt")) - expected = pd.DataFrame({ - "File Name": ["c.mzML"] * 2, - "Base Sequence": ["ABCDXYZ", "ABCDEFG"], - "Full Sequence": ["B.ABCD[+2.817]XYZ.A", "ABCDE(shcah8)FG"], - "Peptide Monoisotopic Mass": [1, 2], - "Scan Retention Time": [1.0, 2.0], - "Precursor Charge": [2, 3], - "Protein Accession": ["A|B|C; B|C|A", "A|B|C"], - }) - - pd.testing.assert_frame_equal(df, expected) - - -def test_with_missing(mock_conf, tmp_path): - """Test that missing columns causes errors""" - conf = mock_conf - cols = conf._optional_columns.copy() - for col in ["filename", "calcmass", "rt", "charge"]: - new_cols = cols.copy() - new_cols[col] = None - conf._optional_columns = new_cols - with pytest.raises(ValueError): - mokapot.to_flashlfq(conf, tmp_path / "test.txt") - - -def test_no_proteins(mock_conf, tmp_path): - """Test when no proteins are available""" - conf = mock_conf - conf._protein_column = None - df = pd.read_table(mokapot.to_flashlfq(conf, tmp_path / "test.txt")) - expected = pd.Series([np.nan, np.nan], name="Protein Accession") - pd.testing.assert_series_equal(df["Protein Accession"], expected) - - -def test_fasta_proteins(mock_conf, mock_proteins, tmp_path): - """Test that using mokapot protein groups works""" - conf = mock_conf - conf._proteins = mock_proteins - conf._has_proteins = True - df = pd.read_table(mokapot.to_flashlfq(conf, tmp_path / "test.txt")) - expected = pd.Series(["X|Y|Z", "A|B|C; X|Y|Z"], name="Protein Accession") - pd.testing.assert_series_equal(df["Protein Accession"], expected) - - conf._proteins.shared_peptides = {} - df = pd.read_table(mokapot.to_flashlfq(conf, tmp_path / "test.txt")) - expected = pd.Series(["X|Y|Z"], name="Protein Accession") - pd.testing.assert_series_equal(df["Protein Accession"], expected) diff --git a/tests/unit_tests/test_writer_sqlite.py b/tests/unit_tests/test_writer_sqlite.py index 84816298..273c5fe3 100644 --- a/tests/unit_tests/test_writer_sqlite.py +++ b/tests/unit_tests/test_writer_sqlite.py @@ -1,7 +1,7 @@ import sqlite3 import pandas as pd -from mokapot.confidence_writer import ConfidenceSqliteWriter +from mokapot.tabular_data import ConfidenceSqliteWriter def test_sqlite_writer(confidence_write_data): @@ -12,7 +12,12 @@ def test_sqlite_writer(confidence_write_data): for level, df in confidence_write_data.items(): confidence_writer = ConfidenceSqliteWriter( - connection, columns=df_psm.columns.to_list(), level=level + connection, + columns=df_psm.columns.to_list(), + column_types=[], + level=level, + qvalue_column="q_value", + pep_column="posterior_error_prob", ) confidence_writer.append_data(df) @@ -48,3 +53,4 @@ def prepare_tables_sqlite_db(connection, candidate_ids): connection.execute( f"INSERT INTO CANDIDATE (CANDIDATE_ID) VALUES({c_id});" ) + connection.commit() diff --git a/tests/unit_tests/test_writer_txt.py b/tests/unit_tests/test_writer_txt.py deleted file mode 100644 index 1ee7a1ab..00000000 --- a/tests/unit_tests/test_writer_txt.py +++ /dev/null @@ -1,41 +0,0 @@ -"""Check that writing tab-delimited files works well""" - -import mokapot -import pandas as pd - - -def test_sanity(psms, tmp_path): - """Run simple sanity checks""" - # conf = psms.assign_confidence(eval_fdr=0.05) - # test1 = conf.to_txt(dest_dir=tmp_path, file_root="test1") - # mokapot.to_txt(conf, dest_dir=tmp_path, file_root="test2") - # test3 = mokapot.to_txt( - # [conf, conf], - # dest_dir=tmp_path, - # file_root="test3") - # with pytest.raises(ValueError): - # mokapot.to_txt("blah", dest_dir=tmp_path) - # - # test4 = mokapot.to_txt(conf, dest_dir=tmp_path, decoys=True) - # assert len(test1) == 2 - # assert len(test4) == 4 - # - # fnames = [Path(f).name for f in test1] - # assert fnames == ["test1.mokapot.psms.txt", "test1.mokapot.peptides.txt"] - # - # df1 = pd.read_table(test1[0]) - # df3 = pd.read_table(test3[1]) - # assert 2 * len(df1) == len(df3) - - # TODO needs to be adapted to OnDisk confidence assignment - pass - - -def test_columns(mock_conf, tmp_path): - """Test other specific things""" - conf = mock_conf - df1 = pd.read_table(mokapot.to_txt(conf, dest_dir=tmp_path)[0]) - assert df1.columns[-1] == "protein" - - test2 = mokapot.to_txt(conf, dest_dir=tmp_path, decoys=True) - assert len(test2) == 2 diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..f8f680d5 --- /dev/null +++ b/uv.lock @@ -0,0 +1,2576 @@ +version = 1 +requires-python = ">=3.9" +resolution-markers = [ + "python_full_version < '3.11'", + "python_full_version == '3.11.*'", + "python_full_version >= '3.12'", +] + +[[package]] +name = "alabaster" +version = "0.7.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/3e/13dd8e5ed9094e734ac430b5d0eb4f2bb001708a8b7856cbf8e084e001ba/alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65", size = 23776 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/34/d4e1c02d3bee589efb5dfa17f88ea08bdb3e3eac12bc475462aec52ed223/alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92", size = 13511 }, +] + +[[package]] +name = "appnope" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321 }, +] + +[[package]] +name = "asttokens" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/1d/f03bcb60c4a3212e15f99a56085d93093a497718adf828d050b9d675da81/asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0", size = 62284 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/86/4736ac618d82a20d87d2f92ae19441ebc7ac9e7a581d7e58bbe79233b24a/asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24", size = 27764 }, +] + +[[package]] +name = "attrs" +version = "24.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/0f/aafca9af9315aee06a89ffde799a10a582fe8de76c563ee80bbcdc08b3fb/attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346", size = 792678 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/21/5b6702a7f963e95456c0de2d495f67bf5fd62840ac655dc451586d23d39a/attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2", size = 63001 }, +] + +[[package]] +name = "babel" +version = "2.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/74/f1bc80f23eeba13393b7222b11d95ca3af2c1e28edca18af487137eefed9/babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316", size = 9348104 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/20/bc79bc575ba2e2a7f70e8a1155618bb1301eaa5132a8271373a6903f73f8/babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b", size = 9587599 }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.12.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/ca/824b1195773ce6166d388573fc106ce56d4a805bd7427b624e063596ec58/beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051", size = 581181 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/fe/e8c672695b37eecc5cbf43e1d0638d88d66ba3a44c4d321c796f4e59167f/beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed", size = 147925 }, +] + +[[package]] +name = "bleach" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/10/77f32b088738f40d4f5be801daa5f327879eadd4562f36a2b5ab975ae571/bleach-6.1.0.tar.gz", hash = "sha256:0a31f1837963c41d46bbf1331b8778e1308ea0791db03cc4e7357b97cf42a8fe", size = 202119 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/63/da7237f805089ecc28a3f36bca6a21c31fcbc2eb380f3b8f1be3312abd14/bleach-6.1.0-py3-none-any.whl", hash = "sha256:3225f354cfc436b9789c66c4ee030194bee0568fbf9cbdad3bc8b5c26c5f12b6", size = 162750 }, +] + +[[package]] +name = "certifi" +version = "2024.8.30" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191 }, + { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592 }, + { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024 }, + { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188 }, + { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571 }, + { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687 }, + { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211 }, + { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325 }, + { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784 }, + { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564 }, + { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804 }, + { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299 }, + { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 }, + { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 }, + { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 }, + { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 }, + { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 }, + { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 }, + { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 }, + { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 }, + { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 }, + { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 }, + { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 }, + { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 }, + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 }, + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, + { url = "https://files.pythonhosted.org/packages/b9/ea/8bb50596b8ffbc49ddd7a1ad305035daa770202a6b782fc164647c2673ad/cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", size = 182220 }, + { url = "https://files.pythonhosted.org/packages/ae/11/e77c8cd24f58285a82c23af484cf5b124a376b32644e445960d1a4654c3a/cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", size = 178605 }, + { url = "https://files.pythonhosted.org/packages/ed/65/25a8dc32c53bf5b7b6c2686b42ae2ad58743f7ff644844af7cdb29b49361/cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", size = 424910 }, + { url = "https://files.pythonhosted.org/packages/42/7a/9d086fab7c66bd7c4d0f27c57a1b6b068ced810afc498cc8c49e0088661c/cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", size = 447200 }, + { url = "https://files.pythonhosted.org/packages/da/63/1785ced118ce92a993b0ec9e0d0ac8dc3e5dbfbcaa81135be56c69cabbb6/cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", size = 454565 }, + { url = "https://files.pythonhosted.org/packages/74/06/90b8a44abf3556599cdec107f7290277ae8901a58f75e6fe8f970cd72418/cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", size = 435635 }, + { url = "https://files.pythonhosted.org/packages/bd/62/a1f468e5708a70b1d86ead5bab5520861d9c7eacce4a885ded9faa7729c3/cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", size = 445218 }, + { url = "https://files.pythonhosted.org/packages/5b/95/b34462f3ccb09c2594aa782d90a90b045de4ff1f70148ee79c69d37a0a5a/cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", size = 460486 }, + { url = "https://files.pythonhosted.org/packages/fc/fc/a1e4bebd8d680febd29cf6c8a40067182b64f00c7d105f8f26b5bc54317b/cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", size = 437911 }, + { url = "https://files.pythonhosted.org/packages/e6/c3/21cab7a6154b6a5ea330ae80de386e7665254835b9e98ecc1340b3a7de9a/cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", size = 460632 }, + { url = "https://files.pythonhosted.org/packages/cb/b5/fd9f8b5a84010ca169ee49f4e4ad6f8c05f4e3545b72ee041dbbcb159882/cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7", size = 171820 }, + { url = "https://files.pythonhosted.org/packages/8c/52/b08750ce0bce45c143e1b5d7357ee8c55341b52bdef4b0f081af1eb248c2/cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", size = 181290 }, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", size = 106620 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/8b/825cc84cf13a28bfbcba7c416ec22bf85a9584971be15b21dd8300c65b7f/charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6", size = 196363 }, + { url = "https://files.pythonhosted.org/packages/23/81/d7eef6a99e42c77f444fdd7bc894b0ceca6c3a95c51239e74a722039521c/charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b", size = 125639 }, + { url = "https://files.pythonhosted.org/packages/21/67/b4564d81f48042f520c948abac7079356e94b30cb8ffb22e747532cf469d/charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99", size = 120451 }, + { url = "https://files.pythonhosted.org/packages/c2/72/12a7f0943dd71fb5b4e7b55c41327ac0a1663046a868ee4d0d8e9c369b85/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca", size = 140041 }, + { url = "https://files.pythonhosted.org/packages/67/56/fa28c2c3e31217c4c52158537a2cf5d98a6c1e89d31faf476c89391cd16b/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d", size = 150333 }, + { url = "https://files.pythonhosted.org/packages/f9/d2/466a9be1f32d89eb1554cf84073a5ed9262047acee1ab39cbaefc19635d2/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7", size = 142921 }, + { url = "https://files.pythonhosted.org/packages/f8/01/344ec40cf5d85c1da3c1f57566c59e0c9b56bcc5566c08804a95a6cc8257/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3", size = 144785 }, + { url = "https://files.pythonhosted.org/packages/73/8b/2102692cb6d7e9f03b9a33a710e0164cadfce312872e3efc7cfe22ed26b4/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907", size = 146631 }, + { url = "https://files.pythonhosted.org/packages/d8/96/cc2c1b5d994119ce9f088a9a0c3ebd489d360a2eb058e2c8049f27092847/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b", size = 140867 }, + { url = "https://files.pythonhosted.org/packages/c9/27/cde291783715b8ec30a61c810d0120411844bc4c23b50189b81188b273db/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912", size = 149273 }, + { url = "https://files.pythonhosted.org/packages/3a/a4/8633b0fc1a2d1834d5393dafecce4a1cc56727bfd82b4dc18fc92f0d3cc3/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95", size = 152437 }, + { url = "https://files.pythonhosted.org/packages/64/ea/69af161062166b5975ccbb0961fd2384853190c70786f288684490913bf5/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e", size = 150087 }, + { url = "https://files.pythonhosted.org/packages/3b/fd/e60a9d9fd967f4ad5a92810138192f825d77b4fa2a557990fd575a47695b/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe", size = 145142 }, + { url = "https://files.pythonhosted.org/packages/6d/02/8cb0988a1e49ac9ce2eed1e07b77ff118f2923e9ebd0ede41ba85f2dcb04/charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc", size = 94701 }, + { url = "https://files.pythonhosted.org/packages/d6/20/f1d4670a8a723c46be695dff449d86d6092916f9e99c53051954ee33a1bc/charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749", size = 102191 }, + { url = "https://files.pythonhosted.org/packages/9c/61/73589dcc7a719582bf56aae309b6103d2762b526bffe189d635a7fcfd998/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", size = 193339 }, + { url = "https://files.pythonhosted.org/packages/77/d5/8c982d58144de49f59571f940e329ad6e8615e1e82ef84584c5eeb5e1d72/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", size = 124366 }, + { url = "https://files.pythonhosted.org/packages/bf/19/411a64f01ee971bed3231111b69eb56f9331a769072de479eae7de52296d/charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", size = 118874 }, + { url = "https://files.pythonhosted.org/packages/4c/92/97509850f0d00e9f14a46bc751daabd0ad7765cff29cdfb66c68b6dad57f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", size = 138243 }, + { url = "https://files.pythonhosted.org/packages/e2/29/d227805bff72ed6d6cb1ce08eec707f7cfbd9868044893617eb331f16295/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", size = 148676 }, + { url = "https://files.pythonhosted.org/packages/13/bc/87c2c9f2c144bedfa62f894c3007cd4530ba4b5351acb10dc786428a50f0/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", size = 141289 }, + { url = "https://files.pythonhosted.org/packages/eb/5b/6f10bad0f6461fa272bfbbdf5d0023b5fb9bc6217c92bf068fa5a99820f5/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", size = 142585 }, + { url = "https://files.pythonhosted.org/packages/3b/a0/a68980ab8a1f45a36d9745d35049c1af57d27255eff8c907e3add84cf68f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", size = 144408 }, + { url = "https://files.pythonhosted.org/packages/d7/a1/493919799446464ed0299c8eef3c3fad0daf1c3cd48bff9263c731b0d9e2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", size = 139076 }, + { url = "https://files.pythonhosted.org/packages/fb/9d/9c13753a5a6e0db4a0a6edb1cef7aee39859177b64e1a1e748a6e3ba62c2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", size = 146874 }, + { url = "https://files.pythonhosted.org/packages/75/d2/0ab54463d3410709c09266dfb416d032a08f97fd7d60e94b8c6ef54ae14b/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", size = 150871 }, + { url = "https://files.pythonhosted.org/packages/8d/c9/27e41d481557be53d51e60750b85aa40eaf52b841946b3cdeff363105737/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", size = 148546 }, + { url = "https://files.pythonhosted.org/packages/ee/44/4f62042ca8cdc0cabf87c0fc00ae27cd8b53ab68be3605ba6d071f742ad3/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", size = 143048 }, + { url = "https://files.pythonhosted.org/packages/01/f8/38842422988b795220eb8038745d27a675ce066e2ada79516c118f291f07/charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", size = 94389 }, + { url = "https://files.pythonhosted.org/packages/0b/6e/b13bd47fa9023b3699e94abf565b5a2f0b0be6e9ddac9812182596ee62e4/charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", size = 101752 }, + { url = "https://files.pythonhosted.org/packages/d3/0b/4b7a70987abf9b8196845806198975b6aab4ce016632f817ad758a5aa056/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", size = 194445 }, + { url = "https://files.pythonhosted.org/packages/50/89/354cc56cf4dd2449715bc9a0f54f3aef3dc700d2d62d1fa5bbea53b13426/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", size = 125275 }, + { url = "https://files.pythonhosted.org/packages/fa/44/b730e2a2580110ced837ac083d8ad222343c96bb6b66e9e4e706e4d0b6df/charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", size = 119020 }, + { url = "https://files.pythonhosted.org/packages/9d/e4/9263b8240ed9472a2ae7ddc3e516e71ef46617fe40eaa51221ccd4ad9a27/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", size = 139128 }, + { url = "https://files.pythonhosted.org/packages/6b/e3/9f73e779315a54334240353eaea75854a9a690f3f580e4bd85d977cb2204/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", size = 149277 }, + { url = "https://files.pythonhosted.org/packages/1a/cf/f1f50c2f295312edb8a548d3fa56a5c923b146cd3f24114d5adb7e7be558/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", size = 142174 }, + { url = "https://files.pythonhosted.org/packages/16/92/92a76dc2ff3a12e69ba94e7e05168d37d0345fa08c87e1fe24d0c2a42223/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", size = 143838 }, + { url = "https://files.pythonhosted.org/packages/a4/01/2117ff2b1dfc61695daf2babe4a874bca328489afa85952440b59819e9d7/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", size = 146149 }, + { url = "https://files.pythonhosted.org/packages/f6/9b/93a332b8d25b347f6839ca0a61b7f0287b0930216994e8bf67a75d050255/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", size = 140043 }, + { url = "https://files.pythonhosted.org/packages/ab/f6/7ac4a01adcdecbc7a7587767c776d53d369b8b971382b91211489535acf0/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", size = 148229 }, + { url = "https://files.pythonhosted.org/packages/9d/be/5708ad18161dee7dc6a0f7e6cf3a88ea6279c3e8484844c0590e50e803ef/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", size = 151556 }, + { url = "https://files.pythonhosted.org/packages/5a/bb/3d8bc22bacb9eb89785e83e6723f9888265f3a0de3b9ce724d66bd49884e/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", size = 149772 }, + { url = "https://files.pythonhosted.org/packages/f7/fa/d3fc622de05a86f30beea5fc4e9ac46aead4731e73fd9055496732bcc0a4/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", size = 144800 }, + { url = "https://files.pythonhosted.org/packages/9a/65/bdb9bc496d7d190d725e96816e20e2ae3a6fa42a5cac99c3c3d6ff884118/charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", size = 94836 }, + { url = "https://files.pythonhosted.org/packages/3e/67/7b72b69d25b89c0b3cea583ee372c43aa24df15f0e0f8d3982c57804984b/charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", size = 102187 }, + { url = "https://files.pythonhosted.org/packages/f3/89/68a4c86f1a0002810a27f12e9a7b22feb198c59b2f05231349fbce5c06f4/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", size = 194617 }, + { url = "https://files.pythonhosted.org/packages/4f/cd/8947fe425e2ab0aa57aceb7807af13a0e4162cd21eee42ef5b053447edf5/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", size = 125310 }, + { url = "https://files.pythonhosted.org/packages/5b/f0/b5263e8668a4ee9becc2b451ed909e9c27058337fda5b8c49588183c267a/charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", size = 119126 }, + { url = "https://files.pythonhosted.org/packages/ff/6e/e445afe4f7fda27a533f3234b627b3e515a1b9429bc981c9a5e2aa5d97b6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", size = 139342 }, + { url = "https://files.pythonhosted.org/packages/a1/b2/4af9993b532d93270538ad4926c8e37dc29f2111c36f9c629840c57cd9b3/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", size = 149383 }, + { url = "https://files.pythonhosted.org/packages/fb/6f/4e78c3b97686b871db9be6f31d64e9264e889f8c9d7ab33c771f847f79b7/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", size = 142214 }, + { url = "https://files.pythonhosted.org/packages/2b/c9/1c8fe3ce05d30c87eff498592c89015b19fade13df42850aafae09e94f35/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", size = 144104 }, + { url = "https://files.pythonhosted.org/packages/ee/68/efad5dcb306bf37db7db338338e7bb8ebd8cf38ee5bbd5ceaaaa46f257e6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", size = 146255 }, + { url = "https://files.pythonhosted.org/packages/0c/75/1ed813c3ffd200b1f3e71121c95da3f79e6d2a96120163443b3ad1057505/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", size = 140251 }, + { url = "https://files.pythonhosted.org/packages/7d/0d/6f32255c1979653b448d3c709583557a4d24ff97ac4f3a5be156b2e6a210/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", size = 148474 }, + { url = "https://files.pythonhosted.org/packages/ac/a0/c1b5298de4670d997101fef95b97ac440e8c8d8b4efa5a4d1ef44af82f0d/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", size = 151849 }, + { url = "https://files.pythonhosted.org/packages/04/4f/b3961ba0c664989ba63e30595a3ed0875d6790ff26671e2aae2fdc28a399/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", size = 149781 }, + { url = "https://files.pythonhosted.org/packages/d8/90/6af4cd042066a4adad58ae25648a12c09c879efa4849c705719ba1b23d8c/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482", size = 144970 }, + { url = "https://files.pythonhosted.org/packages/cc/67/e5e7e0cbfefc4ca79025238b43cdf8a2037854195b37d6417f3d0895c4c2/charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", size = 94973 }, + { url = "https://files.pythonhosted.org/packages/65/97/fc9bbc54ee13d33dc54a7fcf17b26368b18505500fc01e228c27b5222d80/charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", size = 102308 }, + { url = "https://files.pythonhosted.org/packages/54/2f/28659eee7f5d003e0f5a3b572765bf76d6e0fe6601ab1f1b1dd4cba7e4f1/charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa", size = 196326 }, + { url = "https://files.pythonhosted.org/packages/d1/18/92869d5c0057baa973a3ee2af71573be7b084b3c3d428fe6463ce71167f8/charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a", size = 125614 }, + { url = "https://files.pythonhosted.org/packages/d6/27/327904c5a54a7796bb9f36810ec4173d2df5d88b401d2b95ef53111d214e/charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0", size = 120450 }, + { url = "https://files.pythonhosted.org/packages/a4/23/65af317914a0308495133b2d654cf67b11bbd6ca16637c4e8a38f80a5a69/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a", size = 140135 }, + { url = "https://files.pythonhosted.org/packages/f2/41/6190102ad521a8aa888519bb014a74251ac4586cde9b38e790901684f9ab/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242", size = 150413 }, + { url = "https://files.pythonhosted.org/packages/7b/ab/f47b0159a69eab9bd915591106859f49670c75f9a19082505ff16f50efc0/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b", size = 142992 }, + { url = "https://files.pythonhosted.org/packages/28/89/60f51ad71f63aaaa7e51a2a2ad37919985a341a1d267070f212cdf6c2d22/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62", size = 144871 }, + { url = "https://files.pythonhosted.org/packages/0c/48/0050550275fea585a6e24460b42465020b53375017d8596c96be57bfabca/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0", size = 146756 }, + { url = "https://files.pythonhosted.org/packages/dc/b5/47f8ee91455946f745e6c9ddbb0f8f50314d2416dd922b213e7d5551ad09/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd", size = 141034 }, + { url = "https://files.pythonhosted.org/packages/84/79/5c731059ebab43e80bf61fa51666b9b18167974b82004f18c76378ed31a3/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be", size = 149434 }, + { url = "https://files.pythonhosted.org/packages/ca/f3/0719cd09fc4dc42066f239cb3c48ced17fc3316afca3e2a30a4756fe49ab/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d", size = 152443 }, + { url = "https://files.pythonhosted.org/packages/f7/0e/c6357297f1157c8e8227ff337e93fd0a90e498e3d6ab96b2782204ecae48/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3", size = 150294 }, + { url = "https://files.pythonhosted.org/packages/54/9a/acfa96dc4ea8c928040b15822b59d0863d6e1757fba8bd7de3dc4f761c13/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742", size = 145314 }, + { url = "https://files.pythonhosted.org/packages/73/1c/b10a63032eaebb8d7bcb8544f12f063f41f5f463778ac61da15d9985e8b6/charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2", size = 94724 }, + { url = "https://files.pythonhosted.org/packages/c5/77/3a78bf28bfaa0863f9cfef278dbeadf55efe064eafff8c7c424ae3c4c1bf/charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca", size = 102159 }, + { url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "comm" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/a8/fb783cb0abe2b5fded9f55e5703015cdf1c9c85b3669087c538dd15a6a86/comm-0.2.2.tar.gz", hash = "sha256:3fd7a84065306e07bea1773df6eb8282de51ba82f77c72f9c85716ab11fe980e", size = 6210 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/75/49e5bfe642f71f272236b5b2d2691cf915a7283cc0ceda56357b61daa538/comm-0.2.2-py3-none-any.whl", hash = "sha256:e6fb86cb70ff661ee8c9c14e7d36d6de3b4066f1441be4063df9c5009f0a64d3", size = 7180 }, +] + +[[package]] +name = "commonmark" +version = "0.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/60/48/a60f593447e8f0894ebb7f6e6c1f25dafc5e89c5879fdc9360ae93ff83f0/commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60", size = 95764 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/92/dfd892312d822f36c55366118b95d914e5f16de11044a27cf10a7d71bbbf/commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9", size = 51068 }, +] + +[[package]] +name = "contourpy" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/f6/31a8f28b4a2a4fa0e01085e542f3081ab0588eff8e589d39d775172c9792/contourpy-1.3.0.tar.gz", hash = "sha256:7ffa0db17717a8ffb127efd0c95a4362d996b892c2904db72428d5b52e1938a4", size = 13464370 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/e0/be8dcc796cfdd96708933e0e2da99ba4bb8f9b2caa9d560a50f3f09a65f3/contourpy-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:880ea32e5c774634f9fcd46504bf9f080a41ad855f4fef54f5380f5133d343c7", size = 265366 }, + { url = "https://files.pythonhosted.org/packages/50/d6/c953b400219443535d412fcbbc42e7a5e823291236bc0bb88936e3cc9317/contourpy-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:76c905ef940a4474a6289c71d53122a4f77766eef23c03cd57016ce19d0f7b42", size = 249226 }, + { url = "https://files.pythonhosted.org/packages/6f/b4/6fffdf213ffccc28483c524b9dad46bb78332851133b36ad354b856ddc7c/contourpy-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92f8557cbb07415a4d6fa191f20fd9d2d9eb9c0b61d1b2f52a8926e43c6e9af7", size = 308460 }, + { url = "https://files.pythonhosted.org/packages/cf/6c/118fc917b4050f0afe07179a6dcbe4f3f4ec69b94f36c9e128c4af480fb8/contourpy-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36f965570cff02b874773c49bfe85562b47030805d7d8360748f3eca570f4cab", size = 347623 }, + { url = "https://files.pythonhosted.org/packages/f9/a4/30ff110a81bfe3abf7b9673284d21ddce8cc1278f6f77393c91199da4c90/contourpy-1.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cacd81e2d4b6f89c9f8a5b69b86490152ff39afc58a95af002a398273e5ce589", size = 317761 }, + { url = "https://files.pythonhosted.org/packages/99/e6/d11966962b1aa515f5586d3907ad019f4b812c04e4546cc19ebf62b5178e/contourpy-1.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69375194457ad0fad3a839b9e29aa0b0ed53bb54db1bfb6c3ae43d111c31ce41", size = 322015 }, + { url = "https://files.pythonhosted.org/packages/4d/e3/182383743751d22b7b59c3c753277b6aee3637049197624f333dac5b4c80/contourpy-1.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a52040312b1a858b5e31ef28c2e865376a386c60c0e248370bbea2d3f3b760d", size = 1262672 }, + { url = "https://files.pythonhosted.org/packages/78/53/974400c815b2e605f252c8fb9297e2204347d1755a5374354ee77b1ea259/contourpy-1.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3faeb2998e4fcb256542e8a926d08da08977f7f5e62cf733f3c211c2a5586223", size = 1321688 }, + { url = "https://files.pythonhosted.org/packages/52/29/99f849faed5593b2926a68a31882af98afbeac39c7fdf7de491d9c85ec6a/contourpy-1.3.0-cp310-cp310-win32.whl", hash = "sha256:36e0cff201bcb17a0a8ecc7f454fe078437fa6bda730e695a92f2d9932bd507f", size = 171145 }, + { url = "https://files.pythonhosted.org/packages/a9/97/3f89bba79ff6ff2b07a3cbc40aa693c360d5efa90d66e914f0ff03b95ec7/contourpy-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:87ddffef1dbe5e669b5c2440b643d3fdd8622a348fe1983fad7a0f0ccb1cd67b", size = 216019 }, + { url = "https://files.pythonhosted.org/packages/b3/1f/9375917786cb39270b0ee6634536c0e22abf225825602688990d8f5c6c19/contourpy-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fa4c02abe6c446ba70d96ece336e621efa4aecae43eaa9b030ae5fb92b309ad", size = 266356 }, + { url = "https://files.pythonhosted.org/packages/05/46/9256dd162ea52790c127cb58cfc3b9e3413a6e3478917d1f811d420772ec/contourpy-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:834e0cfe17ba12f79963861e0f908556b2cedd52e1f75e6578801febcc6a9f49", size = 250915 }, + { url = "https://files.pythonhosted.org/packages/e1/5d/3056c167fa4486900dfbd7e26a2fdc2338dc58eee36d490a0ed3ddda5ded/contourpy-1.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbc4c3217eee163fa3984fd1567632b48d6dfd29216da3ded3d7b844a8014a66", size = 310443 }, + { url = "https://files.pythonhosted.org/packages/ca/c2/1a612e475492e07f11c8e267ea5ec1ce0d89971be496c195e27afa97e14a/contourpy-1.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4865cd1d419e0c7a7bf6de1777b185eebdc51470800a9f42b9e9decf17762081", size = 348548 }, + { url = "https://files.pythonhosted.org/packages/45/cf/2c2fc6bb5874158277b4faf136847f0689e1b1a1f640a36d76d52e78907c/contourpy-1.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:303c252947ab4b14c08afeb52375b26781ccd6a5ccd81abcdfc1fafd14cf93c1", size = 319118 }, + { url = "https://files.pythonhosted.org/packages/03/33/003065374f38894cdf1040cef474ad0546368eea7e3a51d48b8a423961f8/contourpy-1.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637f674226be46f6ba372fd29d9523dd977a291f66ab2a74fbeb5530bb3f445d", size = 323162 }, + { url = "https://files.pythonhosted.org/packages/42/80/e637326e85e4105a802e42959f56cff2cd39a6b5ef68d5d9aee3ea5f0e4c/contourpy-1.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:76a896b2f195b57db25d6b44e7e03f221d32fe318d03ede41f8b4d9ba1bff53c", size = 1265396 }, + { url = "https://files.pythonhosted.org/packages/7c/3b/8cbd6416ca1bbc0202b50f9c13b2e0b922b64be888f9d9ee88e6cfabfb51/contourpy-1.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e1fd23e9d01591bab45546c089ae89d926917a66dceb3abcf01f6105d927e2cb", size = 1324297 }, + { url = "https://files.pythonhosted.org/packages/4d/2c/021a7afaa52fe891f25535506cc861c30c3c4e5a1c1ce94215e04b293e72/contourpy-1.3.0-cp311-cp311-win32.whl", hash = "sha256:d402880b84df3bec6eab53cd0cf802cae6a2ef9537e70cf75e91618a3801c20c", size = 171808 }, + { url = "https://files.pythonhosted.org/packages/8d/2f/804f02ff30a7fae21f98198828d0857439ec4c91a96e20cf2d6c49372966/contourpy-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:6cb6cc968059db9c62cb35fbf70248f40994dfcd7aa10444bbf8b3faeb7c2d67", size = 217181 }, + { url = "https://files.pythonhosted.org/packages/c9/92/8e0bbfe6b70c0e2d3d81272b58c98ac69ff1a4329f18c73bd64824d8b12e/contourpy-1.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:570ef7cf892f0afbe5b2ee410c507ce12e15a5fa91017a0009f79f7d93a1268f", size = 267838 }, + { url = "https://files.pythonhosted.org/packages/e3/04/33351c5d5108460a8ce6d512307690b023f0cfcad5899499f5c83b9d63b1/contourpy-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:da84c537cb8b97d153e9fb208c221c45605f73147bd4cadd23bdae915042aad6", size = 251549 }, + { url = "https://files.pythonhosted.org/packages/51/3d/aa0fe6ae67e3ef9f178389e4caaaa68daf2f9024092aa3c6032e3d174670/contourpy-1.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0be4d8425bfa755e0fd76ee1e019636ccc7c29f77a7c86b4328a9eb6a26d0639", size = 303177 }, + { url = "https://files.pythonhosted.org/packages/56/c3/c85a7e3e0cab635575d3b657f9535443a6f5d20fac1a1911eaa4bbe1aceb/contourpy-1.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c0da700bf58f6e0b65312d0a5e695179a71d0163957fa381bb3c1f72972537c", size = 341735 }, + { url = "https://files.pythonhosted.org/packages/dd/8d/20f7a211a7be966a53f474bc90b1a8202e9844b3f1ef85f3ae45a77151ee/contourpy-1.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb8b141bb00fa977d9122636b16aa67d37fd40a3d8b52dd837e536d64b9a4d06", size = 314679 }, + { url = "https://files.pythonhosted.org/packages/6e/be/524e377567defac0e21a46e2a529652d165fed130a0d8a863219303cee18/contourpy-1.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3634b5385c6716c258d0419c46d05c8aa7dc8cb70326c9a4fb66b69ad2b52e09", size = 320549 }, + { url = "https://files.pythonhosted.org/packages/0f/96/fdb2552a172942d888915f3a6663812e9bc3d359d53dafd4289a0fb462f0/contourpy-1.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0dce35502151b6bd35027ac39ba6e5a44be13a68f55735c3612c568cac3805fd", size = 1263068 }, + { url = "https://files.pythonhosted.org/packages/2a/25/632eab595e3140adfa92f1322bf8915f68c932bac468e89eae9974cf1c00/contourpy-1.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea348f053c645100612b333adc5983d87be69acdc6d77d3169c090d3b01dc35", size = 1322833 }, + { url = "https://files.pythonhosted.org/packages/73/e3/69738782e315a1d26d29d71a550dbbe3eb6c653b028b150f70c1a5f4f229/contourpy-1.3.0-cp312-cp312-win32.whl", hash = "sha256:90f73a5116ad1ba7174341ef3ea5c3150ddf20b024b98fb0c3b29034752c8aeb", size = 172681 }, + { url = "https://files.pythonhosted.org/packages/0c/89/9830ba00d88e43d15e53d64931e66b8792b46eb25e2050a88fec4a0df3d5/contourpy-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:b11b39aea6be6764f84360fce6c82211a9db32a7c7de8fa6dd5397cf1d079c3b", size = 218283 }, + { url = "https://files.pythonhosted.org/packages/53/a1/d20415febfb2267af2d7f06338e82171824d08614084714fb2c1dac9901f/contourpy-1.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3e1c7fa44aaae40a2247e2e8e0627f4bea3dd257014764aa644f319a5f8600e3", size = 267879 }, + { url = "https://files.pythonhosted.org/packages/aa/45/5a28a3570ff6218d8bdfc291a272a20d2648104815f01f0177d103d985e1/contourpy-1.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:364174c2a76057feef647c802652f00953b575723062560498dc7930fc9b1cb7", size = 251573 }, + { url = "https://files.pythonhosted.org/packages/39/1c/d3f51540108e3affa84f095c8b04f0aa833bb797bc8baa218a952a98117d/contourpy-1.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32b238b3b3b649e09ce9aaf51f0c261d38644bdfa35cbaf7b263457850957a84", size = 303184 }, + { url = "https://files.pythonhosted.org/packages/00/56/1348a44fb6c3a558c1a3a0cd23d329d604c99d81bf5a4b58c6b71aab328f/contourpy-1.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d51fca85f9f7ad0b65b4b9fe800406d0d77017d7270d31ec3fb1cc07358fdea0", size = 340262 }, + { url = "https://files.pythonhosted.org/packages/2b/23/00d665ba67e1bb666152131da07e0f24c95c3632d7722caa97fb61470eca/contourpy-1.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:732896af21716b29ab3e988d4ce14bc5133733b85956316fb0c56355f398099b", size = 313806 }, + { url = "https://files.pythonhosted.org/packages/5a/42/3cf40f7040bb8362aea19af9a5fb7b32ce420f645dd1590edcee2c657cd5/contourpy-1.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d73f659398a0904e125280836ae6f88ba9b178b2fed6884f3b1f95b989d2c8da", size = 319710 }, + { url = "https://files.pythonhosted.org/packages/05/32/f3bfa3fc083b25e1a7ae09197f897476ee68e7386e10404bdf9aac7391f0/contourpy-1.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c6c7c2408b7048082932cf4e641fa3b8ca848259212f51c8c59c45aa7ac18f14", size = 1264107 }, + { url = "https://files.pythonhosted.org/packages/1c/1e/1019d34473a736664f2439542b890b2dc4c6245f5c0d8cdfc0ccc2cab80c/contourpy-1.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f317576606de89da6b7e0861cf6061f6146ead3528acabff9236458a6ba467f8", size = 1322458 }, + { url = "https://files.pythonhosted.org/packages/22/85/4f8bfd83972cf8909a4d36d16b177f7b8bdd942178ea4bf877d4a380a91c/contourpy-1.3.0-cp313-cp313-win32.whl", hash = "sha256:31cd3a85dbdf1fc002280c65caa7e2b5f65e4a973fcdf70dd2fdcb9868069294", size = 172643 }, + { url = "https://files.pythonhosted.org/packages/cc/4a/fb3c83c1baba64ba90443626c228ca14f19a87c51975d3b1de308dd2cf08/contourpy-1.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:4553c421929ec95fb07b3aaca0fae668b2eb5a5203d1217ca7c34c063c53d087", size = 218301 }, + { url = "https://files.pythonhosted.org/packages/76/65/702f4064f397821fea0cb493f7d3bc95a5d703e20954dce7d6d39bacf378/contourpy-1.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:345af746d7766821d05d72cb8f3845dfd08dd137101a2cb9b24de277d716def8", size = 278972 }, + { url = "https://files.pythonhosted.org/packages/80/85/21f5bba56dba75c10a45ec00ad3b8190dbac7fd9a8a8c46c6116c933e9cf/contourpy-1.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3bb3808858a9dc68f6f03d319acd5f1b8a337e6cdda197f02f4b8ff67ad2057b", size = 263375 }, + { url = "https://files.pythonhosted.org/packages/0a/64/084c86ab71d43149f91ab3a4054ccf18565f0a8af36abfa92b1467813ed6/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:420d39daa61aab1221567b42eecb01112908b2cab7f1b4106a52caaec8d36973", size = 307188 }, + { url = "https://files.pythonhosted.org/packages/3d/ff/d61a4c288dc42da0084b8d9dc2aa219a850767165d7d9a9c364ff530b509/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d63ee447261e963af02642ffcb864e5a2ee4cbfd78080657a9880b8b1868e18", size = 345644 }, + { url = "https://files.pythonhosted.org/packages/ca/aa/00d2313d35ec03f188e8f0786c2fc61f589306e02fdc158233697546fd58/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:167d6c890815e1dac9536dca00828b445d5d0df4d6a8c6adb4a7ec3166812fa8", size = 317141 }, + { url = "https://files.pythonhosted.org/packages/8d/6a/b5242c8cb32d87f6abf4f5e3044ca397cb1a76712e3fa2424772e3ff495f/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:710a26b3dc80c0e4febf04555de66f5fd17e9cf7170a7b08000601a10570bda6", size = 323469 }, + { url = "https://files.pythonhosted.org/packages/6f/a6/73e929d43028a9079aca4bde107494864d54f0d72d9db508a51ff0878593/contourpy-1.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:75ee7cb1a14c617f34a51d11fa7524173e56551646828353c4af859c56b766e2", size = 1260894 }, + { url = "https://files.pythonhosted.org/packages/2b/1e/1e726ba66eddf21c940821df8cf1a7d15cb165f0682d62161eaa5e93dae1/contourpy-1.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:33c92cdae89ec5135d036e7218e69b0bb2851206077251f04a6c4e0e21f03927", size = 1314829 }, + { url = "https://files.pythonhosted.org/packages/b3/e3/b9f72758adb6ef7397327ceb8b9c39c75711affb220e4f53c745ea1d5a9a/contourpy-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a11077e395f67ffc2c44ec2418cfebed032cd6da3022a94fc227b6faf8e2acb8", size = 265518 }, + { url = "https://files.pythonhosted.org/packages/ec/22/19f5b948367ab5260fb41d842c7a78dae645603881ea6bc39738bcfcabf6/contourpy-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e8134301d7e204c88ed7ab50028ba06c683000040ede1d617298611f9dc6240c", size = 249350 }, + { url = "https://files.pythonhosted.org/packages/26/76/0c7d43263dd00ae21a91a24381b7e813d286a3294d95d179ef3a7b9fb1d7/contourpy-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e12968fdfd5bb45ffdf6192a590bd8ddd3ba9e58360b29683c6bb71a7b41edca", size = 309167 }, + { url = "https://files.pythonhosted.org/packages/96/3b/cadff6773e89f2a5a492c1a8068e21d3fccaf1a1c1df7d65e7c8e3ef60ba/contourpy-1.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fd2a0fc506eccaaa7595b7e1418951f213cf8255be2600f1ea1b61e46a60c55f", size = 348279 }, + { url = "https://files.pythonhosted.org/packages/e1/86/158cc43aa549d2081a955ab11c6bdccc7a22caacc2af93186d26f5f48746/contourpy-1.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4cfb5c62ce023dfc410d6059c936dcf96442ba40814aefbfa575425a3a7f19dc", size = 318519 }, + { url = "https://files.pythonhosted.org/packages/05/11/57335544a3027e9b96a05948c32e566328e3a2f84b7b99a325b7a06d2b06/contourpy-1.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68a32389b06b82c2fdd68276148d7b9275b5f5cf13e5417e4252f6d1a34f72a2", size = 321922 }, + { url = "https://files.pythonhosted.org/packages/0b/e3/02114f96543f4a1b694333b92a6dcd4f8eebbefcc3a5f3bbb1316634178f/contourpy-1.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:94e848a6b83da10898cbf1311a815f770acc9b6a3f2d646f330d57eb4e87592e", size = 1258017 }, + { url = "https://files.pythonhosted.org/packages/f3/3b/bfe4c81c6d5881c1c643dde6620be0b42bf8aab155976dd644595cfab95c/contourpy-1.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d78ab28a03c854a873787a0a42254a0ccb3cb133c672f645c9f9c8f3ae9d0800", size = 1316773 }, + { url = "https://files.pythonhosted.org/packages/f1/17/c52d2970784383cafb0bd918b6fb036d98d96bbf0bc1befb5d1e31a07a70/contourpy-1.3.0-cp39-cp39-win32.whl", hash = "sha256:81cb5ed4952aae6014bc9d0421dec7c5835c9c8c31cdf51910b708f548cf58e5", size = 171353 }, + { url = "https://files.pythonhosted.org/packages/53/23/db9f69676308e094d3c45f20cc52e12d10d64f027541c995d89c11ad5c75/contourpy-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:14e262f67bd7e6eb6880bc564dcda30b15e351a594657e55b7eec94b6ef72843", size = 211817 }, + { url = "https://files.pythonhosted.org/packages/d1/09/60e486dc2b64c94ed33e58dcfb6f808192c03dfc5574c016218b9b7680dc/contourpy-1.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fe41b41505a5a33aeaed2a613dccaeaa74e0e3ead6dd6fd3a118fb471644fd6c", size = 261886 }, + { url = "https://files.pythonhosted.org/packages/19/20/b57f9f7174fcd439a7789fb47d764974ab646fa34d1790551de386457a8e/contourpy-1.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca7e17a65f72a5133bdbec9ecf22401c62bcf4821361ef7811faee695799779", size = 311008 }, + { url = "https://files.pythonhosted.org/packages/74/fc/5040d42623a1845d4f17a418e590fd7a79ae8cb2bad2b2f83de63c3bdca4/contourpy-1.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1ec4dc6bf570f5b22ed0d7efba0dfa9c5b9e0431aeea7581aa217542d9e809a4", size = 215690 }, + { url = "https://files.pythonhosted.org/packages/2b/24/dc3dcd77ac7460ab7e9d2b01a618cb31406902e50e605a8d6091f0a8f7cc/contourpy-1.3.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:00ccd0dbaad6d804ab259820fa7cb0b8036bda0686ef844d24125d8287178ce0", size = 261894 }, + { url = "https://files.pythonhosted.org/packages/b1/db/531642a01cfec39d1682e46b5457b07cf805e3c3c584ec27e2a6223f8f6c/contourpy-1.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ca947601224119117f7c19c9cdf6b3ab54c5726ef1d906aa4a69dfb6dd58102", size = 311099 }, + { url = "https://files.pythonhosted.org/packages/38/1e/94bda024d629f254143a134eead69e21c836429a2a6ce82209a00ddcb79a/contourpy-1.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6ec93afeb848a0845a18989da3beca3eec2c0f852322efe21af1931147d12cb", size = 215838 }, +] + +[[package]] +name = "coverage" +version = "7.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/60/e781e8302e7b28f21ce06e30af077f856aa2cb4cf2253287dae9a593d509/coverage-7.6.2.tar.gz", hash = "sha256:a5f81e68aa62bc0cfca04f7b19eaa8f9c826b53fc82ab9e2121976dc74f131f3", size = 797872 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/14/fb75c01b8427fb567c90ce920c90ed2bd314ad6960d54e8b377928607fd1/coverage-7.6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c9df1950fb92d49970cce38100d7e7293c84ed3606eaa16ea0b6bc27175bb667", size = 206561 }, + { url = "https://files.pythonhosted.org/packages/93/b4/dcbf15f5583507415d0a78ce206e19d76699f1161e8b1ff6e1a21e9f9743/coverage-7.6.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:24500f4b0e03aab60ce575c85365beab64b44d4db837021e08339f61d1fbfe52", size = 206994 }, + { url = "https://files.pythonhosted.org/packages/47/ee/57d607e14479fb760721ea1784608ade532665934bd75f260b250dc6c877/coverage-7.6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a663b180b6669c400b4630a24cc776f23a992d38ce7ae72ede2a397ce6b0f170", size = 235429 }, + { url = "https://files.pythonhosted.org/packages/76/e1/cd263fd750fdb115aab11a086e3584d99d46fca1f201b5493cc3972aea28/coverage-7.6.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfde025e2793a22efe8c21f807d276bd1d6a4bcc5ba6f19dbdfc4e7a12160909", size = 233329 }, + { url = "https://files.pythonhosted.org/packages/30/3b/a1623d50fcd6ba532cef0c3c1059eec2a08a311676ffa84dbe4beb2b8a33/coverage-7.6.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:087932079c065d7b8ebadd3a0160656c55954144af6439886c8bcf78bbbcde7f", size = 234491 }, + { url = "https://files.pythonhosted.org/packages/b1/a6/8f3b3fd1f9b9400f3df38a7159362622546e2d951cc4984cf4617d0fd4d7/coverage-7.6.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9c6b0c1cafd96213a0327cf680acb39f70e452caf8e9a25aeb05316db9c07f89", size = 233589 }, + { url = "https://files.pythonhosted.org/packages/e3/40/37d64093f57b372435d87679956607ecab066d2aede76c6d215815a35fa3/coverage-7.6.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6e85830eed5b5263ffa0c62428e43cb844296f3b4461f09e4bdb0d44ec190bc2", size = 232050 }, + { url = "https://files.pythonhosted.org/packages/80/63/cbb76298b4f42bffe0030f1bc129a26a26255857c6beaa20419259ac07cc/coverage-7.6.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:62ab4231c01e156ece1b3a187c87173f31cbeee83a5e1f6dff17f288dca93345", size = 233180 }, + { url = "https://files.pythonhosted.org/packages/7a/6a/eafa81503e905d473b799920927b06aa6ffba12db035fc98735b55bc1741/coverage-7.6.2-cp310-cp310-win32.whl", hash = "sha256:7b80fbb0da3aebde102a37ef0138aeedff45997e22f8962e5f16ae1742852676", size = 209281 }, + { url = "https://files.pythonhosted.org/packages/19/d1/6b354c2cd52e0244944c097aaa71896869878df999f5f8e75fcd37eaf0f3/coverage-7.6.2-cp310-cp310-win_amd64.whl", hash = "sha256:d20c3d1f31f14d6962a4e2f549c21d31e670b90f777ef4171be540fb7fb70f02", size = 210092 }, + { url = "https://files.pythonhosted.org/packages/a5/29/72da824da4182f518b054c21552b7ed2473a4e4c6ac616298209808a1a5c/coverage-7.6.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bb21bac7783c1bf6f4bbe68b1e0ff0d20e7e7732cfb7995bc8d96e23aa90fc7b", size = 206667 }, + { url = "https://files.pythonhosted.org/packages/23/52/c15dcf3cf575256c7c0992e441cd41092a6c519d65abe1eb5567aab3d8e8/coverage-7.6.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a7b2e437fbd8fae5bc7716b9c7ff97aecc95f0b4d56e4ca08b3c8d8adcaadb84", size = 207111 }, + { url = "https://files.pythonhosted.org/packages/92/61/0d46dc26cf9f711b7b6078a54680665a5c2d62ec15991adb51e79236c699/coverage-7.6.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:536f77f2bf5797983652d1d55f1a7272a29afcc89e3ae51caa99b2db4e89d658", size = 239050 }, + { url = "https://files.pythonhosted.org/packages/3b/cb/9de71bade0343a0793f645f78a0e409248d85a2e5b4c4a9a1697c3b2e3d2/coverage-7.6.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f361296ca7054f0936b02525646b2731b32c8074ba6defab524b79b2b7eeac72", size = 236454 }, + { url = "https://files.pythonhosted.org/packages/f2/81/b0dc02487447c4a56cf2eed5c57735097f77aeff582277a35f1f70713a8d/coverage-7.6.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7926d8d034e06b479797c199747dd774d5e86179f2ce44294423327a88d66ca7", size = 238320 }, + { url = "https://files.pythonhosted.org/packages/60/90/76815a76234050a87d0d1438a34820c1b857dd17353855c02bddabbedea8/coverage-7.6.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0bbae11c138585c89fb4e991faefb174a80112e1a7557d507aaa07675c62e66b", size = 237250 }, + { url = "https://files.pythonhosted.org/packages/f6/bd/760a599c08c882d97382855264586bba2604901029c3f6bec5710477ae81/coverage-7.6.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fcad7d5d2bbfeae1026b395036a8aa5abf67e8038ae7e6a25c7d0f88b10a8e6a", size = 235880 }, + { url = "https://files.pythonhosted.org/packages/83/de/41c3b90a779e473ae1ca325542aa5fa5464b7d2061288e9c22ba5f1deaa3/coverage-7.6.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f01e53575f27097d75d42de33b1b289c74b16891ce576d767ad8c48d17aeb5e0", size = 236653 }, + { url = "https://files.pythonhosted.org/packages/f4/90/61fe2721b9a9d9446e6c3ca33b6569e81d2a9a795ddfe786a66bf54035b7/coverage-7.6.2-cp311-cp311-win32.whl", hash = "sha256:7781f4f70c9b0b39e1b129b10c7d43a4e0c91f90c60435e6da8288efc2b73438", size = 209251 }, + { url = "https://files.pythonhosted.org/packages/96/87/d586f2b12b98288fc874d366cd8d5601f5a374cb75853647a3e4d02e4eb0/coverage-7.6.2-cp311-cp311-win_amd64.whl", hash = "sha256:9bcd51eeca35a80e76dc5794a9dd7cb04b97f0e8af620d54711793bfc1fbba4b", size = 210083 }, + { url = "https://files.pythonhosted.org/packages/3f/ac/1cca5ed5cf512a71cdd6e3afb75a5ef196f7ef9772be9192dadaaa5cfc1c/coverage-7.6.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ebc94fadbd4a3f4215993326a6a00e47d79889391f5659bf310f55fe5d9f581c", size = 206856 }, + { url = "https://files.pythonhosted.org/packages/e4/58/030354d250f107a95e7aca24c7fd238709a3c7df3083cb206368798e637a/coverage-7.6.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9681516288e3dcf0aa7c26231178cc0be6cac9705cac06709f2353c5b406cfea", size = 207098 }, + { url = "https://files.pythonhosted.org/packages/03/df/5f2cd6048d44a54bb5f58f8ece4efbc5b686ed49f8bd8dbf41eb2a6a687f/coverage-7.6.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d9c5d13927d77af4fbe453953810db766f75401e764727e73a6ee4f82527b3e", size = 240109 }, + { url = "https://files.pythonhosted.org/packages/d3/18/7c53887643d921faa95529643b1b33e60ebba30ab835c8b5abd4e54d946b/coverage-7.6.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b92f9ca04b3e719d69b02dc4a69debb795af84cb7afd09c5eb5d54b4a1ae2191", size = 237141 }, + { url = "https://files.pythonhosted.org/packages/d2/79/339bdf597d128374e6150c089b37436ba694585d769cabf6d5abd73a1365/coverage-7.6.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ff2ef83d6d0b527b5c9dad73819b24a2f76fdddcfd6c4e7a4d7e73ecb0656b4", size = 239210 }, + { url = "https://files.pythonhosted.org/packages/a9/62/7310c6de2bcb8a42f91094d41f0d4793ccda5a54621be3db76a156556cf2/coverage-7.6.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:47ccb6e99a3031ffbbd6e7cc041e70770b4fe405370c66a54dbf26a500ded80b", size = 238698 }, + { url = "https://files.pythonhosted.org/packages/f2/cb/ccb23c084d7f581f770dc7ed547dc5b50763334ad6ce26087a9ad0b5b26d/coverage-7.6.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a867d26f06bcd047ef716175b2696b315cb7571ccb951006d61ca80bbc356e9e", size = 237000 }, + { url = "https://files.pythonhosted.org/packages/e7/ab/58de9e2f94e4dc91b84d6e2705aa1e9d5447a2669fe113b4bbce6d2224a1/coverage-7.6.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cdfcf2e914e2ba653101157458afd0ad92a16731eeba9a611b5cbb3e7124e74b", size = 238666 }, + { url = "https://files.pythonhosted.org/packages/6c/dc/8be87b9ed5dbd4892b603f41088b41982768e928734e5bdce67d2ddd460a/coverage-7.6.2-cp312-cp312-win32.whl", hash = "sha256:f9035695dadfb397bee9eeaf1dc7fbeda483bf7664a7397a629846800ce6e276", size = 209489 }, + { url = "https://files.pythonhosted.org/packages/64/3a/3f44e55273a58bfb39b87ad76541bbb81d14de916b034fdb39971cc99ffe/coverage-7.6.2-cp312-cp312-win_amd64.whl", hash = "sha256:5ed69befa9a9fc796fe015a7040c9398722d6b97df73a6b608e9e275fa0932b0", size = 210270 }, + { url = "https://files.pythonhosted.org/packages/ae/99/c9676a75b57438a19c5174dfcf39798b42728ad56650497286379dc0c2c3/coverage-7.6.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4eea60c79d36a8f39475b1af887663bc3ae4f31289cd216f514ce18d5938df40", size = 206888 }, + { url = "https://files.pythonhosted.org/packages/e0/de/820ecb42e892049c5f384430e98b35b899da3451dd0cdb2f867baf26abfa/coverage-7.6.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa68a6cdbe1bc6793a9dbfc38302c11599bbe1837392ae9b1d238b9ef3dafcf1", size = 207142 }, + { url = "https://files.pythonhosted.org/packages/dd/59/81fc7ad855d65eeb68fe9e7809cbb339946adb07be7ac32d3fc24dc17bd7/coverage-7.6.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ec528ae69f0a139690fad6deac8a7d33629fa61ccce693fdd07ddf7e9931fba", size = 239658 }, + { url = "https://files.pythonhosted.org/packages/cd/a7/865de3eb9e78ffbf7afd92f86d2580b18edfb6f0481bd3c39b205e05a762/coverage-7.6.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed5ac02126f74d190fa2cc14a9eb2a5d9837d5863920fa472b02eb1595cdc925", size = 236802 }, + { url = "https://files.pythonhosted.org/packages/36/94/3b8f3abf88b7c451f97fd14c98f536bcee364e74250d928d57cc97c38ddd/coverage-7.6.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21c0ea0d4db8a36b275cb6fb2437a3715697a4ba3cb7b918d3525cc75f726304", size = 238793 }, + { url = "https://files.pythonhosted.org/packages/d5/4b/57f95e41a10525002f524f3dbd577a3a9871d67998f8a8eb192fe697dc7b/coverage-7.6.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:35a51598f29b2a19e26d0908bd196f771a9b1c5d9a07bf20be0adf28f1ad4f77", size = 238455 }, + { url = "https://files.pythonhosted.org/packages/99/c9/9fbe5b841628e1d9030c8044844afef4f4735586289eb9237eeb5b97f0d7/coverage-7.6.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c9192925acc33e146864b8cf037e2ed32a91fdf7644ae875f5d46cd2ef086a5f", size = 236538 }, + { url = "https://files.pythonhosted.org/packages/43/0d/2200a0d447e30de94d48e4851c04d8dce37340815e7eda27457a7043c037/coverage-7.6.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf4eeecc9e10f5403ec06138978235af79c9a79af494eb6b1d60a50b49ed2869", size = 238383 }, + { url = "https://files.pythonhosted.org/packages/ec/8a/106c66faafb4a87002b698769d6de3c4db0b6c29a7aeb72de13b893c333e/coverage-7.6.2-cp313-cp313-win32.whl", hash = "sha256:e4ee15b267d2dad3e8759ca441ad450c334f3733304c55210c2a44516e8d5530", size = 209551 }, + { url = "https://files.pythonhosted.org/packages/c4/f5/1b39e2faaf5b9cc7eed568c444df5991ce7ff7138e2e735a6801be1bdadb/coverage-7.6.2-cp313-cp313-win_amd64.whl", hash = "sha256:c71965d1ced48bf97aab79fad56df82c566b4c498ffc09c2094605727c4b7e36", size = 210282 }, + { url = "https://files.pythonhosted.org/packages/79/a3/8dd4e6c09f5286094cd6c7edb115b3fbf06ad8304d45431722a4e3bc2508/coverage-7.6.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7571e8bbecc6ac066256f9de40365ff833553e2e0c0c004f4482facb131820ef", size = 207629 }, + { url = "https://files.pythonhosted.org/packages/8e/db/a9aa7009bbdc570a235e1ac781c0a83aa323cac6db8f8f13c2127b110978/coverage-7.6.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:078a87519057dacb5d77e333f740708ec2a8f768655f1db07f8dfd28d7a005f0", size = 207902 }, + { url = "https://files.pythonhosted.org/packages/54/08/d0962be62d4335599ca2ff3a48bb68c9bfb80df74e28ca689ff5f392087b/coverage-7.6.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e5e92e3e84a8718d2de36cd8387459cba9a4508337b8c5f450ce42b87a9e760", size = 250617 }, + { url = "https://files.pythonhosted.org/packages/a5/a2/158570aff1dd88b661a6c11281cbb190e8696e77798b4b2e47c74bfb2f39/coverage-7.6.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ebabdf1c76593a09ee18c1a06cd3022919861365219ea3aca0247ededf6facd6", size = 246334 }, + { url = "https://files.pythonhosted.org/packages/aa/fe/b00428cca325b6585ca77422e4f64d7d86a225b14664b98682ea501efb57/coverage-7.6.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12179eb0575b8900912711688e45474f04ab3934aaa7b624dea7b3c511ecc90f", size = 248692 }, + { url = "https://files.pythonhosted.org/packages/30/21/0a15fefc13039450bc45e7159f3add92489f004555eb7dab9c7ad4365dd0/coverage-7.6.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:39d3b964abfe1519b9d313ab28abf1d02faea26cd14b27f5283849bf59479ff5", size = 248188 }, + { url = "https://files.pythonhosted.org/packages/de/b8/5c093526046a8450a7a3d62ad09517cf38e638f6b3ee9433dd6a73360501/coverage-7.6.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:84c4315577f7cd511d6250ffd0f695c825efe729f4205c0340f7004eda51191f", size = 246072 }, + { url = "https://files.pythonhosted.org/packages/1e/8b/542b607d2cff56e5a90a6948f5a9040b693761d2be2d3c3bf88957b02361/coverage-7.6.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ff797320dcbff57caa6b2301c3913784a010e13b1f6cf4ab3f563f3c5e7919db", size = 247354 }, + { url = "https://files.pythonhosted.org/packages/95/82/2e9111aa5e59f42b332d387f64e3205c2263518d1e660154d0c9fc54390e/coverage-7.6.2-cp313-cp313t-win32.whl", hash = "sha256:2b636a301e53964550e2f3094484fa5a96e699db318d65398cfba438c5c92171", size = 210194 }, + { url = "https://files.pythonhosted.org/packages/9d/46/aabe4305cfc57cab4865f788ceceef746c422469720c32ed7a5b44e20f5e/coverage-7.6.2-cp313-cp313t-win_amd64.whl", hash = "sha256:d03a060ac1a08e10589c27d509bbdb35b65f2d7f3f8d81cf2fa199877c7bc58a", size = 211346 }, + { url = "https://files.pythonhosted.org/packages/6a/a9/85d14426f2449252f302f12c1c2a957a0a7ae7f35317ca3eaa365e1d6453/coverage-7.6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c37faddc8acd826cfc5e2392531aba734b229741d3daec7f4c777a8f0d4993e5", size = 206555 }, + { url = "https://files.pythonhosted.org/packages/71/ff/bc4d5697a55edf1ff077c47df5637ff4518ba2760ada82c142aca79ea3fe/coverage-7.6.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab31fdd643f162c467cfe6a86e9cb5f1965b632e5e65c072d90854ff486d02cf", size = 206990 }, + { url = "https://files.pythonhosted.org/packages/34/65/1301721d09f5b58da9decfd62eb42eaef07fdb854dae904c3482e59cc309/coverage-7.6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97df87e1a20deb75ac7d920c812e9326096aa00a9a4b6d07679b4f1f14b06c90", size = 235022 }, + { url = "https://files.pythonhosted.org/packages/9f/ec/7a2f361485226e6934a8f5d1f6eef7e8b7faf228fb6107476fa584700a32/coverage-7.6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:343056c5e0737487a5291f5691f4dfeb25b3e3c8699b4d36b92bb0e586219d14", size = 232943 }, + { url = "https://files.pythonhosted.org/packages/2d/60/b23e61a372bef93c9d13d87efa2ea3a870130be498e5b81740616b6e6200/coverage-7.6.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad4ef1c56b47b6b9024b939d503ab487231df1f722065a48f4fc61832130b90e", size = 234074 }, + { url = "https://files.pythonhosted.org/packages/89/ec/4a56d9b310b2413987682ae3a858e30ea11d6f6d05366ecab4d73385fbef/coverage-7.6.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fca4a92c8a7a73dee6946471bce6d1443d94155694b893b79e19ca2a540d86e", size = 233226 }, + { url = "https://files.pythonhosted.org/packages/8c/77/31ecc00c525dea216d59090b807e9d1268a07d289f9dbe0cfc6795e33b68/coverage-7.6.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:69f251804e052fc46d29d0e7348cdc5fcbfc4861dc4a1ebedef7e78d241ad39e", size = 231706 }, + { url = "https://files.pythonhosted.org/packages/7b/02/3f84bdd286a9db9b816cb5ca0adfa001575f8e496ba39da26f0ded2f0849/coverage-7.6.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e8ea055b3ea046c0f66217af65bc193bbbeca1c8661dc5fd42698db5795d2627", size = 232697 }, + { url = "https://files.pythonhosted.org/packages/7c/34/158b73026cbc2d2b3a56fbc71d955c0eea52953e49de97f820b3060f62b9/coverage-7.6.2-cp39-cp39-win32.whl", hash = "sha256:6c2ba1e0c24d8fae8f2cf0aeb2fc0a2a7f69b6d20bd8d3749fd6b36ecef5edf0", size = 209278 }, + { url = "https://files.pythonhosted.org/packages/d1/05/4326e4ea071176f0bddc30b5a3555b48fa96c45a8f6a09b6c2e4041dfcc0/coverage-7.6.2-cp39-cp39-win_amd64.whl", hash = "sha256:2186369a654a15628e9c1c9921409a6b3eda833e4b91f3ca2a7d9f77abb4987c", size = 210057 }, + { url = "https://files.pythonhosted.org/packages/9d/5c/88f15b7614ba9ed1dbb1c0bd2c9073184b96c2bead0b93199487b44d04b3/coverage-7.6.2-pp39.pp310-none-any.whl", hash = "sha256:667952739daafe9616db19fbedbdb87917eee253ac4f31d70c7587f7ab531b4e", size = 198799 }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version == '3.11'" }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321 }, +] + +[[package]] +name = "debugpy" +version = "1.8.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/b3/05c94639560cf0eaef33662ee5102d3e2a8b9e8c527c53190bf7187bacdb/debugpy-1.8.6.zip", hash = "sha256:c931a9371a86784cee25dec8d65bc2dc7a21f3f1552e3833d9ef8f919d22280a", size = 4956612 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/ce/5e093945df2da28dbd1bc14c631d71431d1aa08adc629e221c9658841f82/debugpy-1.8.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:30f467c5345d9dfdcc0afdb10e018e47f092e383447500f125b4e013236bf14b", size = 2089048 }, + { url = "https://files.pythonhosted.org/packages/d4/7a/a5fe4eaf648016a27a875403735a089ba7cc9a4cc906d37c8fdb2997b50d/debugpy-1.8.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d73d8c52614432f4215d0fe79a7e595d0dd162b5c15233762565be2f014803b", size = 3547450 }, + { url = "https://files.pythonhosted.org/packages/bf/fe/53d6d46e4a1cb5fb1a979695a9a26c8a04aed6d6ce4ba808a6d42341beba/debugpy-1.8.6-cp310-cp310-win32.whl", hash = "sha256:e3e182cd98eac20ee23a00653503315085b29ab44ed66269482349d307b08df9", size = 5151732 }, + { url = "https://files.pythonhosted.org/packages/ce/68/127cfc6012fbeef126eab1e168ad788ee9832b8b0d572743e5c6fa03ea83/debugpy-1.8.6-cp310-cp310-win_amd64.whl", hash = "sha256:e3a82da039cfe717b6fb1886cbbe5c4a3f15d7df4765af857f4307585121c2dd", size = 5183983 }, + { url = "https://files.pythonhosted.org/packages/9f/cc/3158aa2c96c677e324981230dfd33087ef4bfb5afb1d9cd40b7a1b35edb2/debugpy-1.8.6-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:67479a94cf5fd2c2d88f9615e087fcb4fec169ec780464a3f2ba4a9a2bb79955", size = 2203403 }, + { url = "https://files.pythonhosted.org/packages/d5/9f/5691af62c556392ee45ed9b5c3fde4aaa7cb3b519cc8bea92fc27eab31fc/debugpy-1.8.6-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fb8653f6cbf1dd0a305ac1aa66ec246002145074ea57933978346ea5afdf70b", size = 3120088 }, + { url = "https://files.pythonhosted.org/packages/5e/3e/e32b36f9a391af4f8ff6b9c068ee822b5e4aa2d9cf4dc0937696e9249fa6/debugpy-1.8.6-cp311-cp311-win32.whl", hash = "sha256:cdaf0b9691879da2d13fa39b61c01887c34558d1ff6e5c30e2eb698f5384cd43", size = 5077329 }, + { url = "https://files.pythonhosted.org/packages/9d/de/ddad801b7fdbe2f97c744b44bb61169c4e0ab48a90f881c8f43b463f206b/debugpy-1.8.6-cp311-cp311-win_amd64.whl", hash = "sha256:43996632bee7435583952155c06881074b9a742a86cee74e701d87ca532fe833", size = 5101373 }, + { url = "https://files.pythonhosted.org/packages/b8/9e/882dae43f281fc4742fd9e5d2e0f5dae77f38d4f345e78bf1ed5e1f6202e/debugpy-1.8.6-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:db891b141fc6ee4b5fc6d1cc8035ec329cabc64bdd2ae672b4550c87d4ecb128", size = 2526807 }, + { url = "https://files.pythonhosted.org/packages/77/cf/6c0497f4b092cb4a408dda5ab84750032e5535f994d21eb812086d62094d/debugpy-1.8.6-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:567419081ff67da766c898ccf21e79f1adad0e321381b0dfc7a9c8f7a9347972", size = 4162582 }, + { url = "https://files.pythonhosted.org/packages/8e/66/e9c0aef0a5118aeaa6dfccb6d4f388602271cfb37c689da5e7b6168075d2/debugpy-1.8.6-cp312-cp312-win32.whl", hash = "sha256:c9834dfd701a1f6bf0f7f0b8b1573970ae99ebbeee68314116e0ccc5c78eea3c", size = 5193541 }, + { url = "https://files.pythonhosted.org/packages/c2/97/2196c4132c29f7cd8e574bb05a4b03ed35f94e3fcd1f56e72ea9f10732f4/debugpy-1.8.6-cp312-cp312-win_amd64.whl", hash = "sha256:e4ce0570aa4aca87137890d23b86faeadf184924ad892d20c54237bcaab75d8f", size = 5233374 }, + { url = "https://files.pythonhosted.org/packages/e4/61/38fa2e907aae3a293e887b04045e4d30f931aafc462b207f4cb846e78c13/debugpy-1.8.6-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:c1cef65cffbc96e7b392d9178dbfd524ab0750da6c0023c027ddcac968fd1caa", size = 2090326 }, + { url = "https://files.pythonhosted.org/packages/39/b0/9790509ffeee155038f9707b74d031ed90a17552fe6a63e9069c9c42e0d9/debugpy-1.8.6-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1e60bd06bb3cc5c0e957df748d1fab501e01416c43a7bdc756d2a992ea1b881", size = 3544122 }, + { url = "https://files.pythonhosted.org/packages/7a/a1/d95a015eadf79997cdd2028a5fe3d1f37fbe2548b51470517d3d425960dc/debugpy-1.8.6-cp39-cp39-win32.whl", hash = "sha256:f7158252803d0752ed5398d291dee4c553bb12d14547c0e1843ab74ee9c31123", size = 5152549 }, + { url = "https://files.pythonhosted.org/packages/81/6a/32e2c9e980924f3c4b1b644a5c3d949d05fa7b445673ecf3e3244c883669/debugpy-1.8.6-cp39-cp39-win_amd64.whl", hash = "sha256:3358aa619a073b620cd0d51d8a6176590af24abcc3fe2e479929a154bf591b51", size = 5185080 }, + { url = "https://files.pythonhosted.org/packages/05/ce/785925e87ce735cc3da7fb2bd66d8ca83173d8a0b60ce35a59a60b8d636f/debugpy-1.8.6-py2.py3-none-any.whl", hash = "sha256:b48892df4d810eff21d3ef37274f4c60d32cdcafc462ad5647239036b0f0649f", size = 5209208 }, +] + +[[package]] +name = "decorator" +version = "5.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/0c/8d907af351aa16b42caae42f9d6aa37b900c67308052d10fdce809f8d952/decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330", size = 35016 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/50/83c593b07763e1161326b3b8c6686f0f4b0f24d5526546bee538c89837d6/decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186", size = 9073 }, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604 }, +] + +[[package]] +name = "distlib" +version = "0.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/91/e2df406fb4efacdf46871c25cde65d3c6ee5e173b7e5a4547a47bae91920/distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64", size = 609931 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/41/9307e4f5f9976bc8b7fea0b66367734e8faf3ec84bc0d412d8cfabbb66cd/distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784", size = 468850 }, +] + +[[package]] +name = "docutils" +version = "0.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408 }, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, +] + +[[package]] +name = "executing" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/e3/7d45f492c2c4a0e8e0fad57d081a7c8a0286cdd86372b070cca1ec0caa1e/executing-2.1.0.tar.gz", hash = "sha256:8ea27ddd260da8150fa5a708269c4a10e76161e2496ec3e587da9e3c0fe4b9ab", size = 977485 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/fd/afcd0496feca3276f509df3dbd5dae726fcc756f1a08d9e25abe1733f962/executing-2.1.0-py2.py3-none-any.whl", hash = "sha256:8d63781349375b5ebccc3142f4b30350c0cd9c79f921cde38be2be4637e98eaf", size = 25805 }, +] + +[[package]] +name = "fastjsonschema" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/3f/3ad5e7be13b4b8b55f4477141885ab2364f65d5f6ad5f7a9daffd634d066/fastjsonschema-2.20.0.tar.gz", hash = "sha256:3d48fc5300ee96f5d116f10fe6f28d938e6008f59a6a025c2649475b87f76a23", size = 373056 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/ca/086311cdfc017ec964b2436fe0c98c1f4efcb7e4c328956a22456e497655/fastjsonschema-2.20.0-py3-none-any.whl", hash = "sha256:5875f0b0fa7a0043a91e93a9b8f793bcbbba9691e7fd83dca95c28ba26d21f0a", size = 23543 }, +] + +[[package]] +name = "filelock" +version = "3.16.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/db/3ef5bb276dae18d6ec2124224403d1d67bccdbefc17af4cc8f553e341ab1/filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435", size = 18037 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163 }, +] + +[[package]] +name = "flake8" +version = "7.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mccabe" }, + { name = "pycodestyle" }, + { name = "pyflakes" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/37/72/e8d66150c4fcace3c0a450466aa3480506ba2cae7b61e100a2613afc3907/flake8-7.1.1.tar.gz", hash = "sha256:049d058491e228e03e67b390f311bbf88fce2dbaa8fa673e7aea87b7198b8d38", size = 48054 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/42/65004373ac4617464f35ed15931b30d764f53cdd30cc78d5aea349c8c050/flake8-7.1.1-py2.py3-none-any.whl", hash = "sha256:597477df7860daa5aa0fdd84bf5208a043ab96b8e96ab708770ae0364dd03213", size = 57731 }, +] + +[[package]] +name = "fonttools" +version = "4.54.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/1d/70b58e342e129f9c0ce030029fb4b2b0670084bbbfe1121d008f6a1e361c/fonttools-4.54.1.tar.gz", hash = "sha256:957f669d4922f92c171ba01bef7f29410668db09f6c02111e22b2bce446f3285", size = 3463867 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/f9/285c9a2d0e86b9bf2babfe19bec00502361fda56cea144d6a269ab9a32e6/fonttools-4.54.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7ed7ee041ff7b34cc62f07545e55e1468808691dddfd315d51dd82a6b37ddef2", size = 2766970 }, + { url = "https://files.pythonhosted.org/packages/2f/9a/9d899e7ae55b0dd30632e6ca36c0f5fa1205b1b096ec171c9be903673058/fonttools-4.54.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41bb0b250c8132b2fcac148e2e9198e62ff06f3cc472065dff839327945c5882", size = 2254639 }, + { url = "https://files.pythonhosted.org/packages/16/6f/b99e0c347732fb003077a2cff38c26f381969b74329aa5597e344d540fe1/fonttools-4.54.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7965af9b67dd546e52afcf2e38641b5be956d68c425bef2158e95af11d229f10", size = 4574346 }, + { url = "https://files.pythonhosted.org/packages/e5/12/9a45294a7c4520cc32936edd15df1d5c24af701d2f5f51070a9a43d7664b/fonttools-4.54.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:278913a168f90d53378c20c23b80f4e599dca62fbffae4cc620c8eed476b723e", size = 4630045 }, + { url = "https://files.pythonhosted.org/packages/64/52/ba4f00eb6003e4089264cd9ce126cddec2b39c78f1ab01be9dc389a197ca/fonttools-4.54.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0e88e3018ac809b9662615072dcd6b84dca4c2d991c6d66e1970a112503bba7e", size = 4569527 }, + { url = "https://files.pythonhosted.org/packages/41/ff/85f93a14c8acf978f332508f980dcaff5ed5f0cf284371eb101a78f0b1f4/fonttools-4.54.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4aa4817f0031206e637d1e685251ac61be64d1adef111060df84fdcbc6ab6c44", size = 4741677 }, + { url = "https://files.pythonhosted.org/packages/6f/f0/06ea7d9f8b7b6d4758a50271517db04039c4c6da8fa0475d417e005624d0/fonttools-4.54.1-cp310-cp310-win32.whl", hash = "sha256:7e3b7d44e18c085fd8c16dcc6f1ad6c61b71ff463636fcb13df7b1b818bd0c02", size = 2166797 }, + { url = "https://files.pythonhosted.org/packages/71/73/545c817e34b8c34585291951722e1a5ae579380deb009576d9d244b13ab0/fonttools-4.54.1-cp310-cp310-win_amd64.whl", hash = "sha256:dd9cc95b8d6e27d01e1e1f1fae8559ef3c02c76317da650a19047f249acd519d", size = 2210552 }, + { url = "https://files.pythonhosted.org/packages/aa/2c/8b5d82fe2d9c7f260fb73121418f5e07d4e38c329ea3886a5b0e55586113/fonttools-4.54.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5419771b64248484299fa77689d4f3aeed643ea6630b2ea750eeab219588ba20", size = 2768112 }, + { url = "https://files.pythonhosted.org/packages/37/2e/f94118b92f7b6a9ec93840101b64bfdd09f295b266133857e8e852a5c35c/fonttools-4.54.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:301540e89cf4ce89d462eb23a89464fef50915255ece765d10eee8b2bf9d75b2", size = 2254739 }, + { url = "https://files.pythonhosted.org/packages/45/4b/8a32f56a13e78256192f77d6b65583c43538c7955f5420887bb574b91ddf/fonttools-4.54.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76ae5091547e74e7efecc3cbf8e75200bc92daaeb88e5433c5e3e95ea8ce5aa7", size = 4879772 }, + { url = "https://files.pythonhosted.org/packages/96/13/748b7f7239893ff0796de11074b0ad8aa4c3da2d9f4d79a128b0b16147f3/fonttools-4.54.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82834962b3d7c5ca98cb56001c33cf20eb110ecf442725dc5fdf36d16ed1ab07", size = 4927686 }, + { url = "https://files.pythonhosted.org/packages/7c/82/91bc5a378b4a0593fa90ea706f68ce7e9e871c6873e0d91e134d107758db/fonttools-4.54.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d26732ae002cc3d2ecab04897bb02ae3f11f06dd7575d1df46acd2f7c012a8d8", size = 4890789 }, + { url = "https://files.pythonhosted.org/packages/ea/ca/82be5d4f8b78405cdb3f7f3f1316af5e8db93216121f19da9f684a35beee/fonttools-4.54.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:58974b4987b2a71ee08ade1e7f47f410c367cdfc5a94fabd599c88165f56213a", size = 5061351 }, + { url = "https://files.pythonhosted.org/packages/da/2f/fd6e1b01c80c473c3ac52492dcf8d26cdf5f4a89b4f30875ecfbda55e7ff/fonttools-4.54.1-cp311-cp311-win32.whl", hash = "sha256:ab774fa225238986218a463f3fe151e04d8c25d7de09df7f0f5fce27b1243dbc", size = 2166210 }, + { url = "https://files.pythonhosted.org/packages/63/f1/3a081cd047d83b5966cb0d7ef3fea929ee6eddeb94d8fbfdb2a19bd60cc7/fonttools-4.54.1-cp311-cp311-win_amd64.whl", hash = "sha256:07e005dc454eee1cc60105d6a29593459a06321c21897f769a281ff2d08939f6", size = 2211946 }, + { url = "https://files.pythonhosted.org/packages/27/b6/f9d365932dcefefdcc794985f8846471e60932070c557e0f66ed195fccec/fonttools-4.54.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:54471032f7cb5fca694b5f1a0aaeba4af6e10ae989df408e0216f7fd6cdc405d", size = 2761873 }, + { url = "https://files.pythonhosted.org/packages/67/9d/cfbfe36e5061a8f68b154454ba2304eb01f40d4ba9b63e41d9058909baed/fonttools-4.54.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fa92cb248e573daab8d032919623cc309c005086d743afb014c836636166f08", size = 2251828 }, + { url = "https://files.pythonhosted.org/packages/90/41/5573e074739efd9227dd23647724f01f6f07ad062fe09d02e91c5549dcf7/fonttools-4.54.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a911591200114969befa7f2cb74ac148bce5a91df5645443371aba6d222e263", size = 4792544 }, + { url = "https://files.pythonhosted.org/packages/08/07/aa85cc62abcc940b25d14b542cf585eebf4830032a7f6a1395d696bb3231/fonttools-4.54.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93d458c8a6a354dc8b48fc78d66d2a8a90b941f7fec30e94c7ad9982b1fa6bab", size = 4875892 }, + { url = "https://files.pythonhosted.org/packages/47/23/c5726c2615446c498a976bed21c35a242a97eee39930a2655d616ca885cc/fonttools-4.54.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5eb2474a7c5be8a5331146758debb2669bf5635c021aee00fd7c353558fc659d", size = 4769822 }, + { url = "https://files.pythonhosted.org/packages/8f/7b/87f7f7d35e0732ac67422dfa6f05e2b568fb6ca2dcd7f3e4f500293cfd75/fonttools-4.54.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c9c563351ddc230725c4bdf7d9e1e92cbe6ae8553942bd1fb2b2ff0884e8b714", size = 5029455 }, + { url = "https://files.pythonhosted.org/packages/e0/09/241aa498587889576838aa73c78d22b70ce06970807a5475d372baa7ccb7/fonttools-4.54.1-cp312-cp312-win32.whl", hash = "sha256:fdb062893fd6d47b527d39346e0c5578b7957dcea6d6a3b6794569370013d9ac", size = 2154411 }, + { url = "https://files.pythonhosted.org/packages/b9/0a/a57caaff3bc880779317cb157e5b49dc47fad54effe027016abd355b0651/fonttools-4.54.1-cp312-cp312-win_amd64.whl", hash = "sha256:e4564cf40cebcb53f3dc825e85910bf54835e8a8b6880d59e5159f0f325e637e", size = 2200412 }, + { url = "https://files.pythonhosted.org/packages/05/3d/cc515cae84a11d696f2cb7c139a90997b15f02e2e97ec09a5d79302cbcd7/fonttools-4.54.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6e37561751b017cf5c40fce0d90fd9e8274716de327ec4ffb0df957160be3bff", size = 2749174 }, + { url = "https://files.pythonhosted.org/packages/03/03/05d4b22d1a674d066380657f60bbc0eda2d206446912e676d1a33a206878/fonttools-4.54.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:357cacb988a18aace66e5e55fe1247f2ee706e01debc4b1a20d77400354cddeb", size = 2246267 }, + { url = "https://files.pythonhosted.org/packages/52/c3/bb6086adb675e8b0963a7dbb7769e7118c95b687dd318cd660aefd4b4c8c/fonttools-4.54.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8e953cc0bddc2beaf3a3c3b5dd9ab7554677da72dfaf46951e193c9653e515a", size = 4855090 }, + { url = "https://files.pythonhosted.org/packages/80/a1/d7192b6a104e3f9ea8e5b1c3463a6240399f0fa826a782eff636cbe0495a/fonttools-4.54.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:58d29b9a294573d8319f16f2f79e42428ba9b6480442fa1836e4eb89c4d9d61c", size = 5005449 }, + { url = "https://files.pythonhosted.org/packages/5a/6c/ecfd5c6cd8c9006e85b128d073af26bb263e8aa47506374cb14b25bcf65f/fonttools-4.54.1-cp313-cp313-win32.whl", hash = "sha256:9ef1b167e22709b46bf8168368b7b5d3efeaaa746c6d39661c1b4405b6352e58", size = 2152496 }, + { url = "https://files.pythonhosted.org/packages/63/da/f7a1d837de419e3d4cccbd0dbf53c7399f610f65ceb9bcbf2480f3ae7950/fonttools-4.54.1-cp313-cp313-win_amd64.whl", hash = "sha256:262705b1663f18c04250bd1242b0515d3bbae177bee7752be67c979b7d47f43d", size = 2197257 }, + { url = "https://files.pythonhosted.org/packages/99/14/298292fce6f163f04ec31a79bb6627d9ca85c9874d402f33415ebae313f7/fonttools-4.54.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f5b8a096e649768c2f4233f947cf9737f8dbf8728b90e2771e2497c6e3d21d13", size = 2769825 }, + { url = "https://files.pythonhosted.org/packages/86/dc/acf23baaefac9893d99f5a7dc3396ecfbc4747597075009c8b6452e4b2a6/fonttools-4.54.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4e10d2e0a12e18f4e2dd031e1bf7c3d7017be5c8dbe524d07706179f355c5dac", size = 2256120 }, + { url = "https://files.pythonhosted.org/packages/28/bc/9c57b3f19a4178318e9f1ee4cb6b5be91a07ef11a2a26716ceed3bebc2cc/fonttools-4.54.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31c32d7d4b0958600eac75eaf524b7b7cb68d3a8c196635252b7a2c30d80e986", size = 4578956 }, + { url = "https://files.pythonhosted.org/packages/8c/e7/24870ef7d4014b7904a3b9911199ffe04532d1fb73cf70856471f9f8b252/fonttools-4.54.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c39287f5c8f4a0c5a55daf9eaf9ccd223ea59eed3f6d467133cc727d7b943a55", size = 4637480 }, + { url = "https://files.pythonhosted.org/packages/5d/41/c72f79b24969d04c14bd543faaa3a126d71114eb0e896227e1692a39bec2/fonttools-4.54.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a7a310c6e0471602fe3bf8efaf193d396ea561486aeaa7adc1f132e02d30c4b9", size = 4572104 }, + { url = "https://files.pythonhosted.org/packages/8b/47/b897833f6d659147498517fef95b8978a2125da11392983e0f3acf4671a9/fonttools-4.54.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d3b659d1029946f4ff9b6183984578041b520ce0f8fb7078bb37ec7445806b33", size = 4744962 }, + { url = "https://files.pythonhosted.org/packages/40/ea/61664ee6a587fe59dd67224d3939a3e253e012bbf19b905b934a8c306cf9/fonttools-4.54.1-cp39-cp39-win32.whl", hash = "sha256:e96bc94c8cda58f577277d4a71f51c8e2129b8b36fd05adece6320dd3d57de8a", size = 2167431 }, + { url = "https://files.pythonhosted.org/packages/3c/87/566f79796150029bfce1c93c10adb1c46017fac2caac3996a0a6f73c96e1/fonttools-4.54.1-cp39-cp39-win_amd64.whl", hash = "sha256:e8a4b261c1ef91e7188a30571be6ad98d1c6d9fa2427244c545e2fa0a2494dd7", size = 2211153 }, + { url = "https://files.pythonhosted.org/packages/57/5e/de2e6e51cb6894f2f2bc2641f6c845561361b622e96df3cca04df77222c9/fonttools-4.54.1-py3-none-any.whl", hash = "sha256:37cddd62d83dc4f72f7c3f3c2bcf2697e89a30efb152079896544a93907733bd", size = 1096920 }, +] + +[[package]] +name = "identify" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/bb/25024dbcc93516c492b75919e76f389bac754a3e4248682fba32b250c880/identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98", size = 99097 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/0c/4ef72754c050979fdcc06c744715ae70ea37e734816bb6514f79df77a42f/identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0", size = 98972 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "imagesize" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769 }, +] + +[[package]] +name = "importlib-metadata" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514 }, +] + +[[package]] +name = "importlib-resources" +version = "6.4.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/be/f3e8c6081b684f176b761e6a2fef02a0be939740ed6f54109a2951d806f3/importlib_resources-6.4.5.tar.gz", hash = "sha256:980862a1d16c9e147a59603677fa2aa5fd82b87f223b6cb870695bcfce830065", size = 43372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/6a/4604f9ae2fa62ef47b9de2fa5ad599589d28c9fd1d335f32759813dfa91e/importlib_resources-6.4.5-py3-none-any.whl", hash = "sha256:ac29d5f956f01d5e4bb63102a5a19957f1b9175e45649977264a1416783bb717", size = 36115 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "ipykernel" +version = "6.29.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "appnope", marker = "platform_system == 'Darwin'" }, + { name = "comm" }, + { name = "debugpy" }, + { name = "ipython" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "matplotlib-inline" }, + { name = "nest-asyncio" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/5c/67594cb0c7055dc50814b21731c22a601101ea3b1b50a9a1b090e11f5d0f/ipykernel-6.29.5.tar.gz", hash = "sha256:f093a22c4a40f8828f8e330a9c297cb93dcab13bd9678ded6de8e5cf81c56215", size = 163367 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/5c/368ae6c01c7628438358e6d337c19b05425727fbb221d2a3c4303c372f42/ipykernel-6.29.5-py3-none-any.whl", hash = "sha256:afdb66ba5aa354b09b91379bac28ae4afebbb30e8b39510c9690afb7a10421b5", size = 117173 }, +] + +[[package]] +name = "ipython" +version = "8.18.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "decorator" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "jedi" }, + { name = "matplotlib-inline" }, + { name = "pexpect", marker = "sys_platform != 'win32'" }, + { name = "prompt-toolkit" }, + { name = "pygments" }, + { name = "stack-data" }, + { name = "traitlets" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/b9/3ba6c45a6df813c09a48bac313c22ff83efa26cbb55011218d925a46e2ad/ipython-8.18.1.tar.gz", hash = "sha256:ca6f079bb33457c66e233e4580ebfc4128855b4cf6370dddd73842a9563e8a27", size = 5486330 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/6b/d9fdcdef2eb6a23f391251fde8781c38d42acd82abe84d054cb74f7863b0/ipython-8.18.1-py3-none-any.whl", hash = "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397", size = 808161 }, +] + +[[package]] +name = "jedi" +version = "0.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parso" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/99/99b493cec4bf43176b678de30f81ed003fd6a647a301b9c927280c600f0a/jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd", size = 1227821 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/9f/bc63f0f0737ad7a60800bfd472a4836661adae21f9c2535f3957b1e54ceb/jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0", size = 1569361 }, +] + +[[package]] +name = "jinja2" +version = "3.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 }, +] + +[[package]] +name = "joblib" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/64/33/60135848598c076ce4b231e1b1895170f45fbcaeaa2c9d5e38b04db70c35/joblib-1.4.2.tar.gz", hash = "sha256:2382c5816b2636fbd20a09e0f4e9dad4736765fdfb7dca582943b9c1366b3f0e", size = 2116621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/29/df4b9b42f2be0b623cbd5e2140cafcaa2bef0759a00b7b70104dcfe2fb51/joblib-1.4.2-py3-none-any.whl", hash = "sha256:06d478d5674cbc267e7496a410ee875abd68e4340feff4490bcb7afb88060ae6", size = 301817 }, +] + +[[package]] +name = "jsonschema" +version = "4.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/2e/03362ee4034a4c917f697890ccd4aec0800ccf9ded7f511971c75451deec/jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", size = 325778 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/4a/4f9dbeb84e8850557c02365a0eee0649abe5eb1d84af92a25731c6c0f922/jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566", size = 88462 }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2024.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/db/58f950c996c793472e336ff3655b13fbcf1e3b359dcf52dcf3ed3b52c352/jsonschema_specifications-2024.10.1.tar.gz", hash = "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272", size = 15561 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/0f/8910b19ac0670a0f80ce1008e5e751c4a57e14d2c4c13a482aa6079fa9d6/jsonschema_specifications-2024.10.1-py3-none-any.whl", hash = "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf", size = 18459 }, +] + +[[package]] +name = "jupyter-client" +version = "8.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "jupyter-core" }, + { name = "python-dateutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/22/bf9f12fdaeae18019a468b68952a60fe6dbab5d67cd2a103cac7659b41ca/jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419", size = 342019 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/85/b0394e0b6fcccd2c1eeefc230978a6f8cb0c5df1e4cd3e7625735a0d7d1e/jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f", size = 106105 }, +] + +[[package]] +name = "jupyter-core" +version = "5.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "platformdirs" }, + { name = "pywin32", marker = "platform_python_implementation != 'PyPy' and sys_platform == 'win32'" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/11/b56381fa6c3f4cc5d2cf54a7dbf98ad9aa0b339ef7a601d6053538b079a7/jupyter_core-5.7.2.tar.gz", hash = "sha256:aa5f8d32bbf6b431ac830496da7392035d6f61b4f54872f15c4bd2a9c3f536d9", size = 87629 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/fb/108ecd1fe961941959ad0ee4e12ee7b8b1477247f30b1fdfd83ceaf017f0/jupyter_core-5.7.2-py3-none-any.whl", hash = "sha256:4f7315d2f6b4bcf2e3e7cb6e46772eba760ae459cd1f59d29eb57b0a01bd7409", size = 28965 }, +] + +[[package]] +name = "jupyterlab-pygments" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/51/9187be60d989df97f5f0aba133fa54e7300f17616e065d1ada7d7646b6d6/jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d", size = 512900 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780", size = 15884 }, +] + +[[package]] +name = "kiwisolver" +version = "1.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/4d/2255e1c76304cbd60b48cee302b66d1dde4468dc5b1160e4b7cb43778f2a/kiwisolver-1.4.7.tar.gz", hash = "sha256:9893ff81bd7107f7b685d3017cc6583daadb4fc26e4a888350df530e41980a60", size = 97286 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/14/fc943dd65268a96347472b4fbe5dcc2f6f55034516f80576cd0dd3a8930f/kiwisolver-1.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8a9c83f75223d5e48b0bc9cb1bf2776cf01563e00ade8775ffe13b0b6e1af3a6", size = 122440 }, + { url = "https://files.pythonhosted.org/packages/1e/46/e68fed66236b69dd02fcdb506218c05ac0e39745d696d22709498896875d/kiwisolver-1.4.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:58370b1ffbd35407444d57057b57da5d6549d2d854fa30249771775c63b5fe17", size = 65758 }, + { url = "https://files.pythonhosted.org/packages/ef/fa/65de49c85838681fc9cb05de2a68067a683717321e01ddafb5b8024286f0/kiwisolver-1.4.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aa0abdf853e09aff551db11fce173e2177d00786c688203f52c87ad7fcd91ef9", size = 64311 }, + { url = "https://files.pythonhosted.org/packages/42/9c/cc8d90f6ef550f65443bad5872ffa68f3dee36de4974768628bea7c14979/kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8d53103597a252fb3ab8b5845af04c7a26d5e7ea8122303dd7a021176a87e8b9", size = 1637109 }, + { url = "https://files.pythonhosted.org/packages/55/91/0a57ce324caf2ff5403edab71c508dd8f648094b18cfbb4c8cc0fde4a6ac/kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:88f17c5ffa8e9462fb79f62746428dd57b46eb931698e42e990ad63103f35e6c", size = 1617814 }, + { url = "https://files.pythonhosted.org/packages/12/5d/c36140313f2510e20207708adf36ae4919416d697ee0236b0ddfb6fd1050/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a9ca9c710d598fd75ee5de59d5bda2684d9db36a9f50b6125eaea3969c2599", size = 1400881 }, + { url = "https://files.pythonhosted.org/packages/56/d0/786e524f9ed648324a466ca8df86298780ef2b29c25313d9a4f16992d3cf/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f4d742cb7af1c28303a51b7a27aaee540e71bb8e24f68c736f6f2ffc82f2bf05", size = 1512972 }, + { url = "https://files.pythonhosted.org/packages/67/5a/77851f2f201e6141d63c10a0708e996a1363efaf9e1609ad0441b343763b/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e28c7fea2196bf4c2f8d46a0415c77a1c480cc0724722f23d7410ffe9842c407", size = 1444787 }, + { url = "https://files.pythonhosted.org/packages/06/5f/1f5eaab84355885e224a6fc8d73089e8713dc7e91c121f00b9a1c58a2195/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e968b84db54f9d42046cf154e02911e39c0435c9801681e3fc9ce8a3c4130278", size = 2199212 }, + { url = "https://files.pythonhosted.org/packages/b5/28/9152a3bfe976a0ae21d445415defc9d1cd8614b2910b7614b30b27a47270/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0c18ec74c0472de033e1bebb2911c3c310eef5649133dd0bedf2a169a1b269e5", size = 2346399 }, + { url = "https://files.pythonhosted.org/packages/26/f6/453d1904c52ac3b400f4d5e240ac5fec25263716723e44be65f4d7149d13/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8f0ea6da6d393d8b2e187e6a5e3fb81f5862010a40c3945e2c6d12ae45cfb2ad", size = 2308688 }, + { url = "https://files.pythonhosted.org/packages/5a/9a/d4968499441b9ae187e81745e3277a8b4d7c60840a52dc9d535a7909fac3/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:f106407dda69ae456dd1227966bf445b157ccc80ba0dff3802bb63f30b74e895", size = 2445493 }, + { url = "https://files.pythonhosted.org/packages/07/c9/032267192e7828520dacb64dfdb1d74f292765f179e467c1cba97687f17d/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:84ec80df401cfee1457063732d90022f93951944b5b58975d34ab56bb150dfb3", size = 2262191 }, + { url = "https://files.pythonhosted.org/packages/6c/ad/db0aedb638a58b2951da46ddaeecf204be8b4f5454df020d850c7fa8dca8/kiwisolver-1.4.7-cp310-cp310-win32.whl", hash = "sha256:71bb308552200fb2c195e35ef05de12f0c878c07fc91c270eb3d6e41698c3bcc", size = 46644 }, + { url = "https://files.pythonhosted.org/packages/12/ca/d0f7b7ffbb0be1e7c2258b53554efec1fd652921f10d7d85045aff93ab61/kiwisolver-1.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:44756f9fd339de0fb6ee4f8c1696cfd19b2422e0d70b4cefc1cc7f1f64045a8c", size = 55877 }, + { url = "https://files.pythonhosted.org/packages/97/6c/cfcc128672f47a3e3c0d918ecb67830600078b025bfc32d858f2e2d5c6a4/kiwisolver-1.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:78a42513018c41c2ffd262eb676442315cbfe3c44eed82385c2ed043bc63210a", size = 48347 }, + { url = "https://files.pythonhosted.org/packages/e9/44/77429fa0a58f941d6e1c58da9efe08597d2e86bf2b2cce6626834f49d07b/kiwisolver-1.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d2b0e12a42fb4e72d509fc994713d099cbb15ebf1103545e8a45f14da2dfca54", size = 122442 }, + { url = "https://files.pythonhosted.org/packages/e5/20/8c75caed8f2462d63c7fd65e16c832b8f76cda331ac9e615e914ee80bac9/kiwisolver-1.4.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2a8781ac3edc42ea4b90bc23e7d37b665d89423818e26eb6df90698aa2287c95", size = 65762 }, + { url = "https://files.pythonhosted.org/packages/f4/98/fe010f15dc7230f45bc4cf367b012d651367fd203caaa992fd1f5963560e/kiwisolver-1.4.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46707a10836894b559e04b0fd143e343945c97fd170d69a2d26d640b4e297935", size = 64319 }, + { url = "https://files.pythonhosted.org/packages/8b/1b/b5d618f4e58c0675654c1e5051bcf42c776703edb21c02b8c74135541f60/kiwisolver-1.4.7-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef97b8df011141c9b0f6caf23b29379f87dd13183c978a30a3c546d2c47314cb", size = 1334260 }, + { url = "https://files.pythonhosted.org/packages/b8/01/946852b13057a162a8c32c4c8d2e9ed79f0bb5d86569a40c0b5fb103e373/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab58c12a2cd0fc769089e6d38466c46d7f76aced0a1f54c77652446733d2d02", size = 1426589 }, + { url = "https://files.pythonhosted.org/packages/70/d1/c9f96df26b459e15cf8a965304e6e6f4eb291e0f7a9460b4ad97b047561e/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:803b8e1459341c1bb56d1c5c010406d5edec8a0713a0945851290a7930679b51", size = 1541080 }, + { url = "https://files.pythonhosted.org/packages/d3/73/2686990eb8b02d05f3de759d6a23a4ee7d491e659007dd4c075fede4b5d0/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9a9e8a507420fe35992ee9ecb302dab68550dedc0da9e2880dd88071c5fb052", size = 1470049 }, + { url = "https://files.pythonhosted.org/packages/a7/4b/2db7af3ed3af7c35f388d5f53c28e155cd402a55432d800c543dc6deb731/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18077b53dc3bb490e330669a99920c5e6a496889ae8c63b58fbc57c3d7f33a18", size = 1426376 }, + { url = "https://files.pythonhosted.org/packages/05/83/2857317d04ea46dc5d115f0df7e676997bbd968ced8e2bd6f7f19cfc8d7f/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6af936f79086a89b3680a280c47ea90b4df7047b5bdf3aa5c524bbedddb9e545", size = 2222231 }, + { url = "https://files.pythonhosted.org/packages/0d/b5/866f86f5897cd4ab6d25d22e403404766a123f138bd6a02ecb2cdde52c18/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3abc5b19d24af4b77d1598a585b8a719beb8569a71568b66f4ebe1fb0449460b", size = 2368634 }, + { url = "https://files.pythonhosted.org/packages/c1/ee/73de8385403faba55f782a41260210528fe3273d0cddcf6d51648202d6d0/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:933d4de052939d90afbe6e9d5273ae05fb836cc86c15b686edd4b3560cc0ee36", size = 2329024 }, + { url = "https://files.pythonhosted.org/packages/a1/e7/cd101d8cd2cdfaa42dc06c433df17c8303d31129c9fdd16c0ea37672af91/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:65e720d2ab2b53f1f72fb5da5fb477455905ce2c88aaa671ff0a447c2c80e8e3", size = 2468484 }, + { url = "https://files.pythonhosted.org/packages/e1/72/84f09d45a10bc57a40bb58b81b99d8f22b58b2040c912b7eb97ebf625bf2/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3bf1ed55088f214ba6427484c59553123fdd9b218a42bbc8c6496d6754b1e523", size = 2284078 }, + { url = "https://files.pythonhosted.org/packages/d2/d4/71828f32b956612dc36efd7be1788980cb1e66bfb3706e6dec9acad9b4f9/kiwisolver-1.4.7-cp311-cp311-win32.whl", hash = "sha256:4c00336b9dd5ad96d0a558fd18a8b6f711b7449acce4c157e7343ba92dd0cf3d", size = 46645 }, + { url = "https://files.pythonhosted.org/packages/a1/65/d43e9a20aabcf2e798ad1aff6c143ae3a42cf506754bcb6a7ed8259c8425/kiwisolver-1.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:929e294c1ac1e9f615c62a4e4313ca1823ba37326c164ec720a803287c4c499b", size = 56022 }, + { url = "https://files.pythonhosted.org/packages/35/b3/9f75a2e06f1b4ca00b2b192bc2b739334127d27f1d0625627ff8479302ba/kiwisolver-1.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:e33e8fbd440c917106b237ef1a2f1449dfbb9b6f6e1ce17c94cd6a1e0d438376", size = 48536 }, + { url = "https://files.pythonhosted.org/packages/97/9c/0a11c714cf8b6ef91001c8212c4ef207f772dd84540104952c45c1f0a249/kiwisolver-1.4.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:5360cc32706dab3931f738d3079652d20982511f7c0ac5711483e6eab08efff2", size = 121808 }, + { url = "https://files.pythonhosted.org/packages/f2/d8/0fe8c5f5d35878ddd135f44f2af0e4e1d379e1c7b0716f97cdcb88d4fd27/kiwisolver-1.4.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942216596dc64ddb25adb215c3c783215b23626f8d84e8eff8d6d45c3f29f75a", size = 65531 }, + { url = "https://files.pythonhosted.org/packages/80/c5/57fa58276dfdfa612241d640a64ca2f76adc6ffcebdbd135b4ef60095098/kiwisolver-1.4.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:48b571ecd8bae15702e4f22d3ff6a0f13e54d3d00cd25216d5e7f658242065ee", size = 63894 }, + { url = "https://files.pythonhosted.org/packages/8b/e9/26d3edd4c4ad1c5b891d8747a4f81b1b0aba9fb9721de6600a4adc09773b/kiwisolver-1.4.7-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad42ba922c67c5f219097b28fae965e10045ddf145d2928bfac2eb2e17673640", size = 1369296 }, + { url = "https://files.pythonhosted.org/packages/b6/67/3f4850b5e6cffb75ec40577ddf54f7b82b15269cc5097ff2e968ee32ea7d/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:612a10bdae23404a72941a0fc8fa2660c6ea1217c4ce0dbcab8a8f6543ea9e7f", size = 1461450 }, + { url = "https://files.pythonhosted.org/packages/52/be/86cbb9c9a315e98a8dc6b1d23c43cffd91d97d49318854f9c37b0e41cd68/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e838bba3a3bac0fe06d849d29772eb1afb9745a59710762e4ba3f4cb8424483", size = 1579168 }, + { url = "https://files.pythonhosted.org/packages/0f/00/65061acf64bd5fd34c1f4ae53f20b43b0a017a541f242a60b135b9d1e301/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22f499f6157236c19f4bbbd472fa55b063db77a16cd74d49afe28992dff8c258", size = 1507308 }, + { url = "https://files.pythonhosted.org/packages/21/e4/c0b6746fd2eb62fe702118b3ca0cb384ce95e1261cfada58ff693aeec08a/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693902d433cf585133699972b6d7c42a8b9f8f826ebcaf0132ff55200afc599e", size = 1464186 }, + { url = "https://files.pythonhosted.org/packages/0a/0f/529d0a9fffb4d514f2782c829b0b4b371f7f441d61aa55f1de1c614c4ef3/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4e77f2126c3e0b0d055f44513ed349038ac180371ed9b52fe96a32aa071a5107", size = 2247877 }, + { url = "https://files.pythonhosted.org/packages/d1/e1/66603ad779258843036d45adcbe1af0d1a889a07af4635f8b4ec7dccda35/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:657a05857bda581c3656bfc3b20e353c232e9193eb167766ad2dc58b56504948", size = 2404204 }, + { url = "https://files.pythonhosted.org/packages/8d/61/de5fb1ca7ad1f9ab7970e340a5b833d735df24689047de6ae71ab9d8d0e7/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4bfa75a048c056a411f9705856abfc872558e33c055d80af6a380e3658766038", size = 2352461 }, + { url = "https://files.pythonhosted.org/packages/ba/d2/0edc00a852e369827f7e05fd008275f550353f1f9bcd55db9363d779fc63/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:34ea1de54beef1c104422d210c47c7d2a4999bdecf42c7b5718fbe59a4cac383", size = 2501358 }, + { url = "https://files.pythonhosted.org/packages/84/15/adc15a483506aec6986c01fb7f237c3aec4d9ed4ac10b756e98a76835933/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:90da3b5f694b85231cf93586dad5e90e2d71b9428f9aad96952c99055582f520", size = 2314119 }, + { url = "https://files.pythonhosted.org/packages/36/08/3a5bb2c53c89660863a5aa1ee236912269f2af8762af04a2e11df851d7b2/kiwisolver-1.4.7-cp312-cp312-win32.whl", hash = "sha256:18e0cca3e008e17fe9b164b55735a325140a5a35faad8de92dd80265cd5eb80b", size = 46367 }, + { url = "https://files.pythonhosted.org/packages/19/93/c05f0a6d825c643779fc3c70876bff1ac221f0e31e6f701f0e9578690d70/kiwisolver-1.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:58cb20602b18f86f83a5c87d3ee1c766a79c0d452f8def86d925e6c60fbf7bfb", size = 55884 }, + { url = "https://files.pythonhosted.org/packages/d2/f9/3828d8f21b6de4279f0667fb50a9f5215e6fe57d5ec0d61905914f5b6099/kiwisolver-1.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:f5a8b53bdc0b3961f8b6125e198617c40aeed638b387913bf1ce78afb1b0be2a", size = 48528 }, + { url = "https://files.pythonhosted.org/packages/c4/06/7da99b04259b0f18b557a4effd1b9c901a747f7fdd84cf834ccf520cb0b2/kiwisolver-1.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2e6039dcbe79a8e0f044f1c39db1986a1b8071051efba3ee4d74f5b365f5226e", size = 121913 }, + { url = "https://files.pythonhosted.org/packages/97/f5/b8a370d1aa593c17882af0a6f6755aaecd643640c0ed72dcfd2eafc388b9/kiwisolver-1.4.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a1ecf0ac1c518487d9d23b1cd7139a6a65bc460cd101ab01f1be82ecf09794b6", size = 65627 }, + { url = "https://files.pythonhosted.org/packages/2a/fc/6c0374f7503522539e2d4d1b497f5ebad3f8ed07ab51aed2af988dd0fb65/kiwisolver-1.4.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ab9ccab2b5bd5702ab0803676a580fffa2aa178c2badc5557a84cc943fcf750", size = 63888 }, + { url = "https://files.pythonhosted.org/packages/bf/3e/0b7172793d0f41cae5c923492da89a2ffcd1adf764c16159ca047463ebd3/kiwisolver-1.4.7-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f816dd2277f8d63d79f9c8473a79fe54047bc0467754962840782c575522224d", size = 1369145 }, + { url = "https://files.pythonhosted.org/packages/77/92/47d050d6f6aced2d634258123f2688fbfef8ded3c5baf2c79d94d91f1f58/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf8bcc23ceb5a1b624572a1623b9f79d2c3b337c8c455405ef231933a10da379", size = 1461448 }, + { url = "https://files.pythonhosted.org/packages/9c/1b/8f80b18e20b3b294546a1adb41701e79ae21915f4175f311a90d042301cf/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dea0bf229319828467d7fca8c7c189780aa9ff679c94539eed7532ebe33ed37c", size = 1578750 }, + { url = "https://files.pythonhosted.org/packages/a4/fe/fe8e72f3be0a844f257cadd72689c0848c6d5c51bc1d60429e2d14ad776e/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c06a4c7cf15ec739ce0e5971b26c93638730090add60e183530d70848ebdd34", size = 1507175 }, + { url = "https://files.pythonhosted.org/packages/39/fa/cdc0b6105d90eadc3bee525fecc9179e2b41e1ce0293caaf49cb631a6aaf/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:913983ad2deb14e66d83c28b632fd35ba2b825031f2fa4ca29675e665dfecbe1", size = 1463963 }, + { url = "https://files.pythonhosted.org/packages/6e/5c/0c03c4e542720c6177d4f408e56d1c8315899db72d46261a4e15b8b33a41/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5337ec7809bcd0f424c6b705ecf97941c46279cf5ed92311782c7c9c2026f07f", size = 2248220 }, + { url = "https://files.pythonhosted.org/packages/3d/ee/55ef86d5a574f4e767df7da3a3a7ff4954c996e12d4fbe9c408170cd7dcc/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4c26ed10c4f6fa6ddb329a5120ba3b6db349ca192ae211e882970bfc9d91420b", size = 2404463 }, + { url = "https://files.pythonhosted.org/packages/0f/6d/73ad36170b4bff4825dc588acf4f3e6319cb97cd1fb3eb04d9faa6b6f212/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c619b101e6de2222c1fcb0531e1b17bbffbe54294bfba43ea0d411d428618c27", size = 2352842 }, + { url = "https://files.pythonhosted.org/packages/0b/16/fa531ff9199d3b6473bb4d0f47416cdb08d556c03b8bc1cccf04e756b56d/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:073a36c8273647592ea332e816e75ef8da5c303236ec0167196793eb1e34657a", size = 2501635 }, + { url = "https://files.pythonhosted.org/packages/78/7e/aa9422e78419db0cbe75fb86d8e72b433818f2e62e2e394992d23d23a583/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3ce6b2b0231bda412463e152fc18335ba32faf4e8c23a754ad50ffa70e4091ee", size = 2314556 }, + { url = "https://files.pythonhosted.org/packages/a8/b2/15f7f556df0a6e5b3772a1e076a9d9f6c538ce5f05bd590eca8106508e06/kiwisolver-1.4.7-cp313-cp313-win32.whl", hash = "sha256:f4c9aee212bc89d4e13f58be11a56cc8036cabad119259d12ace14b34476fd07", size = 46364 }, + { url = "https://files.pythonhosted.org/packages/0b/db/32e897e43a330eee8e4770bfd2737a9584b23e33587a0812b8e20aac38f7/kiwisolver-1.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:8a3ec5aa8e38fc4c8af308917ce12c536f1c88452ce554027e55b22cbbfbff76", size = 55887 }, + { url = "https://files.pythonhosted.org/packages/c8/a4/df2bdca5270ca85fd25253049eb6708d4127be2ed0e5c2650217450b59e9/kiwisolver-1.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:76c8094ac20ec259471ac53e774623eb62e6e1f56cd8690c67ce6ce4fcb05650", size = 48530 }, + { url = "https://files.pythonhosted.org/packages/11/88/37ea0ea64512997b13d69772db8dcdc3bfca5442cda3a5e4bb943652ee3e/kiwisolver-1.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3f9362ecfca44c863569d3d3c033dbe8ba452ff8eed6f6b5806382741a1334bd", size = 122449 }, + { url = "https://files.pythonhosted.org/packages/4e/45/5a5c46078362cb3882dcacad687c503089263c017ca1241e0483857791eb/kiwisolver-1.4.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e8df2eb9b2bac43ef8b082e06f750350fbbaf2887534a5be97f6cf07b19d9583", size = 65757 }, + { url = "https://files.pythonhosted.org/packages/8a/be/a6ae58978772f685d48dd2e84460937761c53c4bbd84e42b0336473d9775/kiwisolver-1.4.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f32d6edbc638cde7652bd690c3e728b25332acbadd7cad670cc4a02558d9c417", size = 64312 }, + { url = "https://files.pythonhosted.org/packages/f4/04/18ef6f452d311e1e1eb180c9bf5589187fa1f042db877e6fe443ef10099c/kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e2e6c39bd7b9372b0be21456caab138e8e69cc0fc1190a9dfa92bd45a1e6e904", size = 1626966 }, + { url = "https://files.pythonhosted.org/packages/21/b1/40655f6c3fa11ce740e8a964fa8e4c0479c87d6a7944b95af799c7a55dfe/kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dda56c24d869b1193fcc763f1284b9126550eaf84b88bbc7256e15028f19188a", size = 1607044 }, + { url = "https://files.pythonhosted.org/packages/fd/93/af67dbcfb9b3323bbd2c2db1385a7139d8f77630e4a37bb945b57188eb2d/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79849239c39b5e1fd906556c474d9b0439ea6792b637511f3fe3a41158d89ca8", size = 1391879 }, + { url = "https://files.pythonhosted.org/packages/40/6f/d60770ef98e77b365d96061d090c0cd9e23418121c55fff188fa4bdf0b54/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e3bc157fed2a4c02ec468de4ecd12a6e22818d4f09cde2c31ee3226ffbefab2", size = 1504751 }, + { url = "https://files.pythonhosted.org/packages/fa/3a/5f38667d313e983c432f3fcd86932177519ed8790c724e07d77d1de0188a/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3da53da805b71e41053dc670f9a820d1157aae77b6b944e08024d17bcd51ef88", size = 1436990 }, + { url = "https://files.pythonhosted.org/packages/cb/3b/1520301a47326e6a6043b502647e42892be33b3f051e9791cc8bb43f1a32/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8705f17dfeb43139a692298cb6637ee2e59c0194538153e83e9ee0c75c2eddde", size = 2191122 }, + { url = "https://files.pythonhosted.org/packages/cf/c4/eb52da300c166239a2233f1f9c4a1b767dfab98fae27681bfb7ea4873cb6/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:82a5c2f4b87c26bb1a0ef3d16b5c4753434633b83d365cc0ddf2770c93829e3c", size = 2338126 }, + { url = "https://files.pythonhosted.org/packages/1a/cb/42b92fd5eadd708dd9107c089e817945500685f3437ce1fd387efebc6d6e/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce8be0466f4c0d585cdb6c1e2ed07232221df101a4c6f28821d2aa754ca2d9e2", size = 2298313 }, + { url = "https://files.pythonhosted.org/packages/4f/eb/be25aa791fe5fc75a8b1e0c965e00f942496bc04635c9aae8035f6b76dcd/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:409afdfe1e2e90e6ee7fc896f3df9a7fec8e793e58bfa0d052c8a82f99c37abb", size = 2437784 }, + { url = "https://files.pythonhosted.org/packages/c5/22/30a66be7f3368d76ff95689e1c2e28d382383952964ab15330a15d8bfd03/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5b9c3f4ee0b9a439d2415012bd1b1cc2df59e4d6a9939f4d669241d30b414327", size = 2253988 }, + { url = "https://files.pythonhosted.org/packages/35/d3/5f2ecb94b5211c8a04f218a76133cc8d6d153b0f9cd0b45fad79907f0689/kiwisolver-1.4.7-cp39-cp39-win32.whl", hash = "sha256:a79ae34384df2b615eefca647a2873842ac3b596418032bef9a7283675962644", size = 46980 }, + { url = "https://files.pythonhosted.org/packages/ef/17/cd10d020578764ea91740204edc6b3236ed8106228a46f568d716b11feb2/kiwisolver-1.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:cf0438b42121a66a3a667de17e779330fc0f20b0d97d59d2f2121e182b0505e4", size = 55847 }, + { url = "https://files.pythonhosted.org/packages/91/84/32232502020bd78d1d12be7afde15811c64a95ed1f606c10456db4e4c3ac/kiwisolver-1.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:764202cc7e70f767dab49e8df52c7455e8de0df5d858fa801a11aa0d882ccf3f", size = 48494 }, + { url = "https://files.pythonhosted.org/packages/ac/59/741b79775d67ab67ced9bb38552da688c0305c16e7ee24bba7a2be253fb7/kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:94252291e3fe68001b1dd747b4c0b3be12582839b95ad4d1b641924d68fd4643", size = 59491 }, + { url = "https://files.pythonhosted.org/packages/58/cc/fb239294c29a5656e99e3527f7369b174dd9cc7c3ef2dea7cb3c54a8737b/kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b7dfa3b546da08a9f622bb6becdb14b3e24aaa30adba66749d38f3cc7ea9706", size = 57648 }, + { url = "https://files.pythonhosted.org/packages/3b/ef/2f009ac1f7aab9f81efb2d837301d255279d618d27b6015780115ac64bdd/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd3de6481f4ed8b734da5df134cd5a6a64fe32124fe83dde1e5b5f29fe30b1e6", size = 84257 }, + { url = "https://files.pythonhosted.org/packages/81/e1/c64f50987f85b68b1c52b464bb5bf73e71570c0f7782d626d1eb283ad620/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a91b5f9f1205845d488c928e8570dcb62b893372f63b8b6e98b863ebd2368ff2", size = 80906 }, + { url = "https://files.pythonhosted.org/packages/fd/71/1687c5c0a0be2cee39a5c9c389e546f9c6e215e46b691d00d9f646892083/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40fa14dbd66b8b8f470d5fc79c089a66185619d31645f9b0773b88b19f7223c4", size = 79951 }, + { url = "https://files.pythonhosted.org/packages/ea/8b/d7497df4a1cae9367adf21665dd1f896c2a7aeb8769ad77b662c5e2bcce7/kiwisolver-1.4.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:eb542fe7933aa09d8d8f9d9097ef37532a7df6497819d16efe4359890a2f417a", size = 55715 }, + { url = "https://files.pythonhosted.org/packages/d5/df/ce37d9b26f07ab90880923c94d12a6ff4d27447096b4c849bfc4339ccfdf/kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8b01aac285f91ca889c800042c35ad3b239e704b150cfd3382adfc9dcc780e39", size = 58666 }, + { url = "https://files.pythonhosted.org/packages/b0/d3/e4b04f43bc629ac8e186b77b2b1a251cdfa5b7610fa189dc0db622672ce6/kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:48be928f59a1f5c8207154f935334d374e79f2b5d212826307d072595ad76a2e", size = 57088 }, + { url = "https://files.pythonhosted.org/packages/30/1c/752df58e2d339e670a535514d2db4fe8c842ce459776b8080fbe08ebb98e/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f37cfe618a117e50d8c240555331160d73d0411422b59b5ee217843d7b693608", size = 84321 }, + { url = "https://files.pythonhosted.org/packages/f0/f8/fe6484e847bc6e238ec9f9828089fb2c0bb53f2f5f3a79351fde5b565e4f/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:599b5c873c63a1f6ed7eead644a8a380cfbdf5db91dcb6f85707aaab213b1674", size = 80776 }, + { url = "https://files.pythonhosted.org/packages/9b/57/d7163c0379f250ef763aba85330a19feefb5ce6cb541ade853aaba881524/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:801fa7802e5cfabe3ab0c81a34c323a319b097dfb5004be950482d882f3d7225", size = 79984 }, + { url = "https://files.pythonhosted.org/packages/8c/95/4a103776c265d13b3d2cd24fb0494d4e04ea435a8ef97e1b2c026d43250b/kiwisolver-1.4.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0c6c43471bc764fad4bc99c5c2d6d16a676b1abf844ca7c8702bdae92df01ee0", size = 55811 }, +] + +[[package]] +name = "llvmlite" +version = "0.43.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/3d/f513755f285db51ab363a53e898b85562e950f79a2e6767a364530c2f645/llvmlite-0.43.0.tar.gz", hash = "sha256:ae2b5b5c3ef67354824fb75517c8db5fbe93bc02cd9671f3c62271626bc041d5", size = 157069 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/ff/6ca7e98998b573b4bd6566f15c35e5c8bea829663a6df0c7aa55ab559da9/llvmlite-0.43.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a289af9a1687c6cf463478f0fa8e8aa3b6fb813317b0d70bf1ed0759eab6f761", size = 31064408 }, + { url = "https://files.pythonhosted.org/packages/ca/5c/a27f9257f86f0cda3f764ff21d9f4217b9f6a0d45e7a39ecfa7905f524ce/llvmlite-0.43.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d4fd101f571a31acb1559ae1af30f30b1dc4b3186669f92ad780e17c81e91bc", size = 28793153 }, + { url = "https://files.pythonhosted.org/packages/7e/3c/4410f670ad0a911227ea2ecfcba9f672a77cf1924df5280c4562032ec32d/llvmlite-0.43.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d434ec7e2ce3cc8f452d1cd9a28591745de022f931d67be688a737320dfcead", size = 42857276 }, + { url = "https://files.pythonhosted.org/packages/c6/21/2ffbab5714e72f2483207b4a1de79b2eecd9debbf666ff4e7067bcc5c134/llvmlite-0.43.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6912a87782acdff6eb8bf01675ed01d60ca1f2551f8176a300a886f09e836a6a", size = 43871781 }, + { url = "https://files.pythonhosted.org/packages/f2/26/b5478037c453554a61625ef1125f7e12bb1429ae11c6376f47beba9b0179/llvmlite-0.43.0-cp310-cp310-win_amd64.whl", hash = "sha256:14f0e4bf2fd2d9a75a3534111e8ebeb08eda2f33e9bdd6dfa13282afacdde0ed", size = 28123487 }, + { url = "https://files.pythonhosted.org/packages/95/8c/de3276d773ab6ce3ad676df5fab5aac19696b2956319d65d7dd88fb10f19/llvmlite-0.43.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3e8d0618cb9bfe40ac38a9633f2493d4d4e9fcc2f438d39a4e854f39cc0f5f98", size = 31064409 }, + { url = "https://files.pythonhosted.org/packages/ee/e1/38deed89ced4cf378c61e232265cfe933ccde56ae83c901aa68b477d14b1/llvmlite-0.43.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0a9a1a39d4bf3517f2af9d23d479b4175ead205c592ceeb8b89af48a327ea57", size = 28793149 }, + { url = "https://files.pythonhosted.org/packages/2f/b2/4429433eb2dc8379e2cb582502dca074c23837f8fd009907f78a24de4c25/llvmlite-0.43.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1da416ab53e4f7f3bc8d4eeba36d801cc1894b9fbfbf2022b29b6bad34a7df2", size = 42857277 }, + { url = "https://files.pythonhosted.org/packages/6b/99/5d00a7d671b1ba1751fc9f19d3b36f3300774c6eebe2bcdb5f6191763eb4/llvmlite-0.43.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:977525a1e5f4059316b183fb4fd34fa858c9eade31f165427a3977c95e3ee749", size = 43871781 }, + { url = "https://files.pythonhosted.org/packages/20/ab/ed5ed3688c6ba4f0b8d789da19fd8e30a9cf7fc5852effe311bc5aefe73e/llvmlite-0.43.0-cp311-cp311-win_amd64.whl", hash = "sha256:d5bd550001d26450bd90777736c69d68c487d17bf371438f975229b2b8241a91", size = 28107433 }, + { url = "https://files.pythonhosted.org/packages/0b/67/9443509e5d2b6d8587bae3ede5598fa8bd586b1c7701696663ea8af15b5b/llvmlite-0.43.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f99b600aa7f65235a5a05d0b9a9f31150c390f31261f2a0ba678e26823ec38f7", size = 31064409 }, + { url = "https://files.pythonhosted.org/packages/a2/9c/24139d3712d2d352e300c39c0e00d167472c08b3bd350c3c33d72c88ff8d/llvmlite-0.43.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:35d80d61d0cda2d767f72de99450766250560399edc309da16937b93d3b676e7", size = 28793145 }, + { url = "https://files.pythonhosted.org/packages/bf/f1/4c205a48488e574ee9f6505d50e84370a978c90f08dab41a42d8f2c576b6/llvmlite-0.43.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eccce86bba940bae0d8d48ed925f21dbb813519169246e2ab292b5092aba121f", size = 42857276 }, + { url = "https://files.pythonhosted.org/packages/00/5f/323c4d56e8401c50185fd0e875fcf06b71bf825a863699be1eb10aa2a9cb/llvmlite-0.43.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df6509e1507ca0760787a199d19439cc887bfd82226f5af746d6977bd9f66844", size = 43871781 }, + { url = "https://files.pythonhosted.org/packages/c6/94/dea10e263655ce78d777e78d904903faae39d1fc440762be4a9dc46bed49/llvmlite-0.43.0-cp312-cp312-win_amd64.whl", hash = "sha256:7a2872ee80dcf6b5dbdc838763d26554c2a18aa833d31a2635bff16aafefb9c9", size = 28107442 }, + { url = "https://files.pythonhosted.org/packages/2a/73/12925b1bbb3c2beb6d96f892ef5b4d742c34f00ddb9f4a125e9e87b22f52/llvmlite-0.43.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9cd2a7376f7b3367019b664c21f0c61766219faa3b03731113ead75107f3b66c", size = 31064410 }, + { url = "https://files.pythonhosted.org/packages/cc/61/58c70aa0808a8cba825a7d98cc65bef4801b99328fba80837bfcb5fc767f/llvmlite-0.43.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:18e9953c748b105668487b7c81a3e97b046d8abf95c4ddc0cd3c94f4e4651ae8", size = 28793145 }, + { url = "https://files.pythonhosted.org/packages/c8/c6/9324eb5de2ba9d99cbed853d85ba7a318652a48e077797bec27cf40f911d/llvmlite-0.43.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74937acd22dc11b33946b67dca7680e6d103d6e90eeaaaf932603bec6fe7b03a", size = 42857276 }, + { url = "https://files.pythonhosted.org/packages/e0/d0/889e9705107db7b1ec0767b03f15d7b95b4c4f9fdf91928ab1c7e9ffacf6/llvmlite-0.43.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc9efc739cc6ed760f795806f67889923f7274276f0eb45092a1473e40d9b867", size = 43871777 }, + { url = "https://files.pythonhosted.org/packages/df/41/73cc26a2634b538cfe813f618c91e7e9960b8c163f8f0c94a2b0f008b9da/llvmlite-0.43.0-cp39-cp39-win_amd64.whl", hash = "sha256:47e147cdda9037f94b399bf03bfd8a6b6b1f2f90be94a454e3386f006455a9b4", size = 28123489 }, +] + +[[package]] +name = "lxml" +version = "5.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/6b/20c3a4b24751377aaa6307eb230b66701024012c29dd374999cc92983269/lxml-5.3.0.tar.gz", hash = "sha256:4e109ca30d1edec1ac60cdbe341905dc3b8f55b16855e03a54aaf59e51ec8c6f", size = 3679318 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/ce/2789e39eddf2b13fac29878bfa465f0910eb6b0096e29090e5176bc8cf43/lxml-5.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:dd36439be765e2dde7660212b5275641edbc813e7b24668831a5c8ac91180656", size = 8124570 }, + { url = "https://files.pythonhosted.org/packages/24/a8/f4010166a25d41715527129af2675981a50d3bbf7df09c5d9ab8ca24fbf9/lxml-5.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ae5fe5c4b525aa82b8076c1a59d642c17b6e8739ecf852522c6321852178119d", size = 4413042 }, + { url = "https://files.pythonhosted.org/packages/41/a4/7e45756cecdd7577ddf67a68b69c1db0f5ddbf0c9f65021ee769165ffc5a/lxml-5.3.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:501d0d7e26b4d261fca8132854d845e4988097611ba2531408ec91cf3fd9d20a", size = 5139213 }, + { url = "https://files.pythonhosted.org/packages/02/e2/ecf845b12323c92748077e1818b64e8b4dba509a4cb12920b3762ebe7552/lxml-5.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb66442c2546446944437df74379e9cf9e9db353e61301d1a0e26482f43f0dd8", size = 4838814 }, + { url = "https://files.pythonhosted.org/packages/12/91/619f9fb72cf75e9ceb8700706f7276f23995f6ad757e6d400fbe35ca4990/lxml-5.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e41506fec7a7f9405b14aa2d5c8abbb4dbbd09d88f9496958b6d00cb4d45330", size = 5425084 }, + { url = "https://files.pythonhosted.org/packages/25/3b/162a85a8f0fd2a3032ec3f936636911c6e9523a8e263fffcfd581ce98b54/lxml-5.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f7d4a670107d75dfe5ad080bed6c341d18c4442f9378c9f58e5851e86eb79965", size = 4875993 }, + { url = "https://files.pythonhosted.org/packages/43/af/dd3f58cc7d946da6ae42909629a2b1d5dd2d1b583334d4af9396697d6863/lxml-5.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41ce1f1e2c7755abfc7e759dc34d7d05fd221723ff822947132dc934d122fe22", size = 5012462 }, + { url = "https://files.pythonhosted.org/packages/69/c1/5ea46b2d4c98f5bf5c83fffab8a0ad293c9bc74df9ecfbafef10f77f7201/lxml-5.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:44264ecae91b30e5633013fb66f6ddd05c006d3e0e884f75ce0b4755b3e3847b", size = 4815288 }, + { url = "https://files.pythonhosted.org/packages/1d/51/a0acca077ad35da458f4d3f729ef98effd2b90f003440d35fc36323f8ae6/lxml-5.3.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:3c174dc350d3ec52deb77f2faf05c439331d6ed5e702fc247ccb4e6b62d884b7", size = 5472435 }, + { url = "https://files.pythonhosted.org/packages/4d/6b/0989c9368986961a6b0f55b46c80404c4b758417acdb6d87bfc3bd5f4967/lxml-5.3.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:2dfab5fa6a28a0b60a20638dc48e6343c02ea9933e3279ccb132f555a62323d8", size = 4976354 }, + { url = "https://files.pythonhosted.org/packages/05/9e/87492d03ff604fbf656ed2bf3e2e8d28f5d58ea1f00ff27ac27b06509079/lxml-5.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b1c8c20847b9f34e98080da785bb2336ea982e7f913eed5809e5a3c872900f32", size = 5029973 }, + { url = "https://files.pythonhosted.org/packages/f9/cc/9ae1baf5472af88e19e2c454b3710c1be9ecafb20eb474eeabcd88a055d2/lxml-5.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2c86bf781b12ba417f64f3422cfc302523ac9cd1d8ae8c0f92a1c66e56ef2e86", size = 4888837 }, + { url = "https://files.pythonhosted.org/packages/d2/10/5594ffaec8c120d75b17e3ad23439b740a51549a9b5fd7484b2179adfe8f/lxml-5.3.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c162b216070f280fa7da844531169be0baf9ccb17263cf5a8bf876fcd3117fa5", size = 5530555 }, + { url = "https://files.pythonhosted.org/packages/ea/9b/de17f05377c8833343b629905571fb06cff2028f15a6f58ae2267662e341/lxml-5.3.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:36aef61a1678cb778097b4a6eeae96a69875d51d1e8f4d4b491ab3cfb54b5a03", size = 5405314 }, + { url = "https://files.pythonhosted.org/packages/8a/b4/227be0f1f3cca8255925985164c3838b8b36e441ff0cc10c1d3c6bdba031/lxml-5.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f65e5120863c2b266dbcc927b306c5b78e502c71edf3295dfcb9501ec96e5fc7", size = 5079303 }, + { url = "https://files.pythonhosted.org/packages/5c/ee/19abcebb7fc40319bb71cd6adefa1ad94d09b5660228715854d6cc420713/lxml-5.3.0-cp310-cp310-win32.whl", hash = "sha256:ef0c1fe22171dd7c7c27147f2e9c3e86f8bdf473fed75f16b0c2e84a5030ce80", size = 3475126 }, + { url = "https://files.pythonhosted.org/packages/a1/35/183d32551447e280032b2331738cd850da435a42f850b71ebeaab42c1313/lxml-5.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:052d99051e77a4f3e8482c65014cf6372e61b0a6f4fe9edb98503bb5364cfee3", size = 3805065 }, + { url = "https://files.pythonhosted.org/packages/5c/a8/449faa2a3cbe6a99f8d38dcd51a3ee8844c17862841a6f769ea7c2a9cd0f/lxml-5.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74bcb423462233bc5d6066e4e98b0264e7c1bed7541fff2f4e34fe6b21563c8b", size = 8141056 }, + { url = "https://files.pythonhosted.org/packages/ac/8a/ae6325e994e2052de92f894363b038351c50ee38749d30cc6b6d96aaf90f/lxml-5.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a3d819eb6f9b8677f57f9664265d0a10dd6551d227afb4af2b9cd7bdc2ccbf18", size = 4425238 }, + { url = "https://files.pythonhosted.org/packages/f8/fb/128dddb7f9086236bce0eeae2bfb316d138b49b159f50bc681d56c1bdd19/lxml-5.3.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b8f5db71b28b8c404956ddf79575ea77aa8b1538e8b2ef9ec877945b3f46442", size = 5095197 }, + { url = "https://files.pythonhosted.org/packages/b4/f9/a181a8ef106e41e3086629c8bdb2d21a942f14c84a0e77452c22d6b22091/lxml-5.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c3406b63232fc7e9b8783ab0b765d7c59e7c59ff96759d8ef9632fca27c7ee4", size = 4809809 }, + { url = "https://files.pythonhosted.org/packages/25/2f/b20565e808f7f6868aacea48ddcdd7e9e9fb4c799287f21f1a6c7c2e8b71/lxml-5.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ecdd78ab768f844c7a1d4a03595038c166b609f6395e25af9b0f3f26ae1230f", size = 5407593 }, + { url = "https://files.pythonhosted.org/packages/23/0e/caac672ec246d3189a16c4d364ed4f7d6bf856c080215382c06764058c08/lxml-5.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:168f2dfcfdedf611eb285efac1516c8454c8c99caf271dccda8943576b67552e", size = 4866657 }, + { url = "https://files.pythonhosted.org/packages/67/a4/1f5fbd3f58d4069000522196b0b776a014f3feec1796da03e495cf23532d/lxml-5.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa617107a410245b8660028a7483b68e7914304a6d4882b5ff3d2d3eb5948d8c", size = 4967017 }, + { url = "https://files.pythonhosted.org/packages/ee/73/623ecea6ca3c530dd0a4ed0d00d9702e0e85cd5624e2d5b93b005fe00abd/lxml-5.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:69959bd3167b993e6e710b99051265654133a98f20cec1d9b493b931942e9c16", size = 4810730 }, + { url = "https://files.pythonhosted.org/packages/1d/ce/fb84fb8e3c298f3a245ae3ea6221c2426f1bbaa82d10a88787412a498145/lxml-5.3.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:bd96517ef76c8654446fc3db9242d019a1bb5fe8b751ba414765d59f99210b79", size = 5455154 }, + { url = "https://files.pythonhosted.org/packages/b1/72/4d1ad363748a72c7c0411c28be2b0dc7150d91e823eadad3b91a4514cbea/lxml-5.3.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:ab6dd83b970dc97c2d10bc71aa925b84788c7c05de30241b9e96f9b6d9ea3080", size = 4969416 }, + { url = "https://files.pythonhosted.org/packages/42/07/b29571a58a3a80681722ea8ed0ba569211d9bb8531ad49b5cacf6d409185/lxml-5.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:eec1bb8cdbba2925bedc887bc0609a80e599c75b12d87ae42ac23fd199445654", size = 5013672 }, + { url = "https://files.pythonhosted.org/packages/b9/93/bde740d5a58cf04cbd38e3dd93ad1e36c2f95553bbf7d57807bc6815d926/lxml-5.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6a7095eeec6f89111d03dabfe5883a1fd54da319c94e0fb104ee8f23616b572d", size = 4878644 }, + { url = "https://files.pythonhosted.org/packages/56/b5/645c8c02721d49927c93181de4017164ec0e141413577687c3df8ff0800f/lxml-5.3.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6f651ebd0b21ec65dfca93aa629610a0dbc13dbc13554f19b0113da2e61a4763", size = 5511531 }, + { url = "https://files.pythonhosted.org/packages/85/3f/6a99a12d9438316f4fc86ef88c5d4c8fb674247b17f3173ecadd8346b671/lxml-5.3.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f422a209d2455c56849442ae42f25dbaaba1c6c3f501d58761c619c7836642ec", size = 5402065 }, + { url = "https://files.pythonhosted.org/packages/80/8a/df47bff6ad5ac57335bf552babfb2408f9eb680c074ec1ba412a1a6af2c5/lxml-5.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:62f7fdb0d1ed2065451f086519865b4c90aa19aed51081979ecd05a21eb4d1be", size = 5069775 }, + { url = "https://files.pythonhosted.org/packages/08/ae/e7ad0f0fbe4b6368c5ee1e3ef0c3365098d806d42379c46c1ba2802a52f7/lxml-5.3.0-cp311-cp311-win32.whl", hash = "sha256:c6379f35350b655fd817cd0d6cbeef7f265f3ae5fedb1caae2eb442bbeae9ab9", size = 3474226 }, + { url = "https://files.pythonhosted.org/packages/c3/b5/91c2249bfac02ee514ab135e9304b89d55967be7e53e94a879b74eec7a5c/lxml-5.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c52100e2c2dbb0649b90467935c4b0de5528833c76a35ea1a2691ec9f1ee7a1", size = 3814971 }, + { url = "https://files.pythonhosted.org/packages/eb/6d/d1f1c5e40c64bf62afd7a3f9b34ce18a586a1cccbf71e783cd0a6d8e8971/lxml-5.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e99f5507401436fdcc85036a2e7dc2e28d962550afe1cbfc07c40e454256a859", size = 8171753 }, + { url = "https://files.pythonhosted.org/packages/bd/83/26b1864921869784355459f374896dcf8b44d4af3b15d7697e9156cb2de9/lxml-5.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:384aacddf2e5813a36495233b64cb96b1949da72bef933918ba5c84e06af8f0e", size = 4441955 }, + { url = "https://files.pythonhosted.org/packages/e0/d2/e9bff9fb359226c25cda3538f664f54f2804f4b37b0d7c944639e1a51f69/lxml-5.3.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:874a216bf6afaf97c263b56371434e47e2c652d215788396f60477540298218f", size = 5050778 }, + { url = "https://files.pythonhosted.org/packages/88/69/6972bfafa8cd3ddc8562b126dd607011e218e17be313a8b1b9cc5a0ee876/lxml-5.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65ab5685d56914b9a2a34d67dd5488b83213d680b0c5d10b47f81da5a16b0b0e", size = 4748628 }, + { url = "https://files.pythonhosted.org/packages/5d/ea/a6523c7c7f6dc755a6eed3d2f6d6646617cad4d3d6d8ce4ed71bfd2362c8/lxml-5.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aac0bbd3e8dd2d9c45ceb82249e8bdd3ac99131a32b4d35c8af3cc9db1657179", size = 5322215 }, + { url = "https://files.pythonhosted.org/packages/99/37/396fbd24a70f62b31d988e4500f2068c7f3fd399d2fd45257d13eab51a6f/lxml-5.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b369d3db3c22ed14c75ccd5af429086f166a19627e84a8fdade3f8f31426e52a", size = 4813963 }, + { url = "https://files.pythonhosted.org/packages/09/91/e6136f17459a11ce1757df864b213efbeab7adcb2efa63efb1b846ab6723/lxml-5.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24037349665434f375645fa9d1f5304800cec574d0310f618490c871fd902b3", size = 4923353 }, + { url = "https://files.pythonhosted.org/packages/1d/7c/2eeecf87c9a1fca4f84f991067c693e67340f2b7127fc3eca8fa29d75ee3/lxml-5.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:62d172f358f33a26d6b41b28c170c63886742f5b6772a42b59b4f0fa10526cb1", size = 4740541 }, + { url = "https://files.pythonhosted.org/packages/3b/ed/4c38ba58defca84f5f0d0ac2480fdcd99fc7ae4b28fc417c93640a6949ae/lxml-5.3.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:c1f794c02903c2824fccce5b20c339a1a14b114e83b306ff11b597c5f71a1c8d", size = 5346504 }, + { url = "https://files.pythonhosted.org/packages/a5/22/bbd3995437e5745cb4c2b5d89088d70ab19d4feabf8a27a24cecb9745464/lxml-5.3.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:5d6a6972b93c426ace71e0be9a6f4b2cfae9b1baed2eed2006076a746692288c", size = 4898077 }, + { url = "https://files.pythonhosted.org/packages/0a/6e/94537acfb5b8f18235d13186d247bca478fea5e87d224644e0fe907df976/lxml-5.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:3879cc6ce938ff4eb4900d901ed63555c778731a96365e53fadb36437a131a99", size = 4946543 }, + { url = "https://files.pythonhosted.org/packages/8d/e8/4b15df533fe8e8d53363b23a41df9be907330e1fa28c7ca36893fad338ee/lxml-5.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:74068c601baff6ff021c70f0935b0c7bc528baa8ea210c202e03757c68c5a4ff", size = 4816841 }, + { url = "https://files.pythonhosted.org/packages/1a/e7/03f390ea37d1acda50bc538feb5b2bda6745b25731e4e76ab48fae7106bf/lxml-5.3.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ecd4ad8453ac17bc7ba3868371bffb46f628161ad0eefbd0a855d2c8c32dd81a", size = 5417341 }, + { url = "https://files.pythonhosted.org/packages/ea/99/d1133ab4c250da85a883c3b60249d3d3e7c64f24faff494cf0fd23f91e80/lxml-5.3.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7e2f58095acc211eb9d8b5771bf04df9ff37d6b87618d1cbf85f92399c98dae8", size = 5327539 }, + { url = "https://files.pythonhosted.org/packages/7d/ed/e6276c8d9668028213df01f598f385b05b55a4e1b4662ee12ef05dab35aa/lxml-5.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e63601ad5cd8f860aa99d109889b5ac34de571c7ee902d6812d5d9ddcc77fa7d", size = 5012542 }, + { url = "https://files.pythonhosted.org/packages/36/88/684d4e800f5aa28df2a991a6a622783fb73cf0e46235cfa690f9776f032e/lxml-5.3.0-cp312-cp312-win32.whl", hash = "sha256:17e8d968d04a37c50ad9c456a286b525d78c4a1c15dd53aa46c1d8e06bf6fa30", size = 3486454 }, + { url = "https://files.pythonhosted.org/packages/fc/82/ace5a5676051e60355bd8fb945df7b1ba4f4fb8447f2010fb816bfd57724/lxml-5.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:c1a69e58a6bb2de65902051d57fde951febad631a20a64572677a1052690482f", size = 3816857 }, + { url = "https://files.pythonhosted.org/packages/94/6a/42141e4d373903bfea6f8e94b2f554d05506dfda522ada5343c651410dc8/lxml-5.3.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c72e9563347c7395910de6a3100a4840a75a6f60e05af5e58566868d5eb2d6a", size = 8156284 }, + { url = "https://files.pythonhosted.org/packages/91/5e/fa097f0f7d8b3d113fb7312c6308af702f2667f22644441715be961f2c7e/lxml-5.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e92ce66cd919d18d14b3856906a61d3f6b6a8500e0794142338da644260595cd", size = 4432407 }, + { url = "https://files.pythonhosted.org/packages/2d/a1/b901988aa6d4ff937f2e5cfc114e4ec561901ff00660c3e56713642728da/lxml-5.3.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d04f064bebdfef9240478f7a779e8c5dc32b8b7b0b2fc6a62e39b928d428e51", size = 5048331 }, + { url = "https://files.pythonhosted.org/packages/30/0f/b2a54f48e52de578b71bbe2a2f8160672a8a5e103df3a78da53907e8c7ed/lxml-5.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c2fb570d7823c2bbaf8b419ba6e5662137f8166e364a8b2b91051a1fb40ab8b", size = 4744835 }, + { url = "https://files.pythonhosted.org/packages/82/9d/b000c15538b60934589e83826ecbc437a1586488d7c13f8ee5ff1f79a9b8/lxml-5.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c120f43553ec759f8de1fee2f4794452b0946773299d44c36bfe18e83caf002", size = 5316649 }, + { url = "https://files.pythonhosted.org/packages/e3/ee/ffbb9eaff5e541922611d2c56b175c45893d1c0b8b11e5a497708a6a3b3b/lxml-5.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:562e7494778a69086f0312ec9689f6b6ac1c6b65670ed7d0267e49f57ffa08c4", size = 4812046 }, + { url = "https://files.pythonhosted.org/packages/15/ff/7ff89d567485c7b943cdac316087f16b2399a8b997007ed352a1248397e5/lxml-5.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:423b121f7e6fa514ba0c7918e56955a1d4470ed35faa03e3d9f0e3baa4c7e492", size = 4918597 }, + { url = "https://files.pythonhosted.org/packages/c6/a3/535b6ed8c048412ff51268bdf4bf1cf052a37aa7e31d2e6518038a883b29/lxml-5.3.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:c00f323cc00576df6165cc9d21a4c21285fa6b9989c5c39830c3903dc4303ef3", size = 4738071 }, + { url = "https://files.pythonhosted.org/packages/7a/8f/cbbfa59cb4d4fd677fe183725a76d8c956495d7a3c7f111ab8f5e13d2e83/lxml-5.3.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:1fdc9fae8dd4c763e8a31e7630afef517eab9f5d5d31a278df087f307bf601f4", size = 5342213 }, + { url = "https://files.pythonhosted.org/packages/5c/fb/db4c10dd9958d4b52e34d1d1f7c1f434422aeaf6ae2bbaaff2264351d944/lxml-5.3.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:658f2aa69d31e09699705949b5fc4719cbecbd4a97f9656a232e7d6c7be1a367", size = 4893749 }, + { url = "https://files.pythonhosted.org/packages/f2/38/bb4581c143957c47740de18a3281a0cab7722390a77cc6e610e8ebf2d736/lxml-5.3.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1473427aff3d66a3fa2199004c3e601e6c4500ab86696edffdbc84954c72d832", size = 4945901 }, + { url = "https://files.pythonhosted.org/packages/fc/d5/18b7de4960c731e98037bd48fa9f8e6e8f2558e6fbca4303d9b14d21ef3b/lxml-5.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a87de7dd873bf9a792bf1e58b1c3887b9264036629a5bf2d2e6579fe8e73edff", size = 4815447 }, + { url = "https://files.pythonhosted.org/packages/97/a8/cd51ceaad6eb849246559a8ef60ae55065a3df550fc5fcd27014361c1bab/lxml-5.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0d7b36afa46c97875303a94e8f3ad932bf78bace9e18e603f2085b652422edcd", size = 5411186 }, + { url = "https://files.pythonhosted.org/packages/89/c3/1e3dabab519481ed7b1fdcba21dcfb8832f57000733ef0e71cf6d09a5e03/lxml-5.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cf120cce539453ae086eacc0130a324e7026113510efa83ab42ef3fcfccac7fb", size = 5324481 }, + { url = "https://files.pythonhosted.org/packages/b6/17/71e9984cf0570cd202ac0a1c9ed5c1b8889b0fc8dc736f5ef0ffb181c284/lxml-5.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:df5c7333167b9674aa8ae1d4008fa4bc17a313cc490b2cca27838bbdcc6bb15b", size = 5011053 }, + { url = "https://files.pythonhosted.org/packages/69/68/9f7e6d3312a91e30829368c2b3217e750adef12a6f8eb10498249f4e8d72/lxml-5.3.0-cp313-cp313-win32.whl", hash = "sha256:c802e1c2ed9f0c06a65bc4ed0189d000ada8049312cfeab6ca635e39c9608957", size = 3485634 }, + { url = "https://files.pythonhosted.org/packages/7d/db/214290d58ad68c587bd5d6af3d34e56830438733d0d0856c0275fde43652/lxml-5.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:406246b96d552e0503e17a1006fd27edac678b3fcc9f1be71a2f94b4ff61528d", size = 3814417 }, + { url = "https://files.pythonhosted.org/packages/89/a9/63af38c7f42baff8251d937be91c6decfe9e4725fe16283dcee428e08d5c/lxml-5.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1ffc23010330c2ab67fac02781df60998ca8fe759e8efde6f8b756a20599c5de", size = 8129239 }, + { url = "https://files.pythonhosted.org/packages/23/b2/45e12a5b8508ee9de0af432d0dc5fcc786cd78037d692a3de7571c2db04c/lxml-5.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2b3778cb38212f52fac9fe913017deea2fdf4eb1a4f8e4cfc6b009a13a6d3fcc", size = 4415821 }, + { url = "https://files.pythonhosted.org/packages/88/88/a01dc8055d431c39859ec3806dbe4df6cf7a80b0431227a52de8428d2cf6/lxml-5.3.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b0c7a688944891086ba192e21c5229dea54382f4836a209ff8d0a660fac06be", size = 5139927 }, + { url = "https://files.pythonhosted.org/packages/13/d9/c0f3fd5582a26ea887122feb9cfe84215642ecf10886dcb50a603a6ef448/lxml-5.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:747a3d3e98e24597981ca0be0fd922aebd471fa99d0043a3842d00cdcad7ad6a", size = 4839659 }, + { url = "https://files.pythonhosted.org/packages/64/06/290728f6fde1761c323db28ece9601018db72ecafa21b182cfea99e7cb2e/lxml-5.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86a6b24b19eaebc448dc56b87c4865527855145d851f9fc3891673ff97950540", size = 5427269 }, + { url = "https://files.pythonhosted.org/packages/52/43/af104743bb733e85efc0be0e32c140e3e7be6050aca52b1e8a0b2867c382/lxml-5.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b11a5d918a6216e521c715b02749240fb07ae5a1fefd4b7bf12f833bc8b4fe70", size = 4876667 }, + { url = "https://files.pythonhosted.org/packages/d8/5f/9dea130ae3ba77848f4b93d11dfd365085620fb34c5c9d22746227b86952/lxml-5.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68b87753c784d6acb8a25b05cb526c3406913c9d988d51f80adecc2b0775d6aa", size = 5013541 }, + { url = "https://files.pythonhosted.org/packages/e8/87/a089806f0327ad7f7268c3f4d22f1d76215a923bf33ea808bb665bdeacfa/lxml-5.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:109fa6fede314cc50eed29e6e56c540075e63d922455346f11e4d7a036d2b8cf", size = 4818394 }, + { url = "https://files.pythonhosted.org/packages/87/63/b36ddd4a829a5de681bde7e9be4008a8b53c392dea4c8b1492c35727e150/lxml-5.3.0-cp39-cp39-manylinux_2_28_ppc64le.whl", hash = "sha256:02ced472497b8362c8e902ade23e3300479f4f43e45f4105c85ef43b8db85229", size = 5472977 }, + { url = "https://files.pythonhosted.org/packages/99/1f/677226f48e2d1ea590c24f3ead1799584517a62a394a338b96f62d3c732e/lxml-5.3.0-cp39-cp39-manylinux_2_28_s390x.whl", hash = "sha256:6b038cc86b285e4f9fea2ba5ee76e89f21ed1ea898e287dc277a25884f3a7dfe", size = 4978803 }, + { url = "https://files.pythonhosted.org/packages/9d/f8/1b96af1396f237de488b14f70b2c6ced5079b792770e6a0f7153f912124d/lxml-5.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:7437237c6a66b7ca341e868cda48be24b8701862757426852c9b3186de1da8a2", size = 5026166 }, + { url = "https://files.pythonhosted.org/packages/a9/42/86a09a2cabb7bed04d904e38cc09ac65e4916fc1b7eadf94bb924893988b/lxml-5.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7f41026c1d64043a36fda21d64c5026762d53a77043e73e94b71f0521939cc71", size = 4890234 }, + { url = "https://files.pythonhosted.org/packages/c9/0a/bf0edfe5635ed05ed69a8ae9c1e06dc28cf8becc4ea72f39d3624f20b3d9/lxml-5.3.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:482c2f67761868f0108b1743098640fbb2a28a8e15bf3f47ada9fa59d9fe08c3", size = 5533730 }, + { url = "https://files.pythonhosted.org/packages/00/cd/dfd8fd56415508751caac07c7ddb3b0a40aff346c11fabdd9d8aa2bfb329/lxml-5.3.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:1483fd3358963cc5c1c9b122c80606a3a79ee0875bcac0204149fa09d6ff2727", size = 5406452 }, + { url = "https://files.pythonhosted.org/packages/3f/35/fcc233c86f4e59f9498cde8ad6131e1ca41dc7aa084ec982d2cccca91cd7/lxml-5.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2dec2d1130a9cda5b904696cec33b2cfb451304ba9081eeda7f90f724097300a", size = 5078114 }, + { url = "https://files.pythonhosted.org/packages/9b/55/94c9bc55ec20744a21c949138649442298cff4189067b7e0844dd0a111d0/lxml-5.3.0-cp39-cp39-win32.whl", hash = "sha256:a0eabd0a81625049c5df745209dc7fcef6e2aea7793e5f003ba363610aa0a3ff", size = 3478072 }, + { url = "https://files.pythonhosted.org/packages/bb/ab/68821837e454c4c34f40cbea8806637ec4d814b76d3d017a24a39c651a79/lxml-5.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:89e043f1d9d341c52bf2af6d02e6adde62e0a46e6755d5eb60dc6e4f0b8aeca2", size = 3806100 }, + { url = "https://files.pythonhosted.org/packages/99/f7/b73a431c8500565aa500e99e60b448d305eaf7c0b4c893c7c5a8a69cc595/lxml-5.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7b1cd427cb0d5f7393c31b7496419da594fe600e6fdc4b105a54f82405e6626c", size = 3925431 }, + { url = "https://files.pythonhosted.org/packages/db/48/4a206623c0d093d0e3b15f415ffb4345b0bdf661a3d0b15a112948c033c7/lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51806cfe0279e06ed8500ce19479d757db42a30fd509940b1701be9c86a5ff9a", size = 4216683 }, + { url = "https://files.pythonhosted.org/packages/54/47/577820c45dd954523ae8453b632d91e76da94ca6d9ee40d8c98dd86f916b/lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee70d08fd60c9565ba8190f41a46a54096afa0eeb8f76bd66f2c25d3b1b83005", size = 4326732 }, + { url = "https://files.pythonhosted.org/packages/68/de/96cb6d3269bc994b4f5ede8ca7bf0840f5de0a278bc6e50cb317ff71cafa/lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8dc2c0395bea8254d8daebc76dcf8eb3a95ec2a46fa6fae5eaccee366bfe02ce", size = 4218377 }, + { url = "https://files.pythonhosted.org/packages/a5/43/19b1ef6cbffa4244a217f95cc5f41a6cb4720fed33510a49670b03c5f1a0/lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6ba0d3dcac281aad8a0e5b14c7ed6f9fa89c8612b47939fc94f80b16e2e9bc83", size = 4351237 }, + { url = "https://files.pythonhosted.org/packages/ba/b2/6a22fb5c0885da3b00e116aee81f0b829ec9ac8f736cd414b4a09413fc7d/lxml-5.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:6e91cf736959057f7aac7adfc83481e03615a8e8dd5758aa1d95ea69e8931dba", size = 3487557 }, + { url = "https://files.pythonhosted.org/packages/c9/ac/e8ec7b6f7d76f8b88dfe78dd547b0d8915350160a5a01cca7aceba91e87f/lxml-5.3.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:68934b242c51eb02907c5b81d138cb977b2129a0a75a8f8b60b01cb8586c7b21", size = 3923032 }, + { url = "https://files.pythonhosted.org/packages/f7/b6/d94041c11aa294a09ffac7caa633114941935938eaaba159a93985283c07/lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b710bc2b8292966b23a6a0121f7a6c51d45d2347edcc75f016ac123b8054d3f2", size = 4214557 }, + { url = "https://files.pythonhosted.org/packages/dd/0d/ccb5e4e7a4188a9c881a3c07ee7eaf21772ae847ca5e9a3b140341f2668a/lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18feb4b93302091b1541221196a2155aa296c363fd233814fa11e181adebc52f", size = 4325217 }, + { url = "https://files.pythonhosted.org/packages/7a/17/9d3b43b63b0ddd77f1a680edf00de3c8c2441e8d379be17d2b712b67688b/lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3eb44520c4724c2e1a57c0af33a379eee41792595023f367ba3952a2d96c2aab", size = 4216018 }, + { url = "https://files.pythonhosted.org/packages/19/4f/f71029b3f37f43e846b6ec0d6baaa1791c65f8c3356cc78d18076f4c5422/lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:609251a0ca4770e5a8768ff902aa02bf636339c5a93f9349b48eb1f606f7f3e9", size = 4347893 }, + { url = "https://files.pythonhosted.org/packages/17/45/0fe53cb16a704b35b5ec93af305f77a14ec65830fc399e6634a81f17a1ea/lxml-5.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:516f491c834eb320d6c843156440fe7fc0d50b33e44387fcec5b02f0bc118a4c", size = 3486287 }, +] + +[[package]] +name = "markupsafe" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/d2/38ff920762f2247c3af5cbbbbc40756f575d9692d381d7c520f45deb9b8f/markupsafe-3.0.1.tar.gz", hash = "sha256:3e683ee4f5d0fa2dde4db77ed8dd8a876686e3fc417655c2ece9a90576905344", size = 20249 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/a2/0482d1a157f5f10f72fc4fe8c3be9ffa3651c1f7a12b60a3ab71b2635e13/MarkupSafe-3.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:db842712984e91707437461930e6011e60b39136c7331e971952bb30465bc1a1", size = 14391 }, + { url = "https://files.pythonhosted.org/packages/3b/25/5ea6500d200fd2dc3ea25c765f69dea0a1a8d42ec80a38cd896ad47cb85d/MarkupSafe-3.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3ffb4a8e7d46ed96ae48805746755fadd0909fea2306f93d5d8233ba23dda12a", size = 12414 }, + { url = "https://files.pythonhosted.org/packages/92/41/cf5397dd6bb18895d148aa402cafa71018f2ffc5f6e9d6e90d85b523c741/MarkupSafe-3.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67c519635a4f64e495c50e3107d9b4075aec33634272b5db1cde839e07367589", size = 21787 }, + { url = "https://files.pythonhosted.org/packages/2e/0d/5d91ef2b4f30afa87483a3a7c108c777d144b1c42d7113459296a8a2bfa0/MarkupSafe-3.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48488d999ed50ba8d38c581d67e496f955821dc183883550a6fbc7f1aefdc170", size = 20954 }, + { url = "https://files.pythonhosted.org/packages/f6/de/12a4110c2c7c7b502fe0e6f911367726dbb7a37e03e207495135d064bb48/MarkupSafe-3.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f31ae06f1328595d762c9a2bf29dafd8621c7d3adc130cbb46278079758779ca", size = 21086 }, + { url = "https://files.pythonhosted.org/packages/96/55/59389babc6e8ed206849a9958de9da7c23f3a75d294f46e99624fa38fb79/MarkupSafe-3.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80fcbf3add8790caddfab6764bde258b5d09aefbe9169c183f88a7410f0f6dea", size = 21685 }, + { url = "https://files.pythonhosted.org/packages/3d/cb/cbad5f093e12cd79ceea3e2957ba5bd4c2706810f333d0a3422ab2aef358/MarkupSafe-3.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3341c043c37d78cc5ae6e3e305e988532b072329639007fd408a476642a89fd6", size = 21348 }, + { url = "https://files.pythonhosted.org/packages/8e/70/e19c4f39d68a52406012ee118667b57efb0bbe6e950be21187cd7a1b4b80/MarkupSafe-3.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cb53e2a99df28eee3b5f4fea166020d3ef9116fdc5764bc5117486e6d1211b25", size = 21098 }, + { url = "https://files.pythonhosted.org/packages/30/95/ca809c01624428d427e9b3a4500f9068eca941e0c520328954ce84ad966a/MarkupSafe-3.0.1-cp310-cp310-win32.whl", hash = "sha256:db15ce28e1e127a0013dfb8ac243a8e392db8c61eae113337536edb28bdc1f97", size = 15075 }, + { url = "https://files.pythonhosted.org/packages/23/41/decb99ab07793656821a86f827a394700ce28402ebb02dc6d003210d9859/MarkupSafe-3.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:4ffaaac913c3f7345579db4f33b0020db693f302ca5137f106060316761beea9", size = 15535 }, + { url = "https://files.pythonhosted.org/packages/ce/af/2f5d88a7fc7226bd34c6e15f6061246ad8cff979da9f19d11bdd0addd8e2/MarkupSafe-3.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:26627785a54a947f6d7336ce5963569b5d75614619e75193bdb4e06e21d447ad", size = 14387 }, + { url = "https://files.pythonhosted.org/packages/8d/43/fd588ef5d192308c5e05974bac659bf6ae29c202b7ea2c4194bcf01eacee/MarkupSafe-3.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b954093679d5750495725ea6f88409946d69cfb25ea7b4c846eef5044194f583", size = 12410 }, + { url = "https://files.pythonhosted.org/packages/58/26/78f161d602fb03804118905e5faacafc0ec592bbad71aaee62537529813a/MarkupSafe-3.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:973a371a55ce9ed333a3a0f8e0bcfae9e0d637711534bcb11e130af2ab9334e7", size = 24006 }, + { url = "https://files.pythonhosted.org/packages/ae/1d/7d5ec8bcfd9c2db235d720fa51d818b7e2abc45250ce5f53dd6cb60409ca/MarkupSafe-3.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:244dbe463d5fb6d7ce161301a03a6fe744dac9072328ba9fc82289238582697b", size = 23303 }, + { url = "https://files.pythonhosted.org/packages/26/ce/703ca3b03a709e3bd1fbffa407789e56b9fa664456538092617dd665fc1d/MarkupSafe-3.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d98e66a24497637dd31ccab090b34392dddb1f2f811c4b4cd80c230205c074a3", size = 23205 }, + { url = "https://files.pythonhosted.org/packages/88/60/40be0493decabc2344b12d3a709fd6ccdd15a5ebaee1e8d878315d107ad3/MarkupSafe-3.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ad91738f14eb8da0ff82f2acd0098b6257621410dcbd4df20aaa5b4233d75a50", size = 23684 }, + { url = "https://files.pythonhosted.org/packages/6d/f8/8fd52a66e8f62a9add62b4a0b5a3ab4092027437f2ef027f812d94ae91cf/MarkupSafe-3.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7044312a928a66a4c2a22644147bc61a199c1709712069a344a3fb5cfcf16915", size = 23472 }, + { url = "https://files.pythonhosted.org/packages/d4/0b/998b17b9e06ea45ad1646fea586f1b83d02dfdb14d47dd2fd81fba5a08c9/MarkupSafe-3.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a4792d3b3a6dfafefdf8e937f14906a51bd27025a36f4b188728a73382231d91", size = 23388 }, + { url = "https://files.pythonhosted.org/packages/5a/57/b6b7aa23b2e26d68d601718f8ce3161fbdaf967b31752c7dec52bef828c9/MarkupSafe-3.0.1-cp311-cp311-win32.whl", hash = "sha256:fa7d686ed9883f3d664d39d5a8e74d3c5f63e603c2e3ff0abcba23eac6542635", size = 15106 }, + { url = "https://files.pythonhosted.org/packages/fc/b5/20cb1d714596acb553c810009c8004c809823947da63e13c19a7decfcb6c/MarkupSafe-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:9ba25a71ebf05b9bb0e2ae99f8bc08a07ee8e98c612175087112656ca0f5c8bf", size = 15542 }, + { url = "https://files.pythonhosted.org/packages/45/6d/72ed58d42a12bd9fc288dbff6dd8d03ea973a232ac0538d7f88d105b5251/MarkupSafe-3.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8ae369e84466aa70f3154ee23c1451fda10a8ee1b63923ce76667e3077f2b0c4", size = 14322 }, + { url = "https://files.pythonhosted.org/packages/86/f5/241238f89cdd6461ac9f521af8389f9a48fab97e4f315c69e9e0d52bc919/MarkupSafe-3.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40f1e10d51c92859765522cbd79c5c8989f40f0419614bcdc5015e7b6bf97fc5", size = 12380 }, + { url = "https://files.pythonhosted.org/packages/27/94/79751928bca5841416d8ca02e22198672e021d5c7120338e2a6e3771f8fc/MarkupSafe-3.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a4cb365cb49b750bdb60b846b0c0bc49ed62e59a76635095a179d440540c346", size = 24099 }, + { url = "https://files.pythonhosted.org/packages/10/6e/1b8070bbfc467429c7983cd5ffd4ec57e1d501763d974c7caaa0a9a79f4c/MarkupSafe-3.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee3941769bd2522fe39222206f6dd97ae83c442a94c90f2b7a25d847d40f4729", size = 23249 }, + { url = "https://files.pythonhosted.org/packages/66/50/9389ae6cdff78d7481a2a2641830b5eb1d1f62177550e73355a810a889c9/MarkupSafe-3.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62fada2c942702ef8952754abfc1a9f7658a4d5460fabe95ac7ec2cbe0d02abc", size = 23149 }, + { url = "https://files.pythonhosted.org/packages/16/02/5dddff5366fde47133186efb847fa88bddef85914bbe623e25cfeccb3517/MarkupSafe-3.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c2d64fdba74ad16138300815cfdc6ab2f4647e23ced81f59e940d7d4a1469d9", size = 23864 }, + { url = "https://files.pythonhosted.org/packages/f3/f1/700ee6655561cfda986e03f7afc309e3738918551afa7dedd99225586227/MarkupSafe-3.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fb532dd9900381d2e8f48172ddc5a59db4c445a11b9fab40b3b786da40d3b56b", size = 23440 }, + { url = "https://files.pythonhosted.org/packages/fb/3e/d26623ac7f16709823b4c80e0b4a1c9196eeb46182a6c1d47b5e0c8434f4/MarkupSafe-3.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0f84af7e813784feb4d5e4ff7db633aba6c8ca64a833f61d8e4eade234ef0c38", size = 23610 }, + { url = "https://files.pythonhosted.org/packages/51/04/1f8da0810c39cb9fcff96b6baed62272c97065e9cf11471965a161439e20/MarkupSafe-3.0.1-cp312-cp312-win32.whl", hash = "sha256:cbf445eb5628981a80f54087f9acdbf84f9b7d862756110d172993b9a5ae81aa", size = 15113 }, + { url = "https://files.pythonhosted.org/packages/eb/24/a36dc37365bdd358b1e583cc40475593e36ab02cb7da6b3d0b9c05b0da7a/MarkupSafe-3.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:a10860e00ded1dd0a65b83e717af28845bb7bd16d8ace40fe5531491de76b79f", size = 15611 }, + { url = "https://files.pythonhosted.org/packages/b1/60/4572a8aa1beccbc24b133aa0670781a5d2697f4fa3fecf0a87b46383174b/MarkupSafe-3.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e81c52638315ff4ac1b533d427f50bc0afc746deb949210bc85f05d4f15fd772", size = 14325 }, + { url = "https://files.pythonhosted.org/packages/38/42/849915b99a765ec104bfd07ee933de5fc9c58fa9570efa7db81717f495d8/MarkupSafe-3.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:312387403cd40699ab91d50735ea7a507b788091c416dd007eac54434aee51da", size = 12373 }, + { url = "https://files.pythonhosted.org/packages/ef/82/4caaebd963c6d60b28e4445f38841d24f8b49bc10594a09956c9d73bfc08/MarkupSafe-3.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ae99f31f47d849758a687102afdd05bd3d3ff7dbab0a8f1587981b58a76152a", size = 24059 }, + { url = "https://files.pythonhosted.org/packages/20/15/6b319be2f79fcfa3173f479d69f4e950b5c9b642db4f22cf73ae5ade745f/MarkupSafe-3.0.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c97ff7fedf56d86bae92fa0a646ce1a0ec7509a7578e1ed238731ba13aabcd1c", size = 23211 }, + { url = "https://files.pythonhosted.org/packages/9d/3f/8963bdf4962feb2154475acb7dc350f04217b5e0be7763a39b432291e229/MarkupSafe-3.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7420ceda262dbb4b8d839a4ec63d61c261e4e77677ed7c66c99f4e7cb5030dd", size = 23095 }, + { url = "https://files.pythonhosted.org/packages/af/93/f770bc70953d32de0c6ce4bcb76271512123a1ead91aaef625a020c5bfaf/MarkupSafe-3.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45d42d132cff577c92bfba536aefcfea7e26efb975bd455db4e6602f5c9f45e7", size = 23901 }, + { url = "https://files.pythonhosted.org/packages/11/92/1e5a33aa0a1190161238628fb68eb1bc5e67b56a5c89f0636328704b463a/MarkupSafe-3.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4c8817557d0de9349109acb38b9dd570b03cc5014e8aabf1cbddc6e81005becd", size = 23463 }, + { url = "https://files.pythonhosted.org/packages/0d/fe/657efdfe385d2a3a701f2c4fcc9577c63c438aeefdd642d0d956c4ecd225/MarkupSafe-3.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6a54c43d3ec4cf2a39f4387ad044221c66a376e58c0d0e971d47c475ba79c6b5", size = 23569 }, + { url = "https://files.pythonhosted.org/packages/cf/24/587dea40304046ace60f846cedaebc0d33d967a3ce46c11395a10e7a78ba/MarkupSafe-3.0.1-cp313-cp313-win32.whl", hash = "sha256:c91b394f7601438ff79a4b93d16be92f216adb57d813a78be4446fe0f6bc2d8c", size = 15117 }, + { url = "https://files.pythonhosted.org/packages/32/8f/d8961d633f26a011b4fe054f3bfff52f673423b8c431553268741dfb089e/MarkupSafe-3.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:fe32482b37b4b00c7a52a07211b479653b7fe4f22b2e481b9a9b099d8a430f2f", size = 15613 }, + { url = "https://files.pythonhosted.org/packages/9e/93/d6367ffbcd0c5c371370767f768eaa32af60bc411245b8517e383c6a2b12/MarkupSafe-3.0.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:17b2aea42a7280db02ac644db1d634ad47dcc96faf38ab304fe26ba2680d359a", size = 14563 }, + { url = "https://files.pythonhosted.org/packages/4a/37/f813c3835747dec08fe19ac9b9eced01fdf93a4b3e626521675dc7f423a9/MarkupSafe-3.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:852dc840f6d7c985603e60b5deaae1d89c56cb038b577f6b5b8c808c97580f1d", size = 12505 }, + { url = "https://files.pythonhosted.org/packages/72/bf/800b4d1580298ca91ccd6c95915bbd147142dad1b8cf91d57b93b28670dd/MarkupSafe-3.0.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0778de17cff1acaeccc3ff30cd99a3fd5c50fc58ad3d6c0e0c4c58092b859396", size = 25358 }, + { url = "https://files.pythonhosted.org/packages/fd/78/26e209abc8f0a379f031f0acc151231974e5b153d7eda5759d17d8f329f2/MarkupSafe-3.0.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:800100d45176652ded796134277ecb13640c1a537cad3b8b53da45aa96330453", size = 23797 }, + { url = "https://files.pythonhosted.org/packages/09/e1/918496a9390891756efee818880e71c1bbaf587f4dc8ede3f3852357310a/MarkupSafe-3.0.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d06b24c686a34c86c8c1fba923181eae6b10565e4d80bdd7bc1c8e2f11247aa4", size = 23743 }, + { url = "https://files.pythonhosted.org/packages/cd/c6/26f576cd58d6c2decd9045e4e3f3c5dbc01ea6cb710916e7bbb6ebd95b6b/MarkupSafe-3.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:33d1c36b90e570ba7785dacd1faaf091203d9942bc036118fab8110a401eb1a8", size = 25076 }, + { url = "https://files.pythonhosted.org/packages/b5/fa/10b24fb3b0e15fe5389dc88ecc6226ede08297e0ba7130610efbe0cdfb27/MarkupSafe-3.0.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:beeebf760a9c1f4c07ef6a53465e8cfa776ea6a2021eda0d0417ec41043fe984", size = 24037 }, + { url = "https://files.pythonhosted.org/packages/c8/81/4b3f5537d9f6cc4f5c80d6c4b78af9a5247fd37b5aba95807b2cbc336b9a/MarkupSafe-3.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bbde71a705f8e9e4c3e9e33db69341d040c827c7afa6789b14c6e16776074f5a", size = 24015 }, + { url = "https://files.pythonhosted.org/packages/5f/07/8e8dcecd53216c5e01a51e84c32a2bce166690ed19c184774b38cd41921d/MarkupSafe-3.0.1-cp313-cp313t-win32.whl", hash = "sha256:82b5dba6eb1bcc29cc305a18a3c5365d2af06ee71b123216416f7e20d2a84e5b", size = 15213 }, + { url = "https://files.pythonhosted.org/packages/0d/87/4c364e0f109eea2402079abecbe33fef4f347b551a11423d1f4e187ea497/MarkupSafe-3.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:730d86af59e0e43ce277bb83970530dd223bf7f2a838e086b50affa6ec5f9295", size = 15741 }, + { url = "https://files.pythonhosted.org/packages/6f/4f/420741fb39fa3d40396fb1731a1ca78e6f9fbb225dcf15e5185b1fa954bc/MarkupSafe-3.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4935dd7883f1d50e2ffecca0aa33dc1946a94c8f3fdafb8df5c330e48f71b132", size = 14376 }, + { url = "https://files.pythonhosted.org/packages/91/71/0c4782b9ce7fb68b140b94e1eb9d2b6292990bda91dc3d3b5a34e8bd41f3/MarkupSafe-3.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e9393357f19954248b00bed7c56f29a25c930593a77630c719653d51e7669c2a", size = 12408 }, + { url = "https://files.pythonhosted.org/packages/3e/3c/cbf30bf7ac1da2e013e3d338e1582db85fc3b27bf9f8863137423ad4b0b6/MarkupSafe-3.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40621d60d0e58aa573b68ac5e2d6b20d44392878e0bfc159012a5787c4e35bc8", size = 21654 }, + { url = "https://files.pythonhosted.org/packages/0b/28/229e797b8727427845b79cbd58019f598e478f974730fa705fa23904b18e/MarkupSafe-3.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f94190df587738280d544971500b9cafc9b950d32efcb1fba9ac10d84e6aa4e6", size = 20817 }, + { url = "https://files.pythonhosted.org/packages/e8/b4/1121f3b2614de93cbb3deec7f44df283df44c2258ea9368bb1302b4a0b45/MarkupSafe-3.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6a387d61fe41cdf7ea95b38e9af11cfb1a63499af2759444b99185c4ab33f5b", size = 20956 }, + { url = "https://files.pythonhosted.org/packages/a8/8b/b4d57bafca01c8b1e1fbb037660869fa4f6725983c4105a02bd1242f0066/MarkupSafe-3.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8ad4ad1429cd4f315f32ef263c1342166695fad76c100c5d979c45d5570ed58b", size = 21548 }, + { url = "https://files.pythonhosted.org/packages/83/87/04806f7096ba1d4f1b8c61f35c1d7c0b507c6a3cf7ed495393bf97eb5af7/MarkupSafe-3.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e24bfe89c6ac4c31792793ad9f861b8f6dc4546ac6dc8f1c9083c7c4f2b335cd", size = 21222 }, + { url = "https://files.pythonhosted.org/packages/e9/96/1ecb2bb5ee7298e628cff95833beba7da6a774df7fe890a6d2f0ec460590/MarkupSafe-3.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2a4b34a8d14649315c4bc26bbfa352663eb51d146e35eef231dd739d54a5430a", size = 20952 }, + { url = "https://files.pythonhosted.org/packages/fd/70/b937a12df7bbff14e1ca3385929f464c7af2ca72c8183c95dad26c3bf754/MarkupSafe-3.0.1-cp39-cp39-win32.whl", hash = "sha256:242d6860f1fd9191aef5fae22b51c5c19767f93fb9ead4d21924e0bcb17619d8", size = 15075 }, + { url = "https://files.pythonhosted.org/packages/e3/c4/262fac0328552da9a75a7786d7c0f43adaba4afb5f295979d33fa0f324c7/MarkupSafe-3.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:93e8248d650e7e9d49e8251f883eed60ecbc0e8ffd6349e18550925e31bd029b", size = 15527 }, +] + +[[package]] +name = "matplotlib" +version = "3.9.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "importlib-resources", marker = "python_full_version < '3.10'" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/d8/3d7f706c69e024d4287c1110d74f7dabac91d9843b99eadc90de9efc8869/matplotlib-3.9.2.tar.gz", hash = "sha256:96ab43906269ca64a6366934106fa01534454a69e471b7bf3d79083981aaab92", size = 36088381 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/9d/84eeb82ecdd3ba71b12dd6ab5c820c5cc1e868003ecb3717d41b589ec02a/matplotlib-3.9.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:9d78bbc0cbc891ad55b4f39a48c22182e9bdaea7fc0e5dbd364f49f729ca1bbb", size = 7893310 }, + { url = "https://files.pythonhosted.org/packages/36/98/cbacbd30241369d099f9c13a2b6bc3b7068d85214f5b5795e583ac3d8aba/matplotlib-3.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c375cc72229614632c87355366bdf2570c2dac01ac66b8ad048d2dabadf2d0d4", size = 7764089 }, + { url = "https://files.pythonhosted.org/packages/a8/a0/917f3c6d3a8774a3a1502d9f3dfc1456e07c1fa0c211a23b75a69e154180/matplotlib-3.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d94ff717eb2bd0b58fe66380bd8b14ac35f48a98e7c6765117fe67fb7684e64", size = 8192377 }, + { url = "https://files.pythonhosted.org/packages/8d/9d/d06860390f9d154fa884f1740a5456378fb153ff57443c91a4a32bab7092/matplotlib-3.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab68d50c06938ef28681073327795c5db99bb4666214d2d5f880ed11aeaded66", size = 8303983 }, + { url = "https://files.pythonhosted.org/packages/9e/a7/c0e848ed7de0766c605af62d8097472a37f1a81d93e9afe94faa5890f24d/matplotlib-3.9.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:65aacf95b62272d568044531e41de26285d54aec8cb859031f511f84bd8b495a", size = 9083318 }, + { url = "https://files.pythonhosted.org/packages/09/6c/0fa50c001340a45cde44853c116d6551aea741e59a7261c38f473b53553b/matplotlib-3.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:3fd595f34aa8a55b7fc8bf9ebea8aa665a84c82d275190a61118d33fbc82ccae", size = 7819628 }, + { url = "https://files.pythonhosted.org/packages/77/c2/f9d7fe80a8fcce9bb128d1381c6fe41a8d286d7e18395e273002e8e0fa34/matplotlib-3.9.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d8dd059447824eec055e829258ab092b56bb0579fc3164fa09c64f3acd478772", size = 7902925 }, + { url = "https://files.pythonhosted.org/packages/28/ba/8be09886eb56ac04a218a1dc3fa728a5c4cac60b019b4f1687885166da00/matplotlib-3.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c797dac8bb9c7a3fd3382b16fe8f215b4cf0f22adccea36f1545a6d7be310b41", size = 7773193 }, + { url = "https://files.pythonhosted.org/packages/e6/9a/5991972a560db3ab621312a7ca5efec339ae2122f25901c0846865c4b72f/matplotlib-3.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d719465db13267bcef19ea8954a971db03b9f48b4647e3860e4bc8e6ed86610f", size = 8202378 }, + { url = "https://files.pythonhosted.org/packages/01/75/6c7ce560e95714a10fcbb3367d1304975a1a3e620f72af28921b796403f3/matplotlib-3.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8912ef7c2362f7193b5819d17dae8629b34a95c58603d781329712ada83f9447", size = 8314361 }, + { url = "https://files.pythonhosted.org/packages/6e/49/dc7384c6c092958e0b75e754efbd9e52500154939c3d715789cee9fb8a53/matplotlib-3.9.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7741f26a58a240f43bee74965c4882b6c93df3e7eb3de160126d8c8f53a6ae6e", size = 9091428 }, + { url = "https://files.pythonhosted.org/packages/8b/ce/15b0bb2fb29b3d46211d8ca740b96b5232499fc49200b58b8d571292c9a6/matplotlib-3.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:ae82a14dab96fbfad7965403c643cafe6515e386de723e498cf3eeb1e0b70cc7", size = 7829377 }, + { url = "https://files.pythonhosted.org/packages/82/de/54f7f38ce6de79cb77d513bb3eaa4e0b1031e9fd6022214f47943fa53a88/matplotlib-3.9.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ac43031375a65c3196bee99f6001e7fa5bdfb00ddf43379d3c0609bdca042df9", size = 7892511 }, + { url = "https://files.pythonhosted.org/packages/35/3e/5713b84a02b24b2a4bd4d6673bfc03017e6654e1d8793ece783b7ed4d484/matplotlib-3.9.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:be0fc24a5e4531ae4d8e858a1a548c1fe33b176bb13eff7f9d0d38ce5112a27d", size = 7769370 }, + { url = "https://files.pythonhosted.org/packages/5b/bd/c404502aa1824456d2862dd6b9b0c1917761a51a32f7f83ff8cf94b6d117/matplotlib-3.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf81de2926c2db243c9b2cbc3917619a0fc85796c6ba4e58f541df814bbf83c7", size = 8193260 }, + { url = "https://files.pythonhosted.org/packages/27/75/de5b9cd67648051cae40039da0c8cbc497a0d99acb1a1f3d087cd66d27b7/matplotlib-3.9.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6ee45bc4245533111ced13f1f2cace1e7f89d1c793390392a80c139d6cf0e6c", size = 8306310 }, + { url = "https://files.pythonhosted.org/packages/de/e3/2976e4e54d7ee76eaf54b7639fdc10a223d05c2bdded7045233e9871e469/matplotlib-3.9.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:306c8dfc73239f0e72ac50e5a9cf19cc4e8e331dd0c54f5e69ca8758550f1e1e", size = 9086717 }, + { url = "https://files.pythonhosted.org/packages/d2/92/c2b9464a0562feb6ae780bdc152364810862e07ef5e6affa2b7686028db2/matplotlib-3.9.2-cp312-cp312-win_amd64.whl", hash = "sha256:5413401594cfaff0052f9d8b1aafc6d305b4bd7c4331dccd18f561ff7e1d3bd3", size = 7832805 }, + { url = "https://files.pythonhosted.org/packages/5c/7f/8932eac316b32f464b8f9069f151294dcd892c8fbde61fe8bcd7ba7f7f7e/matplotlib-3.9.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:18128cc08f0d3cfff10b76baa2f296fc28c4607368a8402de61bb3f2eb33c7d9", size = 7893012 }, + { url = "https://files.pythonhosted.org/packages/90/89/9db9db3dd0ff3e2c49e452236dfe29e60b5586a88f8928ca1d153d0da8b5/matplotlib-3.9.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4876d7d40219e8ae8bb70f9263bcbe5714415acfdf781086601211335e24f8aa", size = 7769810 }, + { url = "https://files.pythonhosted.org/packages/67/26/d2661cdc2e1410b8929c5f12dfd521e4528abfed1b3c3d5a28ac48258b43/matplotlib-3.9.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d9f07a80deab4bb0b82858a9e9ad53d1382fd122be8cde11080f4e7dfedb38b", size = 8193779 }, + { url = "https://files.pythonhosted.org/packages/95/70/4839eaa672bf4eacc98ebc8d23633e02b6daf39e294e7433c4ab11a689be/matplotlib-3.9.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7c0410f181a531ec4e93bbc27692f2c71a15c2da16766f5ba9761e7ae518413", size = 8306260 }, + { url = "https://files.pythonhosted.org/packages/88/62/7b263b2cb2724b45d3a4f9c8c6137696cc3ef037d44383fb01ac2a9555c2/matplotlib-3.9.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:909645cce2dc28b735674ce0931a4ac94e12f5b13f6bb0b5a5e65e7cea2c192b", size = 9086073 }, + { url = "https://files.pythonhosted.org/packages/b0/6d/3572fe243c74112fef120f0bc86f5edd21f49b60e8322fc7f6a01fe945dd/matplotlib-3.9.2-cp313-cp313-win_amd64.whl", hash = "sha256:f32c7410c7f246838a77d6d1eff0c0f87f3cb0e7c4247aebea71a6d5a68cab49", size = 7833041 }, + { url = "https://files.pythonhosted.org/packages/03/8f/9d505be3eb2f40ec731674fb6b47d10cc3147bbd6a9ea7a08c8da55415c6/matplotlib-3.9.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:37e51dd1c2db16ede9cfd7b5cabdfc818b2c6397c83f8b10e0e797501c963a03", size = 7933657 }, + { url = "https://files.pythonhosted.org/packages/5d/68/44b458b9794bcff2a66921f8c9a8110a50a0bb099bd5f7cabb428a1dc765/matplotlib-3.9.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b82c5045cebcecd8496a4d694d43f9cc84aeeb49fe2133e036b207abe73f4d30", size = 7799276 }, + { url = "https://files.pythonhosted.org/packages/47/79/8486d4ddcaaf676314b5fb58e8fe19d1a6210a443a7c31fa72d4215fcb87/matplotlib-3.9.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f053c40f94bc51bc03832a41b4f153d83f2062d88c72b5e79997072594e97e51", size = 8221027 }, + { url = "https://files.pythonhosted.org/packages/56/62/72a472181578c3d035dcda0d0fa2e259ba2c4cb91132588a348bb705b70d/matplotlib-3.9.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbe196377a8248972f5cede786d4c5508ed5f5ca4a1e09b44bda889958b33f8c", size = 8329097 }, + { url = "https://files.pythonhosted.org/packages/01/8a/760f7fce66b39f447ad160800619d0bd5d0936d2b4633587116534a4afe0/matplotlib-3.9.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5816b1e1fe8c192cbc013f8f3e3368ac56fbecf02fb41b8f8559303f24c5015e", size = 9093770 }, + { url = "https://files.pythonhosted.org/packages/33/d8/87456eed8fa93db0d32b429dca067d798617698a5d6c2b6f8b2b898fd61f/matplotlib-3.9.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:cef2a73d06601437be399908cf13aee74e86932a5ccc6ccdf173408ebc5f6bb2", size = 7894246 }, + { url = "https://files.pythonhosted.org/packages/46/87/5f567fda78130a8394f9dcf3accb1b7b0c9baf0384307ef59032f5b1d17c/matplotlib-3.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e0830e188029c14e891fadd99702fd90d317df294c3298aad682739c5533721a", size = 7764962 }, + { url = "https://files.pythonhosted.org/packages/9e/ee/cfbfd294d33ad19f7bbf8188d26f2f7705283b750df80bf54b7be9a04cf2/matplotlib-3.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ba9c1299c920964e8d3857ba27173b4dbb51ca4bab47ffc2c2ba0eb5e2cbc5", size = 8194080 }, + { url = "https://files.pythonhosted.org/packages/5a/20/f56d5b88450593ccde3f283e338f3f976b2e479bddd9a147f14f66ee1ca7/matplotlib-3.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1cd93b91ab47a3616b4d3c42b52f8363b88ca021e340804c6ab2536344fad9ca", size = 8306293 }, + { url = "https://files.pythonhosted.org/packages/3d/db/332effcb9779231017e45cc581b280979c717a84202a638f9301da86ab29/matplotlib-3.9.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6d1ce5ed2aefcdce11904fc5bbea7d9c21fff3d5f543841edf3dea84451a09ea", size = 9085520 }, + { url = "https://files.pythonhosted.org/packages/71/a8/9b18bd1fef16f71821c890b4db3697be5102f2b839765d9608479cd33874/matplotlib-3.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:b2696efdc08648536efd4e1601b5fd491fd47f4db97a5fbfd175549a7365c1b2", size = 7813909 }, + { url = "https://files.pythonhosted.org/packages/54/c1/3fc6cad8a7caa05f4b24fb52372de87a736afeccaa9c576e4748df44067b/matplotlib-3.9.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d52a3b618cb1cbb769ce2ee1dcdb333c3ab6e823944e9a2d36e37253815f9556", size = 7885311 }, + { url = "https://files.pythonhosted.org/packages/1c/6f/4e59c032b6f28cc7344f34e14ff247ebf6c975d91784bca22b9512bf43d1/matplotlib-3.9.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:039082812cacd6c6bec8e17a9c1e6baca230d4116d522e81e1f63a74d01d2e21", size = 7762487 }, + { url = "https://files.pythonhosted.org/packages/72/b0/194c61ab2f40a4a685ef01a91c891cd44298871da4e79654494dc00bd56f/matplotlib-3.9.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6758baae2ed64f2331d4fd19be38b7b4eae3ecec210049a26b6a4f3ae1c85dcc", size = 8305689 }, + { url = "https://files.pythonhosted.org/packages/c0/e8/a69f4ad5b544f509ec3718dfa003187a94a37d79bf2e175180668c0ff8ec/matplotlib-3.9.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:050598c2b29e0b9832cde72bcf97627bf00262adbc4a54e2b856426bb2ef0697", size = 7845515 }, +] + +[[package]] +name = "matplotlib-inline" +version = "0.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/5b/a36a337438a14116b16480db471ad061c36c3694df7c2084a0da7ba538b7/matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", size = 8159 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899 }, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350 }, +] + +[[package]] +name = "mistune" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/c8/f0173fe3bf85fd891aee2e7bcd8207dfe26c2c683d727c5a6cc3aec7b628/mistune-3.0.2.tar.gz", hash = "sha256:fc7f93ded930c92394ef2cb6f04a8aabab4117a91449e72dcc8dfa646a508be8", size = 90840 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/74/c95adcdf032956d9ef6c89a9b8a5152bf73915f8c633f3e3d88d06bd699c/mistune-3.0.2-py3-none-any.whl", hash = "sha256:71481854c30fdbc938963d3605b72501f5c10a9320ecd412c121c163a1c7d205", size = 47958 }, +] + +[[package]] +name = "mokapot" +version = "0.10.1.dev25+ga4d04d0.d20241009" +source = { editable = "." } +dependencies = [ + { name = "filelock" }, + { name = "importlib-metadata" }, + { name = "joblib" }, + { name = "lxml" }, + { name = "matplotlib" }, + { name = "numba" }, + { name = "numpy" }, + { name = "pandas" }, + { name = "pyarrow" }, + { name = "scikit-learn" }, + { name = "scipy" }, + { name = "triqler" }, + { name = "typeguard" }, +] + +[package.optional-dependencies] +docs = [ + { name = "ipykernel" }, + { name = "nbsphinx" }, + { name = "numpydoc" }, + { name = "recommonmark" }, + { name = "sphinx-argparse" }, + { name = "sphinx-rtd-theme" }, +] + +[package.dev-dependencies] +dev = [ + { name = "flake8" }, + { name = "pre-commit" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "ruff" }, + { name = "wheel" }, +] + +[package.metadata] +requires-dist = [ + { name = "filelock", specifier = ">=3.16.1" }, + { name = "importlib-metadata", specifier = ">=5.1.0" }, + { name = "ipykernel", marker = "extra == 'docs'", specifier = ">=5.3.0" }, + { name = "joblib", specifier = ">=1.1.0" }, + { name = "lxml", specifier = ">=4.6.2" }, + { name = "matplotlib", specifier = ">=3.1.3" }, + { name = "nbsphinx", marker = "extra == 'docs'", specifier = ">=0.7.1" }, + { name = "numba", specifier = ">=0.48.0" }, + { name = "numpy", specifier = ">=1.18.1" }, + { name = "numpydoc", marker = "extra == 'docs'", specifier = ">=1.0.0" }, + { name = "pandas", specifier = ">=1.0.3" }, + { name = "pyarrow", specifier = ">=15.0.0" }, + { name = "recommonmark", marker = "extra == 'docs'", specifier = ">=0.5.0" }, + { name = "scikit-learn", specifier = ">=0.22.1" }, + { name = "scipy", specifier = ">=1.13.0" }, + { name = "sphinx-argparse", marker = "extra == 'docs'", specifier = ">=0.2.5" }, + { name = "sphinx-rtd-theme", marker = "extra == 'docs'", specifier = ">=0.5.0" }, + { name = "triqler", specifier = ">=0.6.2" }, + { name = "typeguard", specifier = ">=4.1.5" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "flake8", specifier = ">=7.1.1" }, + { name = "pre-commit", specifier = ">=2.7.1" }, + { name = "pytest", specifier = ">=8.2.2" }, + { name = "pytest-cov", specifier = ">=5.0.0" }, + { name = "ruff", specifier = ">=0.4.4" }, + { name = "wheel", specifier = ">=0.44.0" }, +] + +[[package]] +name = "nbclient" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "nbformat" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e2/d2/39bc36604f24bccd44d374ac34769bc58c53a1da5acd1e83f0165aa4940e/nbclient-0.10.0.tar.gz", hash = "sha256:4b3f1b7dba531e498449c4db4f53da339c91d449dc11e9af3a43b4eb5c5abb09", size = 62246 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/e8/00517a23d3eeaed0513e718fbc94aab26eaa1758f5690fc8578839791c79/nbclient-0.10.0-py3-none-any.whl", hash = "sha256:f13e3529332a1f1f81d82a53210322476a168bb7090a0289c795fe9cc11c9d3f", size = 25318 }, +] + +[[package]] +name = "nbconvert" +version = "7.16.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "bleach" }, + { name = "defusedxml" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "jinja2" }, + { name = "jupyter-core" }, + { name = "jupyterlab-pygments" }, + { name = "markupsafe" }, + { name = "mistune" }, + { name = "nbclient" }, + { name = "nbformat" }, + { name = "packaging" }, + { name = "pandocfilters" }, + { name = "pygments" }, + { name = "tinycss2" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/e8/ba521a033b21132008e520c28ceb818f9f092da5f0261e94e509401b29f9/nbconvert-7.16.4.tar.gz", hash = "sha256:86ca91ba266b0a448dc96fa6c5b9d98affabde2867b363258703536807f9f7f4", size = 854422 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/bb/bb5b6a515d1584aa2fd89965b11db6632e4bdc69495a52374bcc36e56cfa/nbconvert-7.16.4-py3-none-any.whl", hash = "sha256:05873c620fe520b6322bf8a5ad562692343fe3452abda5765c7a34b7d1aa3eb3", size = 257388 }, +] + +[[package]] +name = "nbformat" +version = "5.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastjsonschema" }, + { name = "jsonschema" }, + { name = "jupyter-core" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/fd/91545e604bc3dad7dca9ed03284086039b294c6b3d75c0d2fa45f9e9caf3/nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a", size = 142749 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b", size = 78454 }, +] + +[[package]] +name = "nbsphinx" +version = "0.9.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "jinja2" }, + { name = "nbconvert" }, + { name = "nbformat" }, + { name = "sphinx" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/49/a6b1ed510bcc7734daa20372222804d6109d3087ced82f65c8720da90ef4/nbsphinx-0.9.5.tar.gz", hash = "sha256:736916e7b0dab28fc904f4a9ae3b53a9a50c29fccc6329c052fcc7485abcf2b7", size = 179599 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/8a/5a1e56efa95e2038de5646e2bc5c0abe18678ae5d167e267c0fbaa17a372/nbsphinx-0.9.5-py3-none-any.whl", hash = "sha256:d82f71084425db1f48e72515f15c25b4de8652ceaab513ee462ac05f1b8eae0a", size = 31349 }, +] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195 }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, +] + +[[package]] +name = "numba" +version = "0.60.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llvmlite" }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3c/93/2849300a9184775ba274aba6f82f303343669b0592b7bb0849ea713dabb0/numba-0.60.0.tar.gz", hash = "sha256:5df6158e5584eece5fc83294b949fd30b9f1125df7708862205217e068aabf16", size = 2702171 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/cf/baa13a7e3556d73d9e38021e6d6aa4aeb30d8b94545aa8b70d0f24a1ccc4/numba-0.60.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d761de835cd38fb400d2c26bb103a2726f548dc30368853121d66201672e651", size = 2647627 }, + { url = "https://files.pythonhosted.org/packages/ac/ba/4b57fa498564457c3cc9fc9e570a6b08e6086c74220f24baaf04e54b995f/numba-0.60.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:159e618ef213fba758837f9837fb402bbe65326e60ba0633dbe6c7f274d42c1b", size = 2650322 }, + { url = "https://files.pythonhosted.org/packages/28/98/7ea97ee75870a54f938a8c70f7e0be4495ba5349c5f9db09d467c4a5d5b7/numba-0.60.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1527dc578b95c7c4ff248792ec33d097ba6bef9eda466c948b68dfc995c25781", size = 3407390 }, + { url = "https://files.pythonhosted.org/packages/79/58/cb4ac5b8f7ec64200460aef1fed88258fb872ceef504ab1f989d2ff0f684/numba-0.60.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe0b28abb8d70f8160798f4de9d486143200f34458d34c4a214114e445d7124e", size = 3699694 }, + { url = "https://files.pythonhosted.org/packages/1c/b0/c61a93ca947d12233ff45de506ddbf52af3f752066a0b8be4d27426e16da/numba-0.60.0-cp310-cp310-win_amd64.whl", hash = "sha256:19407ced081d7e2e4b8d8c36aa57b7452e0283871c296e12d798852bc7d7f198", size = 2687030 }, + { url = "https://files.pythonhosted.org/packages/98/ad/df18d492a8f00d29a30db307904b9b296e37507034eedb523876f3a2e13e/numba-0.60.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a17b70fc9e380ee29c42717e8cc0bfaa5556c416d94f9aa96ba13acb41bdece8", size = 2647254 }, + { url = "https://files.pythonhosted.org/packages/9a/51/a4dc2c01ce7a850b8e56ff6d5381d047a5daea83d12bad08aa071d34b2ee/numba-0.60.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3fb02b344a2a80efa6f677aa5c40cd5dd452e1b35f8d1c2af0dfd9ada9978e4b", size = 2649970 }, + { url = "https://files.pythonhosted.org/packages/f9/4c/8889ac94c0b33dca80bed11564b8c6d9ea14d7f094e674c58e5c5b05859b/numba-0.60.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5f4fde652ea604ea3c86508a3fb31556a6157b2c76c8b51b1d45eb40c8598703", size = 3412492 }, + { url = "https://files.pythonhosted.org/packages/57/03/2b4245b05b71c0cee667e6a0b51606dfa7f4157c9093d71c6b208385a611/numba-0.60.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4142d7ac0210cc86432b818338a2bc368dc773a2f5cf1e32ff7c5b378bd63ee8", size = 3705018 }, + { url = "https://files.pythonhosted.org/packages/79/89/2d924ca60dbf949f18a6fec223a2445f5f428d9a5f97a6b29c2122319015/numba-0.60.0-cp311-cp311-win_amd64.whl", hash = "sha256:cac02c041e9b5bc8cf8f2034ff6f0dbafccd1ae9590dc146b3a02a45e53af4e2", size = 2686920 }, + { url = "https://files.pythonhosted.org/packages/eb/5c/b5ec752c475e78a6c3676b67c514220dbde2725896bbb0b6ec6ea54b2738/numba-0.60.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d7da4098db31182fc5ffe4bc42c6f24cd7d1cb8a14b59fd755bfee32e34b8404", size = 2647866 }, + { url = "https://files.pythonhosted.org/packages/65/42/39559664b2e7c15689a638c2a38b3b74c6e69a04e2b3019b9f7742479188/numba-0.60.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38d6ea4c1f56417076ecf8fc327c831ae793282e0ff51080c5094cb726507b1c", size = 2650208 }, + { url = "https://files.pythonhosted.org/packages/67/88/c4459ccc05674ef02119abf2888ccd3e2fed12a323f52255f4982fc95876/numba-0.60.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:62908d29fb6a3229c242e981ca27e32a6e606cc253fc9e8faeb0e48760de241e", size = 3466946 }, + { url = "https://files.pythonhosted.org/packages/8b/41/ac11cf33524def12aa5bd698226ae196a1185831c05ed29dc0c56eaa308b/numba-0.60.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0ebaa91538e996f708f1ab30ef4d3ddc344b64b5227b67a57aa74f401bb68b9d", size = 3761463 }, + { url = "https://files.pythonhosted.org/packages/ca/bd/0fe29fcd1b6a8de479a4ed25c6e56470e467e3611c079d55869ceef2b6d1/numba-0.60.0-cp312-cp312-win_amd64.whl", hash = "sha256:f75262e8fe7fa96db1dca93d53a194a38c46da28b112b8a4aca168f0df860347", size = 2707588 }, + { url = "https://files.pythonhosted.org/packages/68/1a/87c53f836cdf557083248c3f47212271f220280ff766538795e77c8c6bbf/numba-0.60.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:01ef4cd7d83abe087d644eaa3d95831b777aa21d441a23703d649e06b8e06b74", size = 2647186 }, + { url = "https://files.pythonhosted.org/packages/28/14/a5baa1f2edea7b49afa4dc1bb1b126645198cf1075186853b5b497be826e/numba-0.60.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:819a3dfd4630d95fd574036f99e47212a1af41cbcb019bf8afac63ff56834449", size = 2650038 }, + { url = "https://files.pythonhosted.org/packages/3b/bd/f1985719ff34e37e07bb18f9d3acd17e5a21da255f550c8eae031e2ddf5f/numba-0.60.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0b983bd6ad82fe868493012487f34eae8bf7dd94654951404114f23c3466d34b", size = 3403010 }, + { url = "https://files.pythonhosted.org/packages/54/9b/cd73d3f6617ddc8398a63ef97d8dc9139a9879b9ca8a7ca4b8789056ea46/numba-0.60.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c151748cd269ddeab66334bd754817ffc0cabd9433acb0f551697e5151917d25", size = 3695086 }, + { url = "https://files.pythonhosted.org/packages/01/01/8b7b670c77c5ea0e47e283d82332969bf672ab6410d0b2610cac5b7a3ded/numba-0.60.0-cp39-cp39-win_amd64.whl", hash = "sha256:3031547a015710140e8c87226b4cfe927cac199835e5bf7d4fe5cb64e814e3ab", size = 2686978 }, +] + +[[package]] +name = "numpy" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/75/10dd1f8116a8b796cb2c737b674e02d02e80454bda953fa7e65d8c12b016/numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78", size = 18902015 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/91/3495b3237510f79f5d81f2508f9f13fea78ebfdf07538fc7444badda173d/numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece", size = 21165245 }, + { url = "https://files.pythonhosted.org/packages/05/33/26178c7d437a87082d11019292dce6d3fe6f0e9026b7b2309cbf3e489b1d/numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04", size = 13738540 }, + { url = "https://files.pythonhosted.org/packages/ec/31/cc46e13bf07644efc7a4bf68df2df5fb2a1a88d0cd0da9ddc84dc0033e51/numpy-2.0.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8c5713284ce4e282544c68d1c3b2c7161d38c256d2eefc93c1d683cf47683e66", size = 5300623 }, + { url = "https://files.pythonhosted.org/packages/6e/16/7bfcebf27bb4f9d7ec67332ffebee4d1bf085c84246552d52dbb548600e7/numpy-2.0.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:becfae3ddd30736fe1889a37f1f580e245ba79a5855bff5f2a29cb3ccc22dd7b", size = 6901774 }, + { url = "https://files.pythonhosted.org/packages/f9/a3/561c531c0e8bf082c5bef509d00d56f82e0ea7e1e3e3a7fc8fa78742a6e5/numpy-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2da5960c3cf0df7eafefd806d4e612c5e19358de82cb3c343631188991566ccd", size = 13907081 }, + { url = "https://files.pythonhosted.org/packages/fa/66/f7177ab331876200ac7563a580140643d1179c8b4b6a6b0fc9838de2a9b8/numpy-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:496f71341824ed9f3d2fd36cf3ac57ae2e0165c143b55c3a035ee219413f3318", size = 19523451 }, + { url = "https://files.pythonhosted.org/packages/25/7f/0b209498009ad6453e4efc2c65bcdf0ae08a182b2b7877d7ab38a92dc542/numpy-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a61ec659f68ae254e4d237816e33171497e978140353c0c2038d46e63282d0c8", size = 19927572 }, + { url = "https://files.pythonhosted.org/packages/3e/df/2619393b1e1b565cd2d4c4403bdd979621e2c4dea1f8532754b2598ed63b/numpy-2.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d731a1c6116ba289c1e9ee714b08a8ff882944d4ad631fd411106a30f083c326", size = 14400722 }, + { url = "https://files.pythonhosted.org/packages/22/ad/77e921b9f256d5da36424ffb711ae79ca3f451ff8489eeca544d0701d74a/numpy-2.0.2-cp310-cp310-win32.whl", hash = "sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97", size = 6472170 }, + { url = "https://files.pythonhosted.org/packages/10/05/3442317535028bc29cf0c0dd4c191a4481e8376e9f0db6bcf29703cadae6/numpy-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131", size = 15905558 }, + { url = "https://files.pythonhosted.org/packages/8b/cf/034500fb83041aa0286e0fb16e7c76e5c8b67c0711bb6e9e9737a717d5fe/numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448", size = 21169137 }, + { url = "https://files.pythonhosted.org/packages/4a/d9/32de45561811a4b87fbdee23b5797394e3d1504b4a7cf40c10199848893e/numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195", size = 13703552 }, + { url = "https://files.pythonhosted.org/packages/c1/ca/2f384720020c7b244d22508cb7ab23d95f179fcfff33c31a6eeba8d6c512/numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57", size = 5298957 }, + { url = "https://files.pythonhosted.org/packages/0e/78/a3e4f9fb6aa4e6fdca0c5428e8ba039408514388cf62d89651aade838269/numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a", size = 6905573 }, + { url = "https://files.pythonhosted.org/packages/a0/72/cfc3a1beb2caf4efc9d0b38a15fe34025230da27e1c08cc2eb9bfb1c7231/numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669", size = 13914330 }, + { url = "https://files.pythonhosted.org/packages/ba/a8/c17acf65a931ce551fee11b72e8de63bf7e8a6f0e21add4c937c83563538/numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951", size = 19534895 }, + { url = "https://files.pythonhosted.org/packages/ba/86/8767f3d54f6ae0165749f84648da9dcc8cd78ab65d415494962c86fac80f/numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9", size = 19937253 }, + { url = "https://files.pythonhosted.org/packages/df/87/f76450e6e1c14e5bb1eae6836478b1028e096fd02e85c1c37674606ab752/numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15", size = 14414074 }, + { url = "https://files.pythonhosted.org/packages/5c/ca/0f0f328e1e59f73754f06e1adfb909de43726d4f24c6a3f8805f34f2b0fa/numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4", size = 6470640 }, + { url = "https://files.pythonhosted.org/packages/eb/57/3a3f14d3a759dcf9bf6e9eda905794726b758819df4663f217d658a58695/numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc", size = 15910230 }, + { url = "https://files.pythonhosted.org/packages/45/40/2e117be60ec50d98fa08c2f8c48e09b3edea93cfcabd5a9ff6925d54b1c2/numpy-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b", size = 20895803 }, + { url = "https://files.pythonhosted.org/packages/46/92/1b8b8dee833f53cef3e0a3f69b2374467789e0bb7399689582314df02651/numpy-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e", size = 13471835 }, + { url = "https://files.pythonhosted.org/packages/7f/19/e2793bde475f1edaea6945be141aef6c8b4c669b90c90a300a8954d08f0a/numpy-2.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c", size = 5038499 }, + { url = "https://files.pythonhosted.org/packages/e3/ff/ddf6dac2ff0dd50a7327bcdba45cb0264d0e96bb44d33324853f781a8f3c/numpy-2.0.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c", size = 6633497 }, + { url = "https://files.pythonhosted.org/packages/72/21/67f36eac8e2d2cd652a2e69595a54128297cdcb1ff3931cfc87838874bd4/numpy-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692", size = 13621158 }, + { url = "https://files.pythonhosted.org/packages/39/68/e9f1126d757653496dbc096cb429014347a36b228f5a991dae2c6b6cfd40/numpy-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a", size = 19236173 }, + { url = "https://files.pythonhosted.org/packages/d1/e9/1f5333281e4ebf483ba1c888b1d61ba7e78d7e910fdd8e6499667041cc35/numpy-2.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c", size = 19634174 }, + { url = "https://files.pythonhosted.org/packages/71/af/a469674070c8d8408384e3012e064299f7a2de540738a8e414dcfd639996/numpy-2.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded", size = 14099701 }, + { url = "https://files.pythonhosted.org/packages/d0/3d/08ea9f239d0e0e939b6ca52ad403c84a2bce1bde301a8eb4888c1c1543f1/numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5", size = 6174313 }, + { url = "https://files.pythonhosted.org/packages/b2/b5/4ac39baebf1fdb2e72585c8352c56d063b6126be9fc95bd2bb5ef5770c20/numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a", size = 15606179 }, + { url = "https://files.pythonhosted.org/packages/43/c1/41c8f6df3162b0c6ffd4437d729115704bd43363de0090c7f913cfbc2d89/numpy-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9059e10581ce4093f735ed23f3b9d283b9d517ff46009ddd485f1747eb22653c", size = 21169942 }, + { url = "https://files.pythonhosted.org/packages/39/bc/fd298f308dcd232b56a4031fd6ddf11c43f9917fbc937e53762f7b5a3bb1/numpy-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:423e89b23490805d2a5a96fe40ec507407b8ee786d66f7328be214f9679df6dd", size = 13711512 }, + { url = "https://files.pythonhosted.org/packages/96/ff/06d1aa3eeb1c614eda245c1ba4fb88c483bee6520d361641331872ac4b82/numpy-2.0.2-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:2b2955fa6f11907cf7a70dab0d0755159bca87755e831e47932367fc8f2f2d0b", size = 5306976 }, + { url = "https://files.pythonhosted.org/packages/2d/98/121996dcfb10a6087a05e54453e28e58694a7db62c5a5a29cee14c6e047b/numpy-2.0.2-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:97032a27bd9d8988b9a97a8c4d2c9f2c15a81f61e2f21404d7e8ef00cb5be729", size = 6906494 }, + { url = "https://files.pythonhosted.org/packages/15/31/9dffc70da6b9bbf7968f6551967fc21156207366272c2a40b4ed6008dc9b/numpy-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e795a8be3ddbac43274f18588329c72939870a16cae810c2b73461c40718ab1", size = 13912596 }, + { url = "https://files.pythonhosted.org/packages/b9/14/78635daab4b07c0930c919d451b8bf8c164774e6a3413aed04a6d95758ce/numpy-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b258c385842546006213344c50655ff1555a9338e2e5e02a0756dc3e803dd", size = 19526099 }, + { url = "https://files.pythonhosted.org/packages/26/4c/0eeca4614003077f68bfe7aac8b7496f04221865b3a5e7cb230c9d055afd/numpy-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fec9451a7789926bcf7c2b8d187292c9f93ea30284802a0ab3f5be8ab36865d", size = 19932823 }, + { url = "https://files.pythonhosted.org/packages/f1/46/ea25b98b13dccaebddf1a803f8c748680d972e00507cd9bc6dcdb5aa2ac1/numpy-2.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9189427407d88ff25ecf8f12469d4d39d35bee1db5d39fc5c168c6f088a6956d", size = 14404424 }, + { url = "https://files.pythonhosted.org/packages/c8/a6/177dd88d95ecf07e722d21008b1b40e681a929eb9e329684d449c36586b2/numpy-2.0.2-cp39-cp39-win32.whl", hash = "sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa", size = 6476809 }, + { url = "https://files.pythonhosted.org/packages/ea/2b/7fc9f4e7ae5b507c1a3a21f0f15ed03e794c1242ea8a242ac158beb56034/numpy-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73", size = 15911314 }, + { url = "https://files.pythonhosted.org/packages/8f/3b/df5a870ac6a3be3a86856ce195ef42eec7ae50d2a202be1f5a4b3b340e14/numpy-2.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7f0a0c6f12e07fa94133c8a67404322845220c06a9e80e85999afe727f7438b8", size = 21025288 }, + { url = "https://files.pythonhosted.org/packages/2c/97/51af92f18d6f6f2d9ad8b482a99fb74e142d71372da5d834b3a2747a446e/numpy-2.0.2-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:312950fdd060354350ed123c0e25a71327d3711584beaef30cdaa93320c392d4", size = 6762793 }, + { url = "https://files.pythonhosted.org/packages/12/46/de1fbd0c1b5ccaa7f9a005b66761533e2f6a3e560096682683a223631fe9/numpy-2.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26df23238872200f63518dd2aa984cfca675d82469535dc7162dc2ee52d9dd5c", size = 19334885 }, + { url = "https://files.pythonhosted.org/packages/cc/dc/d330a6faefd92b446ec0f0dfea4c3207bb1fef3c4771d19cf4543efd2c78/numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385", size = 15828784 }, +] + +[[package]] +name = "numpydoc" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, + { name = "tabulate" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ee/59/5d1d1afb0b9598e21e7cda477935188e39ef845bcf59cb65ac20845bfd45/numpydoc-1.8.0.tar.gz", hash = "sha256:022390ab7464a44f8737f79f8b31ce1d3cfa4b4af79ccaa1aac5e8368db587fb", size = 90445 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/45/56d99ba9366476cd8548527667f01869279cedb9e66b28eb4dfb27701679/numpydoc-1.8.0-py3-none-any.whl", hash = "sha256:72024c7fd5e17375dec3608a27c03303e8ad00c81292667955c6fea7a3ccf541", size = 64003 }, +] + +[[package]] +name = "packaging" +version = "24.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/65/50db4dda066951078f0a96cf12f4b9ada6e4b811516bf0262c0f4f7064d4/packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", size = 148788 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124", size = 53985 }, +] + +[[package]] +name = "pandas" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/70/c853aec59839bceed032d52010ff5f1b8d87dc3114b762e4ba2727661a3b/pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5", size = 12580827 }, + { url = "https://files.pythonhosted.org/packages/99/f2/c4527768739ffa4469b2b4fff05aa3768a478aed89a2f271a79a40eee984/pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348", size = 11303897 }, + { url = "https://files.pythonhosted.org/packages/ed/12/86c1747ea27989d7a4064f806ce2bae2c6d575b950be087837bdfcabacc9/pandas-2.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed", size = 66480908 }, + { url = "https://files.pythonhosted.org/packages/44/50/7db2cd5e6373ae796f0ddad3675268c8d59fb6076e66f0c339d61cea886b/pandas-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57", size = 13064210 }, + { url = "https://files.pythonhosted.org/packages/61/61/a89015a6d5536cb0d6c3ba02cebed51a95538cf83472975275e28ebf7d0c/pandas-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42", size = 16754292 }, + { url = "https://files.pythonhosted.org/packages/ce/0d/4cc7b69ce37fac07645a94e1d4b0880b15999494372c1523508511b09e40/pandas-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f", size = 14416379 }, + { url = "https://files.pythonhosted.org/packages/31/9e/6ebb433de864a6cd45716af52a4d7a8c3c9aaf3a98368e61db9e69e69a9c/pandas-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645", size = 11598471 }, + { url = "https://files.pythonhosted.org/packages/a8/44/d9502bf0ed197ba9bf1103c9867d5904ddcaf869e52329787fc54ed70cc8/pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039", size = 12602222 }, + { url = "https://files.pythonhosted.org/packages/52/11/9eac327a38834f162b8250aab32a6781339c69afe7574368fffe46387edf/pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd", size = 11321274 }, + { url = "https://files.pythonhosted.org/packages/45/fb/c4beeb084718598ba19aa9f5abbc8aed8b42f90930da861fcb1acdb54c3a/pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698", size = 15579836 }, + { url = "https://files.pythonhosted.org/packages/cd/5f/4dba1d39bb9c38d574a9a22548c540177f78ea47b32f99c0ff2ec499fac5/pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc", size = 13058505 }, + { url = "https://files.pythonhosted.org/packages/b9/57/708135b90391995361636634df1f1130d03ba456e95bcf576fada459115a/pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3", size = 16744420 }, + { url = "https://files.pythonhosted.org/packages/86/4a/03ed6b7ee323cf30404265c284cee9c65c56a212e0a08d9ee06984ba2240/pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32", size = 14440457 }, + { url = "https://files.pythonhosted.org/packages/ed/8c/87ddf1fcb55d11f9f847e3c69bb1c6f8e46e2f40ab1a2d2abadb2401b007/pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5", size = 11617166 }, + { url = "https://files.pythonhosted.org/packages/17/a3/fb2734118db0af37ea7433f57f722c0a56687e14b14690edff0cdb4b7e58/pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9", size = 12529893 }, + { url = "https://files.pythonhosted.org/packages/e1/0c/ad295fd74bfac85358fd579e271cded3ac969de81f62dd0142c426b9da91/pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4", size = 11363475 }, + { url = "https://files.pythonhosted.org/packages/c6/2a/4bba3f03f7d07207481fed47f5b35f556c7441acddc368ec43d6643c5777/pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3", size = 15188645 }, + { url = "https://files.pythonhosted.org/packages/38/f8/d8fddee9ed0d0c0f4a2132c1dfcf0e3e53265055da8df952a53e7eaf178c/pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319", size = 12739445 }, + { url = "https://files.pythonhosted.org/packages/20/e8/45a05d9c39d2cea61ab175dbe6a2de1d05b679e8de2011da4ee190d7e748/pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8", size = 16359235 }, + { url = "https://files.pythonhosted.org/packages/1d/99/617d07a6a5e429ff90c90da64d428516605a1ec7d7bea494235e1c3882de/pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a", size = 14056756 }, + { url = "https://files.pythonhosted.org/packages/29/d4/1244ab8edf173a10fd601f7e13b9566c1b525c4f365d6bee918e68381889/pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13", size = 11504248 }, + { url = "https://files.pythonhosted.org/packages/64/22/3b8f4e0ed70644e85cfdcd57454686b9057c6c38d2f74fe4b8bc2527214a/pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015", size = 12477643 }, + { url = "https://files.pythonhosted.org/packages/e4/93/b3f5d1838500e22c8d793625da672f3eec046b1a99257666c94446969282/pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28", size = 11281573 }, + { url = "https://files.pythonhosted.org/packages/f5/94/6c79b07f0e5aab1dcfa35a75f4817f5c4f677931d4234afcd75f0e6a66ca/pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0", size = 15196085 }, + { url = "https://files.pythonhosted.org/packages/e8/31/aa8da88ca0eadbabd0a639788a6da13bb2ff6edbbb9f29aa786450a30a91/pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24", size = 12711809 }, + { url = "https://files.pythonhosted.org/packages/ee/7c/c6dbdb0cb2a4344cacfb8de1c5808ca885b2e4dcfde8008266608f9372af/pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659", size = 16356316 }, + { url = "https://files.pythonhosted.org/packages/57/b7/8b757e7d92023b832869fa8881a992696a0bfe2e26f72c9ae9f255988d42/pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb", size = 14022055 }, + { url = "https://files.pythonhosted.org/packages/3b/bc/4b18e2b8c002572c5a441a64826252ce5da2aa738855747247a971988043/pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d", size = 11481175 }, + { url = "https://files.pythonhosted.org/packages/76/a3/a5d88146815e972d40d19247b2c162e88213ef51c7c25993942c39dbf41d/pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468", size = 12615650 }, + { url = "https://files.pythonhosted.org/packages/9c/8c/f0fd18f6140ddafc0c24122c8a964e48294acc579d47def376fef12bcb4a/pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18", size = 11290177 }, + { url = "https://files.pythonhosted.org/packages/ed/f9/e995754eab9c0f14c6777401f7eece0943840b7a9fc932221c19d1abee9f/pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2", size = 14651526 }, + { url = "https://files.pythonhosted.org/packages/25/b0/98d6ae2e1abac4f35230aa756005e8654649d305df9a28b16b9ae4353bff/pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4", size = 11871013 }, + { url = "https://files.pythonhosted.org/packages/cc/57/0f72a10f9db6a4628744c8e8f0df4e6e21de01212c7c981d31e50ffc8328/pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d", size = 15711620 }, + { url = "https://files.pythonhosted.org/packages/ab/5f/b38085618b950b79d2d9164a711c52b10aefc0ae6833b96f626b7021b2ed/pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", size = 13098436 }, + { url = "https://files.pythonhosted.org/packages/ca/8c/8848a4c9b8fdf5a534fe2077af948bf53cd713d77ffbcd7bd15710348fd7/pandas-2.2.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc6b93f9b966093cb0fd62ff1a7e4c09e6d546ad7c1de191767baffc57628f39", size = 12595535 }, + { url = "https://files.pythonhosted.org/packages/9c/b9/5cead4f63b6d31bdefeb21a679bc5a7f4aaf262ca7e07e2bc1c341b68470/pandas-2.2.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5dbca4c1acd72e8eeef4753eeca07de9b1db4f398669d5994086f788a5d7cc30", size = 11319822 }, + { url = "https://files.pythonhosted.org/packages/31/af/89e35619fb573366fa68dc26dad6ad2c08c17b8004aad6d98f1a31ce4bb3/pandas-2.2.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8cd6d7cc958a3910f934ea8dbdf17b2364827bb4dafc38ce6eef6bb3d65ff09c", size = 15625439 }, + { url = "https://files.pythonhosted.org/packages/3d/dd/bed19c2974296661493d7acc4407b1d2db4e2a482197df100f8f965b6225/pandas-2.2.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99df71520d25fade9db7c1076ac94eb994f4d2673ef2aa2e86ee039b6746d20c", size = 13068928 }, + { url = "https://files.pythonhosted.org/packages/31/a3/18508e10a31ea108d746c848b5a05c0711e0278fa0d6f1c52a8ec52b80a5/pandas-2.2.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:31d0ced62d4ea3e231a9f228366919a5ea0b07440d9d4dac345376fd8e1477ea", size = 16783266 }, + { url = "https://files.pythonhosted.org/packages/c4/a5/3429bd13d82bebc78f4d78c3945efedef63a7cd0c15c17b2eeb838d1121f/pandas-2.2.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7eee9e7cea6adf3e3d24e304ac6b8300646e2a5d1cd3a3c2abed9101b0846761", size = 14450871 }, + { url = "https://files.pythonhosted.org/packages/2f/49/5c30646e96c684570925b772eac4eb0a8cb0ca590fa978f56c5d3ae73ea1/pandas-2.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:4850ba03528b6dd51d6c5d273c46f183f39a9baf3f0143e566b89450965b105e", size = 11618011 }, +] + +[[package]] +name = "pandocfilters" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/6f/3dd4940bbe001c06a65f88e36bad298bc7a0de5036115639926b0c5c0458/pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e", size = 8454 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc", size = 8663 }, +] + +[[package]] +name = "parso" +version = "0.8.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/94/68e2e17afaa9169cf6412ab0f28623903be73d1b32e208d9e8e541bb086d/parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d", size = 400609 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size = 103650 }, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772 }, +] + +[[package]] +name = "pillow" +version = "10.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/74/ad3d526f3bf7b6d3f408b73fde271ec69dfac8b81341a318ce825f2b3812/pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06", size = 46555059 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/69/a31cccd538ca0b5272be2a38347f8839b97a14be104ea08b0db92f749c74/pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e", size = 3509271 }, + { url = "https://files.pythonhosted.org/packages/9a/9e/4143b907be8ea0bce215f2ae4f7480027473f8b61fcedfda9d851082a5d2/pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d", size = 3375658 }, + { url = "https://files.pythonhosted.org/packages/8a/25/1fc45761955f9359b1169aa75e241551e74ac01a09f487adaaf4c3472d11/pillow-10.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856", size = 4332075 }, + { url = "https://files.pythonhosted.org/packages/5e/dd/425b95d0151e1d6c951f45051112394f130df3da67363b6bc75dc4c27aba/pillow-10.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f", size = 4444808 }, + { url = "https://files.pythonhosted.org/packages/b1/84/9a15cc5726cbbfe7f9f90bfb11f5d028586595907cd093815ca6644932e3/pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b", size = 4356290 }, + { url = "https://files.pythonhosted.org/packages/b5/5b/6651c288b08df3b8c1e2f8c1152201e0b25d240e22ddade0f1e242fc9fa0/pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc", size = 4525163 }, + { url = "https://files.pythonhosted.org/packages/07/8b/34854bf11a83c248505c8cb0fcf8d3d0b459a2246c8809b967963b6b12ae/pillow-10.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e", size = 4463100 }, + { url = "https://files.pythonhosted.org/packages/78/63/0632aee4e82476d9cbe5200c0cdf9ba41ee04ed77887432845264d81116d/pillow-10.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46", size = 4592880 }, + { url = "https://files.pythonhosted.org/packages/df/56/b8663d7520671b4398b9d97e1ed9f583d4afcbefbda3c6188325e8c297bd/pillow-10.4.0-cp310-cp310-win32.whl", hash = "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984", size = 2235218 }, + { url = "https://files.pythonhosted.org/packages/f4/72/0203e94a91ddb4a9d5238434ae6c1ca10e610e8487036132ea9bf806ca2a/pillow-10.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141", size = 2554487 }, + { url = "https://files.pythonhosted.org/packages/bd/52/7e7e93d7a6e4290543f17dc6f7d3af4bd0b3dd9926e2e8a35ac2282bc5f4/pillow-10.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1", size = 2243219 }, + { url = "https://files.pythonhosted.org/packages/a7/62/c9449f9c3043c37f73e7487ec4ef0c03eb9c9afc91a92b977a67b3c0bbc5/pillow-10.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c", size = 3509265 }, + { url = "https://files.pythonhosted.org/packages/f4/5f/491dafc7bbf5a3cc1845dc0430872e8096eb9e2b6f8161509d124594ec2d/pillow-10.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be", size = 3375655 }, + { url = "https://files.pythonhosted.org/packages/73/d5/c4011a76f4207a3c151134cd22a1415741e42fa5ddecec7c0182887deb3d/pillow-10.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3", size = 4340304 }, + { url = "https://files.pythonhosted.org/packages/ac/10/c67e20445a707f7a610699bba4fe050583b688d8cd2d202572b257f46600/pillow-10.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6", size = 4452804 }, + { url = "https://files.pythonhosted.org/packages/a9/83/6523837906d1da2b269dee787e31df3b0acb12e3d08f024965a3e7f64665/pillow-10.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe", size = 4365126 }, + { url = "https://files.pythonhosted.org/packages/ba/e5/8c68ff608a4203085158cff5cc2a3c534ec384536d9438c405ed6370d080/pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319", size = 4533541 }, + { url = "https://files.pythonhosted.org/packages/f4/7c/01b8dbdca5bc6785573f4cee96e2358b0918b7b2c7b60d8b6f3abf87a070/pillow-10.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d", size = 4471616 }, + { url = "https://files.pythonhosted.org/packages/c8/57/2899b82394a35a0fbfd352e290945440e3b3785655a03365c0ca8279f351/pillow-10.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696", size = 4600802 }, + { url = "https://files.pythonhosted.org/packages/4d/d7/a44f193d4c26e58ee5d2d9db3d4854b2cfb5b5e08d360a5e03fe987c0086/pillow-10.4.0-cp311-cp311-win32.whl", hash = "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496", size = 2235213 }, + { url = "https://files.pythonhosted.org/packages/c1/d0/5866318eec2b801cdb8c82abf190c8343d8a1cd8bf5a0c17444a6f268291/pillow-10.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91", size = 2554498 }, + { url = "https://files.pythonhosted.org/packages/d4/c8/310ac16ac2b97e902d9eb438688de0d961660a87703ad1561fd3dfbd2aa0/pillow-10.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22", size = 2243219 }, + { url = "https://files.pythonhosted.org/packages/05/cb/0353013dc30c02a8be34eb91d25e4e4cf594b59e5a55ea1128fde1e5f8ea/pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94", size = 3509350 }, + { url = "https://files.pythonhosted.org/packages/e7/cf/5c558a0f247e0bf9cec92bff9b46ae6474dd736f6d906315e60e4075f737/pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597", size = 3374980 }, + { url = "https://files.pythonhosted.org/packages/84/48/6e394b86369a4eb68b8a1382c78dc092245af517385c086c5094e3b34428/pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80", size = 4343799 }, + { url = "https://files.pythonhosted.org/packages/3b/f3/a8c6c11fa84b59b9df0cd5694492da8c039a24cd159f0f6918690105c3be/pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca", size = 4459973 }, + { url = "https://files.pythonhosted.org/packages/7d/1b/c14b4197b80150fb64453585247e6fb2e1d93761fa0fa9cf63b102fde822/pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef", size = 4370054 }, + { url = "https://files.pythonhosted.org/packages/55/77/40daddf677897a923d5d33329acd52a2144d54a9644f2a5422c028c6bf2d/pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a", size = 4539484 }, + { url = "https://files.pythonhosted.org/packages/40/54/90de3e4256b1207300fb2b1d7168dd912a2fb4b2401e439ba23c2b2cabde/pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b", size = 4477375 }, + { url = "https://files.pythonhosted.org/packages/13/24/1bfba52f44193860918ff7c93d03d95e3f8748ca1de3ceaf11157a14cf16/pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9", size = 4608773 }, + { url = "https://files.pythonhosted.org/packages/55/04/5e6de6e6120451ec0c24516c41dbaf80cce1b6451f96561235ef2429da2e/pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42", size = 2235690 }, + { url = "https://files.pythonhosted.org/packages/74/0a/d4ce3c44bca8635bd29a2eab5aa181b654a734a29b263ca8efe013beea98/pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a", size = 2554951 }, + { url = "https://files.pythonhosted.org/packages/b5/ca/184349ee40f2e92439be9b3502ae6cfc43ac4b50bc4fc6b3de7957563894/pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9", size = 2243427 }, + { url = "https://files.pythonhosted.org/packages/c3/00/706cebe7c2c12a6318aabe5d354836f54adff7156fd9e1bd6c89f4ba0e98/pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3", size = 3525685 }, + { url = "https://files.pythonhosted.org/packages/cf/76/f658cbfa49405e5ecbfb9ba42d07074ad9792031267e782d409fd8fe7c69/pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb", size = 3374883 }, + { url = "https://files.pythonhosted.org/packages/46/2b/99c28c4379a85e65378211971c0b430d9c7234b1ec4d59b2668f6299e011/pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70", size = 4339837 }, + { url = "https://files.pythonhosted.org/packages/f1/74/b1ec314f624c0c43711fdf0d8076f82d9d802afd58f1d62c2a86878e8615/pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be", size = 4455562 }, + { url = "https://files.pythonhosted.org/packages/4a/2a/4b04157cb7b9c74372fa867096a1607e6fedad93a44deeff553ccd307868/pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0", size = 4366761 }, + { url = "https://files.pythonhosted.org/packages/ac/7b/8f1d815c1a6a268fe90481232c98dd0e5fa8c75e341a75f060037bd5ceae/pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc", size = 4536767 }, + { url = "https://files.pythonhosted.org/packages/e5/77/05fa64d1f45d12c22c314e7b97398ffb28ef2813a485465017b7978b3ce7/pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a", size = 4477989 }, + { url = "https://files.pythonhosted.org/packages/12/63/b0397cfc2caae05c3fb2f4ed1b4fc4fc878f0243510a7a6034ca59726494/pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309", size = 4610255 }, + { url = "https://files.pythonhosted.org/packages/7b/f9/cfaa5082ca9bc4a6de66ffe1c12c2d90bf09c309a5f52b27759a596900e7/pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060", size = 2235603 }, + { url = "https://files.pythonhosted.org/packages/01/6a/30ff0eef6e0c0e71e55ded56a38d4859bf9d3634a94a88743897b5f96936/pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea", size = 2554972 }, + { url = "https://files.pythonhosted.org/packages/48/2c/2e0a52890f269435eee38b21c8218e102c621fe8d8df8b9dd06fabf879ba/pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d", size = 2243375 }, + { url = "https://files.pythonhosted.org/packages/31/85/955fa5400fa8039921f630372cfe5056eed6e1b8e0430ee4507d7de48832/pillow-10.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0ae24a547e8b711ccaaf99c9ae3cd975470e1a30caa80a6aaee9a2f19c05701d", size = 3509283 }, + { url = "https://files.pythonhosted.org/packages/23/9c/343827267eb28d41cd82b4180d33b10d868af9077abcec0af9793aa77d2d/pillow-10.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:298478fe4f77a4408895605f3482b6cc6222c018b2ce565c2b6b9c354ac3229b", size = 3375691 }, + { url = "https://files.pythonhosted.org/packages/60/a3/7ebbeabcd341eab722896d1a5b59a3df98c4b4d26cf4b0385f8aa94296f7/pillow-10.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:134ace6dc392116566980ee7436477d844520a26a4b1bd4053f6f47d096997fd", size = 4328295 }, + { url = "https://files.pythonhosted.org/packages/32/3f/c02268d0c6fb6b3958bdda673c17b315c821d97df29ae6969f20fb49388a/pillow-10.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:930044bb7679ab003b14023138b50181899da3f25de50e9dbee23b61b4de2126", size = 4440810 }, + { url = "https://files.pythonhosted.org/packages/67/5d/1c93c8cc35f2fdd3d6cc7e4ad72d203902859a2867de6ad957d9b708eb8d/pillow-10.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:c76e5786951e72ed3686e122d14c5d7012f16c8303a674d18cdcd6d89557fc5b", size = 4352283 }, + { url = "https://files.pythonhosted.org/packages/bc/a8/8655557c9c7202b8abbd001f61ff36711cefaf750debcaa1c24d154ef602/pillow-10.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b2724fdb354a868ddf9a880cb84d102da914e99119211ef7ecbdc613b8c96b3c", size = 4521800 }, + { url = "https://files.pythonhosted.org/packages/58/78/6f95797af64d137124f68af1bdaa13b5332da282b86031f6fa70cf368261/pillow-10.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dbc6ae66518ab3c5847659e9988c3b60dc94ffb48ef9168656e0019a93dbf8a1", size = 4459177 }, + { url = "https://files.pythonhosted.org/packages/8a/6d/2b3ce34f1c4266d79a78c9a51d1289a33c3c02833fe294ef0dcbb9cba4ed/pillow-10.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:06b2f7898047ae93fad74467ec3d28fe84f7831370e3c258afa533f81ef7f3df", size = 4589079 }, + { url = "https://files.pythonhosted.org/packages/e3/e0/456258c74da1ff5bf8ef1eab06a95ca994d8b9ed44c01d45c3f8cbd1db7e/pillow-10.4.0-cp39-cp39-win32.whl", hash = "sha256:7970285ab628a3779aecc35823296a7869f889b8329c16ad5a71e4901a3dc4ef", size = 2235247 }, + { url = "https://files.pythonhosted.org/packages/37/f8/bef952bdb32aa53741f58bf21798642209e994edc3f6598f337f23d5400a/pillow-10.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:961a7293b2457b405967af9c77dcaa43cc1a8cd50d23c532e62d48ab6cdd56f5", size = 2554479 }, + { url = "https://files.pythonhosted.org/packages/bb/8e/805201619cad6651eef5fc1fdef913804baf00053461522fabbc5588ea12/pillow-10.4.0-cp39-cp39-win_arm64.whl", hash = "sha256:32cda9e3d601a52baccb2856b8ea1fc213c90b340c542dcef77140dfa3278a9e", size = 2243226 }, + { url = "https://files.pythonhosted.org/packages/38/30/095d4f55f3a053392f75e2eae45eba3228452783bab3d9a920b951ac495c/pillow-10.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4", size = 3493889 }, + { url = "https://files.pythonhosted.org/packages/f3/e8/4ff79788803a5fcd5dc35efdc9386af153569853767bff74540725b45863/pillow-10.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da", size = 3346160 }, + { url = "https://files.pythonhosted.org/packages/d7/ac/4184edd511b14f760c73f5bb8a5d6fd85c591c8aff7c2229677a355c4179/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026", size = 3435020 }, + { url = "https://files.pythonhosted.org/packages/da/21/1749cd09160149c0a246a81d646e05f35041619ce76f6493d6a96e8d1103/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e", size = 3490539 }, + { url = "https://files.pythonhosted.org/packages/b6/f5/f71fe1888b96083b3f6dfa0709101f61fc9e972c0c8d04e9d93ccef2a045/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5", size = 3476125 }, + { url = "https://files.pythonhosted.org/packages/96/b9/c0362c54290a31866c3526848583a2f45a535aa9d725fd31e25d318c805f/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885", size = 3579373 }, + { url = "https://files.pythonhosted.org/packages/52/3b/ce7a01026a7cf46e5452afa86f97a5e88ca97f562cafa76570178ab56d8d/pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5", size = 2554661 }, + { url = "https://files.pythonhosted.org/packages/e1/1f/5a9fcd6ced51633c22481417e11b1b47d723f64fb536dfd67c015eb7f0ab/pillow-10.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a02364621fe369e06200d4a16558e056fe2805d3468350df3aef21e00d26214b", size = 3493850 }, + { url = "https://files.pythonhosted.org/packages/cb/e6/3ea4755ed5320cb62aa6be2f6de47b058c6550f752dd050e86f694c59798/pillow-10.4.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1b5dea9831a90e9d0721ec417a80d4cbd7022093ac38a568db2dd78363b00908", size = 3346118 }, + { url = "https://files.pythonhosted.org/packages/0a/22/492f9f61e4648422b6ca39268ec8139277a5b34648d28f400faac14e0f48/pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b885f89040bb8c4a1573566bbb2f44f5c505ef6e74cec7ab9068c900047f04b", size = 3434958 }, + { url = "https://files.pythonhosted.org/packages/f9/19/559a48ad4045704bb0547965b9a9345f5cd461347d977a56d178db28819e/pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87dd88ded2e6d74d31e1e0a99a726a6765cda32d00ba72dc37f0651f306daaa8", size = 3490340 }, + { url = "https://files.pythonhosted.org/packages/d9/de/cebaca6fb79905b3a1aa0281d238769df3fb2ede34fd7c0caa286575915a/pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:2db98790afc70118bd0255c2eeb465e9767ecf1f3c25f9a1abb8ffc8cfd1fe0a", size = 3476048 }, + { url = "https://files.pythonhosted.org/packages/71/f0/86d5b2f04693b0116a01d75302b0a307800a90d6c351a8aa4f8ae76cd499/pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f7baece4ce06bade126fb84b8af1c33439a76d8a6fd818970215e0560ca28c27", size = 3579366 }, + { url = "https://files.pythonhosted.org/packages/37/ae/2dbfc38cc4fd14aceea14bc440d5151b21f64c4c3ba3f6f4191610b7ee5d/pillow-10.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3", size = 2554652 }, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "pre-commit" +version = "4.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/c8/e22c292035f1bac8b9f5237a2622305bc0304e776080b246f3df57c4ff9f/pre_commit-4.0.1.tar.gz", hash = "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2", size = 191678 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/8f/496e10d51edd6671ebe0432e33ff800aa86775d2d147ce7d43389324a525/pre_commit-4.0.1-py2.py3-none-any.whl", hash = "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878", size = 218713 }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.48" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2d/4f/feb5e137aff82f7c7f3248267b97451da3644f6cdc218edfe549fb354127/prompt_toolkit-3.0.48.tar.gz", hash = "sha256:d6623ab0477a80df74e646bdbc93621143f5caf104206aa29294d53de1a03d90", size = 424684 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/6a/fd08d94654f7e67c52ca30523a178b3f8ccc4237fce4be90d39c938a831a/prompt_toolkit-3.0.48-py3-none-any.whl", hash = "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e", size = 386595 }, +] + +[[package]] +name = "psutil" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/c7/8c6872f7372eb6a6b2e4708b88419fb46b857f7a2e1892966b851cc79fc9/psutil-6.0.0.tar.gz", hash = "sha256:8faae4f310b6d969fa26ca0545338b21f73c6b15db7c4a8d934a5482faa818f2", size = 508067 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/66/78c9c3020f573c58101dc43a44f6855d01bbbd747e24da2f0c4491200ea3/psutil-6.0.0-cp27-none-win32.whl", hash = "sha256:02b69001f44cc73c1c5279d02b30a817e339ceb258ad75997325e0e6169d8b35", size = 249766 }, + { url = "https://files.pythonhosted.org/packages/e1/3f/2403aa9558bea4d3854b0e5e567bc3dd8e9fbc1fc4453c0aa9aafeb75467/psutil-6.0.0-cp27-none-win_amd64.whl", hash = "sha256:21f1fb635deccd510f69f485b87433460a603919b45e2a324ad65b0cc74f8fb1", size = 253024 }, + { url = "https://files.pythonhosted.org/packages/0b/37/f8da2fbd29690b3557cca414c1949f92162981920699cd62095a984983bf/psutil-6.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c588a7e9b1173b6e866756dde596fd4cad94f9399daf99ad8c3258b3cb2b47a0", size = 250961 }, + { url = "https://files.pythonhosted.org/packages/35/56/72f86175e81c656a01c4401cd3b1c923f891b31fbcebe98985894176d7c9/psutil-6.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ed2440ada7ef7d0d608f20ad89a04ec47d2d3ab7190896cd62ca5fc4fe08bf0", size = 287478 }, + { url = "https://files.pythonhosted.org/packages/19/74/f59e7e0d392bc1070e9a70e2f9190d652487ac115bb16e2eff6b22ad1d24/psutil-6.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd9a97c8e94059b0ef54a7d4baf13b405011176c3b6ff257c247cae0d560ecd", size = 290455 }, + { url = "https://files.pythonhosted.org/packages/cd/5f/60038e277ff0a9cc8f0c9ea3d0c5eb6ee1d2470ea3f9389d776432888e47/psutil-6.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e8d0054fc88153ca0544f5c4d554d42e33df2e009c4ff42284ac9ebdef4132", size = 292046 }, + { url = "https://files.pythonhosted.org/packages/8b/20/2ff69ad9c35c3df1858ac4e094f20bd2374d33c8643cf41da8fd7cdcb78b/psutil-6.0.0-cp37-abi3-win32.whl", hash = "sha256:a495580d6bae27291324fe60cea0b5a7c23fa36a7cd35035a16d93bdcf076b9d", size = 253560 }, + { url = "https://files.pythonhosted.org/packages/73/44/561092313ae925f3acfaace6f9ddc4f6a9c748704317bad9c8c8f8a36a79/psutil-6.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:33ea5e1c975250a720b3a6609c490db40dae5d83a4eb315170c4fe0d8b1f34b3", size = 257399 }, + { url = "https://files.pythonhosted.org/packages/7c/06/63872a64c312a24fb9b4af123ee7007a306617da63ff13bcc1432386ead7/psutil-6.0.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:ffe7fc9b6b36beadc8c322f84e1caff51e8703b88eee1da46d1e3a6ae11b4fd0", size = 251988 }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993 }, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842 }, +] + +[[package]] +name = "pyarrow" +version = "17.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/27/4e/ea6d43f324169f8aec0e57569443a38bab4b398d09769ca64f7b4d467de3/pyarrow-17.0.0.tar.gz", hash = "sha256:4beca9521ed2c0921c1023e68d097d0299b62c362639ea315572a58f3f50fd28", size = 1112479 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/5d/78d4b040bc5ff2fc6c3d03e80fca396b742f6c125b8af06bcf7427f931bc/pyarrow-17.0.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:a5c8b238d47e48812ee577ee20c9a2779e6a5904f1708ae240f53ecbee7c9f07", size = 28994846 }, + { url = "https://files.pythonhosted.org/packages/3b/73/8ed168db7642e91180330e4ea9f3ff8bab404678f00d32d7df0871a4933b/pyarrow-17.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:db023dc4c6cae1015de9e198d41250688383c3f9af8f565370ab2b4cb5f62655", size = 27165908 }, + { url = "https://files.pythonhosted.org/packages/81/36/e78c24be99242063f6d0590ef68c857ea07bdea470242c361e9a15bd57a4/pyarrow-17.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da1e060b3876faa11cee287839f9cc7cdc00649f475714b8680a05fd9071d545", size = 39264209 }, + { url = "https://files.pythonhosted.org/packages/18/4c/3db637d7578f683b0a8fb8999b436bdbedd6e3517bd4f90c70853cf3ad20/pyarrow-17.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75c06d4624c0ad6674364bb46ef38c3132768139ddec1c56582dbac54f2663e2", size = 39862883 }, + { url = "https://files.pythonhosted.org/packages/81/3c/0580626896c842614a523e66b351181ed5bb14e5dfc263cd68cea2c46d90/pyarrow-17.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:fa3c246cc58cb5a4a5cb407a18f193354ea47dd0648194e6265bd24177982fe8", size = 38723009 }, + { url = "https://files.pythonhosted.org/packages/ee/fb/c1b47f0ada36d856a352da261a44d7344d8f22e2f7db3945f8c3b81be5dd/pyarrow-17.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:f7ae2de664e0b158d1607699a16a488de3d008ba99b3a7aa5de1cbc13574d047", size = 39855626 }, + { url = "https://files.pythonhosted.org/packages/19/09/b0a02908180a25d57312ab5919069c39fddf30602568980419f4b02393f6/pyarrow-17.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:5984f416552eea15fd9cee03da53542bf4cddaef5afecefb9aa8d1010c335087", size = 25147242 }, + { url = "https://files.pythonhosted.org/packages/f9/46/ce89f87c2936f5bb9d879473b9663ce7a4b1f4359acc2f0eb39865eaa1af/pyarrow-17.0.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:1c8856e2ef09eb87ecf937104aacfa0708f22dfeb039c363ec99735190ffb977", size = 29028748 }, + { url = "https://files.pythonhosted.org/packages/8d/8e/ce2e9b2146de422f6638333c01903140e9ada244a2a477918a368306c64c/pyarrow-17.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2e19f569567efcbbd42084e87f948778eb371d308e137a0f97afe19bb860ccb3", size = 27190965 }, + { url = "https://files.pythonhosted.org/packages/3b/c8/5675719570eb1acd809481c6d64e2136ffb340bc387f4ca62dce79516cea/pyarrow-17.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b244dc8e08a23b3e352899a006a26ae7b4d0da7bb636872fa8f5884e70acf15", size = 39269081 }, + { url = "https://files.pythonhosted.org/packages/5e/78/3931194f16ab681ebb87ad252e7b8d2c8b23dad49706cadc865dff4a1dd3/pyarrow-17.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b72e87fe3e1db343995562f7fff8aee354b55ee83d13afba65400c178ab2597", size = 39864921 }, + { url = "https://files.pythonhosted.org/packages/d8/81/69b6606093363f55a2a574c018901c40952d4e902e670656d18213c71ad7/pyarrow-17.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:dc5c31c37409dfbc5d014047817cb4ccd8c1ea25d19576acf1a001fe07f5b420", size = 38740798 }, + { url = "https://files.pythonhosted.org/packages/4c/21/9ca93b84b92ef927814cb7ba37f0774a484c849d58f0b692b16af8eebcfb/pyarrow-17.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:e3343cb1e88bc2ea605986d4b94948716edc7a8d14afd4e2c097232f729758b4", size = 39871877 }, + { url = "https://files.pythonhosted.org/packages/30/d1/63a7c248432c71c7d3ee803e706590a0b81ce1a8d2b2ae49677774b813bb/pyarrow-17.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:a27532c38f3de9eb3e90ecab63dfda948a8ca859a66e3a47f5f42d1e403c4d03", size = 25151089 }, + { url = "https://files.pythonhosted.org/packages/d4/62/ce6ac1275a432b4a27c55fe96c58147f111d8ba1ad800a112d31859fae2f/pyarrow-17.0.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:9b8a823cea605221e61f34859dcc03207e52e409ccf6354634143e23af7c8d22", size = 29019418 }, + { url = "https://files.pythonhosted.org/packages/8e/0a/dbd0c134e7a0c30bea439675cc120012337202e5fac7163ba839aa3691d2/pyarrow-17.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f1e70de6cb5790a50b01d2b686d54aaf73da01266850b05e3af2a1bc89e16053", size = 27152197 }, + { url = "https://files.pythonhosted.org/packages/cb/05/3f4a16498349db79090767620d6dc23c1ec0c658a668d61d76b87706c65d/pyarrow-17.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0071ce35788c6f9077ff9ecba4858108eebe2ea5a3f7cf2cf55ebc1dbc6ee24a", size = 39263026 }, + { url = "https://files.pythonhosted.org/packages/c2/0c/ea2107236740be8fa0e0d4a293a095c9f43546a2465bb7df34eee9126b09/pyarrow-17.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:757074882f844411fcca735e39aae74248a1531367a7c80799b4266390ae51cc", size = 39880798 }, + { url = "https://files.pythonhosted.org/packages/f6/b0/b9164a8bc495083c10c281cc65064553ec87b7537d6f742a89d5953a2a3e/pyarrow-17.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:9ba11c4f16976e89146781a83833df7f82077cdab7dc6232c897789343f7891a", size = 38715172 }, + { url = "https://files.pythonhosted.org/packages/f1/c4/9625418a1413005e486c006e56675334929fad864347c5ae7c1b2e7fe639/pyarrow-17.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b0c6ac301093b42d34410b187bba560b17c0330f64907bfa4f7f7f2444b0cf9b", size = 39874508 }, + { url = "https://files.pythonhosted.org/packages/ae/49/baafe2a964f663413be3bd1cf5c45ed98c5e42e804e2328e18f4570027c1/pyarrow-17.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:392bc9feabc647338e6c89267635e111d71edad5fcffba204425a7c8d13610d7", size = 25099235 }, + { url = "https://files.pythonhosted.org/packages/43/e0/a898096d35be240aa61fb2d54db58b86d664b10e1e51256f9300f47565e8/pyarrow-17.0.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:13d7a460b412f31e4c0efa1148e1d29bdf18ad1411eb6757d38f8fbdcc8645fb", size = 29007881 }, + { url = "https://files.pythonhosted.org/packages/59/22/f7d14907ed0697b5dd488d393129f2738629fa5bcba863e00931b7975946/pyarrow-17.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9b564a51fbccfab5a04a80453e5ac6c9954a9c5ef2890d1bcf63741909c3f8df", size = 27178117 }, + { url = "https://files.pythonhosted.org/packages/bf/ee/661211feac0ed48467b1d5c57298c91403809ec3ab78b1d175e1d6ad03cf/pyarrow-17.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32503827abbc5aadedfa235f5ece8c4f8f8b0a3cf01066bc8d29de7539532687", size = 39273896 }, + { url = "https://files.pythonhosted.org/packages/af/61/bcd9b58e38ead6ad42b9ed00da33a3f862bc1d445e3d3164799c25550ac2/pyarrow-17.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a155acc7f154b9ffcc85497509bcd0d43efb80d6f733b0dc3bb14e281f131c8b", size = 39875438 }, + { url = "https://files.pythonhosted.org/packages/75/63/29d1bfcc57af73cde3fc3baccab2f37548de512dbe0ab294b033cd203516/pyarrow-17.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:dec8d129254d0188a49f8a1fc99e0560dc1b85f60af729f47de4046015f9b0a5", size = 38735092 }, + { url = "https://files.pythonhosted.org/packages/39/f4/90258b4de753df7cc61cefb0312f8abcf226672e96cc64996e66afce817a/pyarrow-17.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:a48ddf5c3c6a6c505904545c25a4ae13646ae1f8ba703c4df4a1bfe4f4006bda", size = 39867610 }, + { url = "https://files.pythonhosted.org/packages/e7/f6/b75d4816c32f1618ed31a005ee635dd1d91d8164495d94f2ea092f594661/pyarrow-17.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:42bf93249a083aca230ba7e2786c5f673507fa97bbd9725a1e2754715151a204", size = 25148611 }, +] + +[[package]] +name = "pycodestyle" +version = "2.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/aa/210b2c9aedd8c1cbeea31a50e42050ad56187754b34eb214c46709445801/pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521", size = 39232 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/d8/a211b3f85e99a0daa2ddec96c949cac6824bd305b040571b82a03dd62636/pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3", size = 31284 }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, +] + +[[package]] +name = "pyflakes" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/f9/669d8c9c86613c9d568757c7f5824bd3197d7b1c6c27553bc5618a27cce2/pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f", size = 63788 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/d7/f1b7db88d8e4417c5d47adad627a93547f44bdc9028372dbd2313f34a855/pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a", size = 62725 }, +] + +[[package]] +name = "pygments" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 }, +] + +[[package]] +name = "pyparsing" +version = "3.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/08/13f3bce01b2061f2bbd582c9df82723de943784cf719a35ac886c652043a/pyparsing-3.1.4.tar.gz", hash = "sha256:f86ec8d1a83f11977c9a6ea7598e8c27fc5cddfa5b07ea2241edbbde1d7bc032", size = 900231 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/0c/0e3c05b1c87bb6a1c76d281b0f35e78d2d80ac91b5f8f524cebf77f51049/pyparsing-3.1.4-py3-none-any.whl", hash = "sha256:a6a7ee4235a3f944aa1fa2249307708f893fe5717dc603503c6c7969c070fb7c", size = 104100 }, +] + +[[package]] +name = "pytest" +version = "8.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/6c/62bbd536103af674e227c41a8f3dcd022d591f6eed5facb5a0f31ee33bbc/pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", size = 1442487 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341 }, +] + +[[package]] +name = "pytest-cov" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/67/00efc8d11b630c56f15f4ad9c7f9223f1e5ec275aaae3fa9118c6a223ad2/pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857", size = 63042 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/3a/af5b4fa5961d9a1e6237b530eb87dd04aea6eb83da09d2a4073d81b54ccf/pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652", size = 21990 }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, +] + +[[package]] +name = "pytz" +version = "2024.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/31/3c70bf7603cc2dca0f19bdc53b4537a797747a58875b552c8c413d963a3f/pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a", size = 319692 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/c3/005fcca25ce078d2cc29fd559379817424e94885510568bc1bc53d7d5846/pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725", size = 508002 }, +] + +[[package]] +name = "pywin32" +version = "307" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/3d/91d710c40cc61fd241025351fd61fb674859973c5a0b3111e532d7229012/pywin32-307-cp310-cp310-win32.whl", hash = "sha256:f8f25d893c1e1ce2d685ef6d0a481e87c6f510d0f3f117932781f412e0eba31b", size = 5904291 }, + { url = "https://files.pythonhosted.org/packages/94/b4/20804bb7528419d503c71cfcb8988f0eb9f3596501a9d86eb528c9998055/pywin32-307-cp310-cp310-win_amd64.whl", hash = "sha256:36e650c5e5e6b29b5d317385b02d20803ddbac5d1031e1f88d20d76676dd103d", size = 6535115 }, + { url = "https://files.pythonhosted.org/packages/65/55/f1c84fcccbd5b75c09aa2a948551ad4569f9c14994a39959d3fee3267911/pywin32-307-cp310-cp310-win_arm64.whl", hash = "sha256:0c12d61e0274e0c62acee79e3e503c312426ddd0e8d4899c626cddc1cafe0ff4", size = 7948521 }, + { url = "https://files.pythonhosted.org/packages/f9/29/5f50cb02aef57711bf941e1d93bfe602625f89faf33abb737441ab698496/pywin32-307-cp311-cp311-win32.whl", hash = "sha256:fec5d27cc893178fab299de911b8e4d12c5954e1baf83e8a664311e56a272b75", size = 5905392 }, + { url = "https://files.pythonhosted.org/packages/5e/8d/dd2bf7e5dbfed3ea17b07763bc13d007583ef48914ed446be1c329c8e601/pywin32-307-cp311-cp311-win_amd64.whl", hash = "sha256:987a86971753ed7fdd52a7fb5747aba955b2c7fbbc3d8b76ec850358c1cc28c3", size = 6536159 }, + { url = "https://files.pythonhosted.org/packages/63/72/dce6d08a2adeaf9e7e0462173610900d01d16a449aa74c9e035b7c2ec8f8/pywin32-307-cp311-cp311-win_arm64.whl", hash = "sha256:fd436897c186a2e693cd0437386ed79f989f4d13d6f353f8787ecbb0ae719398", size = 7949586 }, + { url = "https://files.pythonhosted.org/packages/90/4e/9c660fa6c34db3c9542c9682b0ccd9edd63a6a4cb6ac4d22014b2c3355c9/pywin32-307-cp312-cp312-win32.whl", hash = "sha256:07649ec6b01712f36debf39fc94f3d696a46579e852f60157a729ac039df0815", size = 5916997 }, + { url = "https://files.pythonhosted.org/packages/9c/11/c56e771d2cdbd2dac8e656edb2c814e4b2239da2c9028aa7265cdfff8aed/pywin32-307-cp312-cp312-win_amd64.whl", hash = "sha256:00d047992bb5dcf79f8b9b7c81f72e0130f9fe4b22df613f755ab1cc021d8347", size = 6519708 }, + { url = "https://files.pythonhosted.org/packages/cd/64/53b1112cb05f85a6c87339a9f90a3b82d67ecb46f16b45abaac3bf4dee2b/pywin32-307-cp312-cp312-win_arm64.whl", hash = "sha256:b53658acbfc6a8241d72cc09e9d1d666be4e6c99376bc59e26cdb6223c4554d2", size = 7952978 }, + { url = "https://files.pythonhosted.org/packages/61/c2/bdff07ee75b9c0a0f87cd52bfb45152e40d4c6f99e7256336e243cf4da2d/pywin32-307-cp313-cp313-win32.whl", hash = "sha256:ea4d56e48dc1ab2aa0a5e3c0741ad6e926529510516db7a3b6981a1ae74405e5", size = 5915947 }, + { url = "https://files.pythonhosted.org/packages/fd/59/b891cf47d5893ee87e09686e736a84b80a8c5112a1a80e37363ab8801f54/pywin32-307-cp313-cp313-win_amd64.whl", hash = "sha256:576d09813eaf4c8168d0bfd66fb7cb3b15a61041cf41598c2db4a4583bf832d2", size = 6518782 }, + { url = "https://files.pythonhosted.org/packages/08/9b/3c797468a96f68ce86f84917c198f60fc4189ab2ddc5841bcd71ead7680f/pywin32-307-cp313-cp313-win_arm64.whl", hash = "sha256:b30c9bdbffda6a260beb2919f918daced23d32c79109412c2085cbc513338a0a", size = 7952027 }, + { url = "https://files.pythonhosted.org/packages/f1/34/c375ce1a3960f085a93d8ec2e8daf944492d1895336517eac3838777d096/pywin32-307-cp39-cp39-win32.whl", hash = "sha256:55ee87f2f8c294e72ad9d4261ca423022310a6e79fb314a8ca76ab3f493854c6", size = 5968766 }, + { url = "https://files.pythonhosted.org/packages/fd/6a/a6a21b879594a13fe43a67b6ad1e1fc3dcb02579a1708ff8f7264ee7d599/pywin32-307-cp39-cp39-win_amd64.whl", hash = "sha256:e9d5202922e74985b037c9ef46778335c102b74b95cec70f629453dbe7235d87", size = 6616662 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, + { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777 }, + { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318 }, + { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891 }, + { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614 }, + { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360 }, + { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006 }, + { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577 }, + { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593 }, + { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312 }, +] + +[[package]] +name = "pyzmq" +version = "26.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/05/bed626b9f7bb2322cdbbf7b4bd8f54b1b617b0d2ab2d3547d6e39428a48e/pyzmq-26.2.0.tar.gz", hash = "sha256:070672c258581c8e4f640b5159297580a9974b026043bd4ab0470be9ed324f1f", size = 271975 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/a8/9837c39aba390eb7d01924ace49d761c8dbe7bc2d6082346d00c8332e431/pyzmq-26.2.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:ddf33d97d2f52d89f6e6e7ae66ee35a4d9ca6f36eda89c24591b0c40205a3629", size = 1340058 }, + { url = "https://files.pythonhosted.org/packages/a2/1f/a006f2e8e4f7d41d464272012695da17fb95f33b54342612a6890da96ff6/pyzmq-26.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dacd995031a01d16eec825bf30802fceb2c3791ef24bcce48fa98ce40918c27b", size = 1008818 }, + { url = "https://files.pythonhosted.org/packages/b6/09/b51b6683fde5ca04593a57bbe81788b6b43114d8f8ee4e80afc991e14760/pyzmq-26.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89289a5ee32ef6c439086184529ae060c741334b8970a6855ec0b6ad3ff28764", size = 673199 }, + { url = "https://files.pythonhosted.org/packages/c9/78/486f3e2e824f3a645238332bf5a4c4b4477c3063033a27c1e4052358dee2/pyzmq-26.2.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5506f06d7dc6ecf1efacb4a013b1f05071bb24b76350832c96449f4a2d95091c", size = 911762 }, + { url = "https://files.pythonhosted.org/packages/5e/3b/2eb1667c9b866f53e76ee8b0c301b0469745a23bd5a87b7ee3d5dd9eb6e5/pyzmq-26.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ea039387c10202ce304af74def5021e9adc6297067f3441d348d2b633e8166a", size = 868773 }, + { url = "https://files.pythonhosted.org/packages/16/29/ca99b4598a9dc7e468b5417eda91f372b595be1e3eec9b7cbe8e5d3584e8/pyzmq-26.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a2224fa4a4c2ee872886ed00a571f5e967c85e078e8e8c2530a2fb01b3309b88", size = 868834 }, + { url = "https://files.pythonhosted.org/packages/ad/e5/9efaeb1d2f4f8c50da04144f639b042bc52869d3a206d6bf672ab3522163/pyzmq-26.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:28ad5233e9c3b52d76196c696e362508959741e1a005fb8fa03b51aea156088f", size = 1202861 }, + { url = "https://files.pythonhosted.org/packages/c3/62/c721b5608a8ac0a69bb83cbb7d07a56f3ff00b3991a138e44198a16f94c7/pyzmq-26.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:1c17211bc037c7d88e85ed8b7d8f7e52db6dc8eca5590d162717c654550f7282", size = 1515304 }, + { url = "https://files.pythonhosted.org/packages/87/84/e8bd321aa99b72f48d4606fc5a0a920154125bd0a4608c67eab742dab087/pyzmq-26.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b8f86dd868d41bea9a5f873ee13bf5551c94cf6bc51baebc6f85075971fe6eea", size = 1414712 }, + { url = "https://files.pythonhosted.org/packages/cd/cd/420e3fd1ac6977b008b72e7ad2dae6350cc84d4c5027fc390b024e61738f/pyzmq-26.2.0-cp310-cp310-win32.whl", hash = "sha256:46a446c212e58456b23af260f3d9fb785054f3e3653dbf7279d8f2b5546b21c2", size = 578113 }, + { url = "https://files.pythonhosted.org/packages/5c/57/73930d56ed45ae0cb4946f383f985c855c9b3d4063f26416998f07523c0e/pyzmq-26.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:49d34ab71db5a9c292a7644ce74190b1dd5a3475612eefb1f8be1d6961441971", size = 641631 }, + { url = "https://files.pythonhosted.org/packages/61/d2/ae6ac5c397f1ccad59031c64beaafce7a0d6182e0452cc48f1c9c87d2dd0/pyzmq-26.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:bfa832bfa540e5b5c27dcf5de5d82ebc431b82c453a43d141afb1e5d2de025fa", size = 543528 }, + { url = "https://files.pythonhosted.org/packages/12/20/de7442172f77f7c96299a0ac70e7d4fb78cd51eca67aa2cf552b66c14196/pyzmq-26.2.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:8f7e66c7113c684c2b3f1c83cdd3376103ee0ce4c49ff80a648643e57fb22218", size = 1340639 }, + { url = "https://files.pythonhosted.org/packages/98/4d/5000468bd64c7910190ed0a6c76a1ca59a68189ec1f007c451dc181a22f4/pyzmq-26.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3a495b30fc91db2db25120df5847d9833af237546fd59170701acd816ccc01c4", size = 1008710 }, + { url = "https://files.pythonhosted.org/packages/e1/bf/c67fd638c2f9fbbab8090a3ee779370b97c82b84cc12d0c498b285d7b2c0/pyzmq-26.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77eb0968da535cba0470a5165468b2cac7772cfb569977cff92e240f57e31bef", size = 673129 }, + { url = "https://files.pythonhosted.org/packages/86/94/99085a3f492aa538161cbf27246e8886ff850e113e0c294a5b8245f13b52/pyzmq-26.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ace4f71f1900a548f48407fc9be59c6ba9d9aaf658c2eea6cf2779e72f9f317", size = 910107 }, + { url = "https://files.pythonhosted.org/packages/31/1d/346809e8a9b999646d03f21096428453465b1bca5cd5c64ecd048d9ecb01/pyzmq-26.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92a78853d7280bffb93df0a4a6a2498cba10ee793cc8076ef797ef2f74d107cf", size = 867960 }, + { url = "https://files.pythonhosted.org/packages/ab/68/6fb6ae5551846ad5beca295b7bca32bf0a7ce19f135cb30e55fa2314e6b6/pyzmq-26.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:689c5d781014956a4a6de61d74ba97b23547e431e9e7d64f27d4922ba96e9d6e", size = 869204 }, + { url = "https://files.pythonhosted.org/packages/0f/f9/18417771dee223ccf0f48e29adf8b4e25ba6d0e8285e33bcbce078070bc3/pyzmq-26.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0aca98bc423eb7d153214b2df397c6421ba6373d3397b26c057af3c904452e37", size = 1203351 }, + { url = "https://files.pythonhosted.org/packages/e0/46/f13e67fe0d4f8a2315782cbad50493de6203ea0d744610faf4d5f5b16e90/pyzmq-26.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f3496d76b89d9429a656293744ceca4d2ac2a10ae59b84c1da9b5165f429ad3", size = 1514204 }, + { url = "https://files.pythonhosted.org/packages/50/11/ddcf7343b7b7a226e0fc7b68cbf5a5bb56291fac07f5c3023bb4c319ebb4/pyzmq-26.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5c2b3bfd4b9689919db068ac6c9911f3fcb231c39f7dd30e3138be94896d18e6", size = 1414339 }, + { url = "https://files.pythonhosted.org/packages/01/14/1c18d7d5b7be2708f513f37c61bfadfa62161c10624f8733f1c8451b3509/pyzmq-26.2.0-cp311-cp311-win32.whl", hash = "sha256:eac5174677da084abf378739dbf4ad245661635f1600edd1221f150b165343f4", size = 576928 }, + { url = "https://files.pythonhosted.org/packages/3b/1b/0a540edd75a41df14ec416a9a500b9fec66e554aac920d4c58fbd5756776/pyzmq-26.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:5a509df7d0a83a4b178d0f937ef14286659225ef4e8812e05580776c70e155d5", size = 642317 }, + { url = "https://files.pythonhosted.org/packages/98/77/1cbfec0358078a4c5add529d8a70892db1be900980cdb5dd0898b3d6ab9d/pyzmq-26.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:c0e6091b157d48cbe37bd67233318dbb53e1e6327d6fc3bb284afd585d141003", size = 543834 }, + { url = "https://files.pythonhosted.org/packages/28/2f/78a766c8913ad62b28581777ac4ede50c6d9f249d39c2963e279524a1bbe/pyzmq-26.2.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:ded0fc7d90fe93ae0b18059930086c51e640cdd3baebdc783a695c77f123dcd9", size = 1343105 }, + { url = "https://files.pythonhosted.org/packages/b7/9c/4b1e2d3d4065be715e007fe063ec7885978fad285f87eae1436e6c3201f4/pyzmq-26.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:17bf5a931c7f6618023cdacc7081f3f266aecb68ca692adac015c383a134ca52", size = 1008365 }, + { url = "https://files.pythonhosted.org/packages/4f/ef/5a23ec689ff36d7625b38d121ef15abfc3631a9aecb417baf7a4245e4124/pyzmq-26.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55cf66647e49d4621a7e20c8d13511ef1fe1efbbccf670811864452487007e08", size = 665923 }, + { url = "https://files.pythonhosted.org/packages/ae/61/d436461a47437d63c6302c90724cf0981883ec57ceb6073873f32172d676/pyzmq-26.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4661c88db4a9e0f958c8abc2b97472e23061f0bc737f6f6179d7a27024e1faa5", size = 903400 }, + { url = "https://files.pythonhosted.org/packages/47/42/fc6d35ecefe1739a819afaf6f8e686f7f02a4dd241c78972d316f403474c/pyzmq-26.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea7f69de383cb47522c9c208aec6dd17697db7875a4674c4af3f8cfdac0bdeae", size = 860034 }, + { url = "https://files.pythonhosted.org/packages/07/3b/44ea6266a6761e9eefaa37d98fabefa112328808ac41aa87b4bbb668af30/pyzmq-26.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:7f98f6dfa8b8ccaf39163ce872bddacca38f6a67289116c8937a02e30bbe9711", size = 860579 }, + { url = "https://files.pythonhosted.org/packages/38/6f/4df2014ab553a6052b0e551b37da55166991510f9e1002c89cab7ce3b3f2/pyzmq-26.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e3e0210287329272539eea617830a6a28161fbbd8a3271bf4150ae3e58c5d0e6", size = 1196246 }, + { url = "https://files.pythonhosted.org/packages/38/9d/ee240fc0c9fe9817f0c9127a43238a3e28048795483c403cc10720ddef22/pyzmq-26.2.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6b274e0762c33c7471f1a7471d1a2085b1a35eba5cdc48d2ae319f28b6fc4de3", size = 1507441 }, + { url = "https://files.pythonhosted.org/packages/85/4f/01711edaa58d535eac4a26c294c617c9a01f09857c0ce191fd574d06f359/pyzmq-26.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:29c6a4635eef69d68a00321e12a7d2559fe2dfccfa8efae3ffb8e91cd0b36a8b", size = 1406498 }, + { url = "https://files.pythonhosted.org/packages/07/18/907134c85c7152f679ed744e73e645b365f3ad571f38bdb62e36f347699a/pyzmq-26.2.0-cp312-cp312-win32.whl", hash = "sha256:989d842dc06dc59feea09e58c74ca3e1678c812a4a8a2a419046d711031f69c7", size = 575533 }, + { url = "https://files.pythonhosted.org/packages/ce/2c/a6f4a20202a4d3c582ad93f95ee78d79bbdc26803495aec2912b17dbbb6c/pyzmq-26.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:2a50625acdc7801bc6f74698c5c583a491c61d73c6b7ea4dee3901bb99adb27a", size = 637768 }, + { url = "https://files.pythonhosted.org/packages/5f/0e/eb16ff731632d30554bf5af4dbba3ffcd04518219d82028aea4ae1b02ca5/pyzmq-26.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:4d29ab8592b6ad12ebbf92ac2ed2bedcfd1cec192d8e559e2e099f648570e19b", size = 540675 }, + { url = "https://files.pythonhosted.org/packages/04/a7/0f7e2f6c126fe6e62dbae0bc93b1bd3f1099cf7fea47a5468defebe3f39d/pyzmq-26.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9dd8cd1aeb00775f527ec60022004d030ddc51d783d056e3e23e74e623e33726", size = 1006564 }, + { url = "https://files.pythonhosted.org/packages/31/b6/a187165c852c5d49f826a690857684333a6a4a065af0a6015572d2284f6a/pyzmq-26.2.0-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:28c812d9757fe8acecc910c9ac9dafd2ce968c00f9e619db09e9f8f54c3a68a3", size = 1340447 }, + { url = "https://files.pythonhosted.org/packages/68/ba/f4280c58ff71f321602a6e24fd19879b7e79793fb8ab14027027c0fb58ef/pyzmq-26.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d80b1dd99c1942f74ed608ddb38b181b87476c6a966a88a950c7dee118fdf50", size = 665485 }, + { url = "https://files.pythonhosted.org/packages/77/b5/c987a5c53c7d8704216f29fc3d810b32f156bcea488a940e330e1bcbb88d/pyzmq-26.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c997098cc65e3208eca09303630e84d42718620e83b733d0fd69543a9cab9cb", size = 903484 }, + { url = "https://files.pythonhosted.org/packages/29/c9/07da157d2db18c72a7eccef8e684cefc155b712a88e3d479d930aa9eceba/pyzmq-26.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ad1bc8d1b7a18497dda9600b12dc193c577beb391beae5cd2349184db40f187", size = 859981 }, + { url = "https://files.pythonhosted.org/packages/43/09/e12501bd0b8394b7d02c41efd35c537a1988da67fc9c745cae9c6c776d31/pyzmq-26.2.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:bea2acdd8ea4275e1278350ced63da0b166421928276c7c8e3f9729d7402a57b", size = 860334 }, + { url = "https://files.pythonhosted.org/packages/eb/ff/f5ec1d455f8f7385cc0a8b2acd8c807d7fade875c14c44b85c1bddabae21/pyzmq-26.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:23f4aad749d13698f3f7b64aad34f5fc02d6f20f05999eebc96b89b01262fb18", size = 1196179 }, + { url = "https://files.pythonhosted.org/packages/ec/8a/bb2ac43295b1950fe436a81fc5b298be0b96ac76fb029b514d3ed58f7b27/pyzmq-26.2.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:a4f96f0d88accc3dbe4a9025f785ba830f968e21e3e2c6321ccdfc9aef755115", size = 1507668 }, + { url = "https://files.pythonhosted.org/packages/a9/49/dbc284ebcfd2dca23f6349227ff1616a7ee2c4a35fe0a5d6c3deff2b4fed/pyzmq-26.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ced65e5a985398827cc9276b93ef6dfabe0273c23de8c7931339d7e141c2818e", size = 1406539 }, + { url = "https://files.pythonhosted.org/packages/00/68/093cdce3fe31e30a341d8e52a1ad86392e13c57970d722c1f62a1d1a54b6/pyzmq-26.2.0-cp313-cp313-win32.whl", hash = "sha256:31507f7b47cc1ead1f6e86927f8ebb196a0bab043f6345ce070f412a59bf87b5", size = 575567 }, + { url = "https://files.pythonhosted.org/packages/92/ae/6cc4657148143412b5819b05e362ae7dd09fb9fe76e2a539dcff3d0386bc/pyzmq-26.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:70fc7fcf0410d16ebdda9b26cbd8bf8d803d220a7f3522e060a69a9c87bf7bad", size = 637551 }, + { url = "https://files.pythonhosted.org/packages/6c/67/fbff102e201688f97c8092e4c3445d1c1068c2f27bbd45a578df97ed5f94/pyzmq-26.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:c3789bd5768ab5618ebf09cef6ec2b35fed88709b104351748a63045f0ff9797", size = 540378 }, + { url = "https://files.pythonhosted.org/packages/3f/fe/2d998380b6e0122c6c4bdf9b6caf490831e5f5e2d08a203b5adff060c226/pyzmq-26.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:034da5fc55d9f8da09015d368f519478a52675e558c989bfcb5cf6d4e16a7d2a", size = 1007378 }, + { url = "https://files.pythonhosted.org/packages/4a/f4/30d6e7157f12b3a0390bde94d6a8567cdb88846ed068a6e17238a4ccf600/pyzmq-26.2.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:c92d73464b886931308ccc45b2744e5968cbaade0b1d6aeb40d8ab537765f5bc", size = 1329532 }, + { url = "https://files.pythonhosted.org/packages/82/86/3fe917870e15ee1c3ad48229a2a64458e36036e64b4afa9659045d82bfa8/pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:794a4562dcb374f7dbbfb3f51d28fb40123b5a2abadee7b4091f93054909add5", size = 653242 }, + { url = "https://files.pythonhosted.org/packages/50/2d/242e7e6ef6c8c19e6cb52d095834508cd581ffb925699fd3c640cdc758f1/pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aee22939bb6075e7afededabad1a56a905da0b3c4e3e0c45e75810ebe3a52672", size = 888404 }, + { url = "https://files.pythonhosted.org/packages/ac/11/7270566e1f31e4ea73c81ec821a4b1688fd551009a3d2bab11ec66cb1e8f/pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ae90ff9dad33a1cfe947d2c40cb9cb5e600d759ac4f0fd22616ce6540f72797", size = 845858 }, + { url = "https://files.pythonhosted.org/packages/91/d5/72b38fbc69867795c8711bdd735312f9fef1e3d9204e2f63ab57085434b9/pyzmq-26.2.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:43a47408ac52647dfabbc66a25b05b6a61700b5165807e3fbd40063fcaf46386", size = 847375 }, + { url = "https://files.pythonhosted.org/packages/dd/9a/10ed3c7f72b4c24e719c59359fbadd1a27556a28b36cdf1cd9e4fb7845d5/pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:25bf2374a2a8433633c65ccb9553350d5e17e60c8eb4de4d92cc6bd60f01d306", size = 1183489 }, + { url = "https://files.pythonhosted.org/packages/72/2d/8660892543fabf1fe41861efa222455811adac9f3c0818d6c3170a1153e3/pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_i686.whl", hash = "sha256:007137c9ac9ad5ea21e6ad97d3489af654381324d5d3ba614c323f60dab8fae6", size = 1492932 }, + { url = "https://files.pythonhosted.org/packages/7b/d6/32fd69744afb53995619bc5effa2a405ae0d343cd3e747d0fbc43fe894ee/pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:470d4a4f6d48fb34e92d768b4e8a5cc3780db0d69107abf1cd7ff734b9766eb0", size = 1392485 }, + { url = "https://files.pythonhosted.org/packages/ac/9e/ad5fbbe1bcc7a9d1e8c5f4f7de48f2c1dc481e151ef80cc1ce9a7fe67b55/pyzmq-26.2.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:b1d464cb8d72bfc1a3adc53305a63a8e0cac6bc8c5a07e8ca190ab8d3faa43c2", size = 1341256 }, + { url = "https://files.pythonhosted.org/packages/4c/d9/d7a8022108c214803a82b0b69d4885cee00933d21928f1f09dca371cf4bf/pyzmq-26.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4da04c48873a6abdd71811c5e163bd656ee1b957971db7f35140a2d573f6949c", size = 1009385 }, + { url = "https://files.pythonhosted.org/packages/ed/69/0529b59ac667ea8bfe8796ac71796b688fbb42ff78e06525dabfed3bc7ae/pyzmq-26.2.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d049df610ac811dcffdc147153b414147428567fbbc8be43bb8885f04db39d98", size = 908009 }, + { url = "https://files.pythonhosted.org/packages/6e/bd/3ff3e1172f12f55769793a3a334e956ec2886805ebfb2f64756b6b5c6a1a/pyzmq-26.2.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:05590cdbc6b902101d0e65d6a4780af14dc22914cc6ab995d99b85af45362cc9", size = 862078 }, + { url = "https://files.pythonhosted.org/packages/c3/ec/ab13585c3a1f48e2874253844c47b194d56eb25c94718691349c646f336f/pyzmq-26.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c811cfcd6a9bf680236c40c6f617187515269ab2912f3d7e8c0174898e2519db", size = 673756 }, + { url = "https://files.pythonhosted.org/packages/1e/be/febcd4b04dd50ee6d514dfbc33a3d5d9cb38ec9516e02bbfc929baa0f141/pyzmq-26.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6835dd60355593de10350394242b5757fbbd88b25287314316f266e24c61d073", size = 1203684 }, + { url = "https://files.pythonhosted.org/packages/16/28/304150e71afd2df3b82f52f66c0d8ab9ac6fe1f1ffdf92bad4c8cc91d557/pyzmq-26.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc6bee759a6bddea5db78d7dcd609397449cb2d2d6587f48f3ca613b19410cfc", size = 1515864 }, + { url = "https://files.pythonhosted.org/packages/18/89/8d48d8cd505c12a1f5edee597cc32ffcedc65fd8d2603aebaaedc38a7041/pyzmq-26.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c530e1eecd036ecc83c3407f77bb86feb79916d4a33d11394b8234f3bd35b940", size = 1415383 }, + { url = "https://files.pythonhosted.org/packages/d4/7e/43a60c3b179f7da0cbc2b649bd2702fd6a39bff5f72aa38d6e1aeb00256d/pyzmq-26.2.0-cp39-cp39-win32.whl", hash = "sha256:367b4f689786fca726ef7a6c5ba606958b145b9340a5e4808132cc65759abd44", size = 578540 }, + { url = "https://files.pythonhosted.org/packages/3a/55/8841dcd28f783ad06674c8fe8d7d72794b548d0bff8829aaafeb72e8b44d/pyzmq-26.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:e6fa2e3e683f34aea77de8112f6483803c96a44fd726d7358b9888ae5bb394ec", size = 642147 }, + { url = "https://files.pythonhosted.org/packages/b4/78/b3c31ccfcfcdd6ea50b6abc8f46a2a7aadb9c3d40531d1b908d834aaa12e/pyzmq-26.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:7445be39143a8aa4faec43b076e06944b8f9d0701b669df4af200531b21e40bb", size = 543903 }, + { url = "https://files.pythonhosted.org/packages/53/fb/36b2b2548286e9444e52fcd198760af99fd89102b5be50f0660fcfe902df/pyzmq-26.2.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:706e794564bec25819d21a41c31d4df2d48e1cc4b061e8d345d7fb4dd3e94072", size = 906955 }, + { url = "https://files.pythonhosted.org/packages/77/8f/6ce54f8979a01656e894946db6299e2273fcee21c8e5fa57c6295ef11f57/pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b435f2753621cd36e7c1762156815e21c985c72b19135dac43a7f4f31d28dd1", size = 565701 }, + { url = "https://files.pythonhosted.org/packages/ee/1c/bf8cd66730a866b16db8483286078892b7f6536f8c389fb46e4beba0a970/pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:160c7e0a5eb178011e72892f99f918c04a131f36056d10d9c1afb223fc952c2d", size = 794312 }, + { url = "https://files.pythonhosted.org/packages/71/43/91fa4ff25bbfdc914ab6bafa0f03241d69370ef31a761d16bb859f346582/pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c4a71d5d6e7b28a47a394c0471b7e77a0661e2d651e7ae91e0cab0a587859ca", size = 752775 }, + { url = "https://files.pythonhosted.org/packages/ec/d2/3b2ab40f455a256cb6672186bea95cd97b459ce4594050132d71e76f0d6f/pyzmq-26.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:90412f2db8c02a3864cbfc67db0e3dcdbda336acf1c469526d3e869394fe001c", size = 550762 }, + { url = "https://files.pythonhosted.org/packages/6c/78/3096d72581365dfb0081ac9512a3b53672fa69854aa174d78636510c4db8/pyzmq-26.2.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cdeabcff45d1c219636ee2e54d852262e5c2e085d6cb476d938aee8d921356b3", size = 906945 }, + { url = "https://files.pythonhosted.org/packages/da/f2/8054574d77c269c31d055d4daf3d8407adf61ea384a50c8d14b158551d09/pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35cffef589bcdc587d06f9149f8d5e9e8859920a071df5a2671de2213bef592a", size = 565698 }, + { url = "https://files.pythonhosted.org/packages/77/21/c3ad93236d1d60eea10b67528f55e7db115a9d32e2bf163fcf601f85e9cc/pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18c8dc3b7468d8b4bdf60ce9d7141897da103c7a4690157b32b60acb45e333e6", size = 794307 }, + { url = "https://files.pythonhosted.org/packages/6a/49/e95b491724500fcb760178ce8db39b923429e328e57bcf9162e32c2c187c/pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7133d0a1677aec369d67dd78520d3fa96dd7f3dcec99d66c1762870e5ea1a50a", size = 752769 }, + { url = "https://files.pythonhosted.org/packages/9b/a9/50c9c06762b30792f71aaad8d1886748d39c4bffedc1171fbc6ad2b92d67/pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6a96179a24b14fa6428cbfc08641c779a53f8fcec43644030328f44034c7f1f4", size = 751338 }, + { url = "https://files.pythonhosted.org/packages/ca/63/27e6142b4f67a442ee480986ca5b88edb01462dd2319843057683a5148bd/pyzmq-26.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4f78c88905461a9203eac9faac157a2a0dbba84a0fd09fd29315db27be40af9f", size = 550757 }, +] + +[[package]] +name = "recommonmark" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "commonmark" }, + { name = "docutils" }, + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/00/3dd2bdc4184b0ce754b5b446325abf45c2e0a347e022292ddc44670f628c/recommonmark-0.7.1.tar.gz", hash = "sha256:bdb4db649f2222dcd8d2d844f0006b958d627f732415d399791ee436a3686d67", size = 34444 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/77/ed589c75db5d02a77a1d5d2d9abc63f29676467d396c64277f98b50b79c2/recommonmark-0.7.1-py2.py3-none-any.whl", hash = "sha256:1b1db69af0231efce3fa21b94ff627ea33dee7079a01dd0a7f8482c3da148b3f", size = 10214 }, +] + +[[package]] +name = "referencing" +version = "0.35.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/5b/73ca1f8e72fff6fa52119dbd185f73a907b1989428917b24cff660129b6d/referencing-0.35.1.tar.gz", hash = "sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c", size = 62991 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/59/2056f61236782a2c86b33906c025d4f4a0b17be0161b63b70fd9e8775d36/referencing-0.35.1-py3-none-any.whl", hash = "sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de", size = 26684 }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + +[[package]] +name = "rpds-py" +version = "0.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/55/64/b693f262791b818880d17268f3f8181ef799b0d187f6f731b1772e05a29a/rpds_py-0.20.0.tar.gz", hash = "sha256:d72a210824facfdaf8768cf2d7ca25a042c30320b3020de2fa04640920d4e121", size = 25814 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/2d/a7e60483b72b91909e18f29a5c5ae847bac4e2ae95b77bb77e1f41819a58/rpds_py-0.20.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3ad0fda1635f8439cde85c700f964b23ed5fc2d28016b32b9ee5fe30da5c84e2", size = 318432 }, + { url = "https://files.pythonhosted.org/packages/b5/b4/f15b0c55a6d880ce74170e7e28c3ed6c5acdbbd118df50b91d1dabf86008/rpds_py-0.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9bb4a0d90fdb03437c109a17eade42dfbf6190408f29b2744114d11586611d6f", size = 311333 }, + { url = "https://files.pythonhosted.org/packages/36/10/3f4e490fe6eb069c07c22357d0b4804cd94cb9f8d01345ef9b1d93482b9d/rpds_py-0.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6377e647bbfd0a0b159fe557f2c6c602c159fc752fa316572f012fc0bf67150", size = 366697 }, + { url = "https://files.pythonhosted.org/packages/f5/c8/cd6ab31b4424c7fab3b17e153b6ea7d1bb0d7cabea5c1ef683cc8adb8bc2/rpds_py-0.20.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb851b7df9dda52dc1415ebee12362047ce771fc36914586b2e9fcbd7d293b3e", size = 368386 }, + { url = "https://files.pythonhosted.org/packages/60/5e/642a44fda6dda90b5237af7a0ef1d088159c30a504852b94b0396eb62125/rpds_py-0.20.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e0f80b739e5a8f54837be5d5c924483996b603d5502bfff79bf33da06164ee2", size = 395374 }, + { url = "https://files.pythonhosted.org/packages/7c/b5/ff18c093c9e72630f6d6242e5ccb0728ef8265ba0a154b5972f89d23790a/rpds_py-0.20.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a8c94dad2e45324fc74dce25e1645d4d14df9a4e54a30fa0ae8bad9a63928e3", size = 433189 }, + { url = "https://files.pythonhosted.org/packages/4a/6d/1166a157b227f2333f8e8ae320b6b7ea2a6a38fbe7a3563ad76dffc8608d/rpds_py-0.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8e604fe73ba048c06085beaf51147eaec7df856824bfe7b98657cf436623daf", size = 354849 }, + { url = "https://files.pythonhosted.org/packages/70/a4/70ea49863ea09ae4c2971f2eef58e80b757e3c0f2f618c5815bb751f7847/rpds_py-0.20.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:df3de6b7726b52966edf29663e57306b23ef775faf0ac01a3e9f4012a24a4140", size = 373233 }, + { url = "https://files.pythonhosted.org/packages/3b/d3/822a28152a1e7e2ba0dc5d06cf8736f4cd64b191bb6ec47fb51d1c3c5ccf/rpds_py-0.20.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf258ede5bc22a45c8e726b29835b9303c285ab46fc7c3a4cc770736b5304c9f", size = 541852 }, + { url = "https://files.pythonhosted.org/packages/c6/a5/6ef91e4425dc8b3445ff77d292fc4c5e37046462434a0423c4e0a596a8bd/rpds_py-0.20.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:55fea87029cded5df854ca7e192ec7bdb7ecd1d9a3f63d5c4eb09148acf4a7ce", size = 547630 }, + { url = "https://files.pythonhosted.org/packages/72/f8/d5625ee05c4e5c478954a16d9359069c66fe8ac8cd5ddf28f80d3b321837/rpds_py-0.20.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ae94bd0b2f02c28e199e9bc51485d0c5601f58780636185660f86bf80c89af94", size = 525766 }, + { url = "https://files.pythonhosted.org/packages/94/3c/1ff1ed6ae323b3e16fdfcdae0f0a67f373a6c3d991229dc32b499edeffb7/rpds_py-0.20.0-cp310-none-win32.whl", hash = "sha256:28527c685f237c05445efec62426d285e47a58fb05ba0090a4340b73ecda6dee", size = 199174 }, + { url = "https://files.pythonhosted.org/packages/ec/ba/5762c0aee2403dfea14ed74b0f8a2415cfdbb21cf745d600d9a8ac952c5b/rpds_py-0.20.0-cp310-none-win_amd64.whl", hash = "sha256:238a2d5b1cad28cdc6ed15faf93a998336eb041c4e440dd7f902528b8891b399", size = 213543 }, + { url = "https://files.pythonhosted.org/packages/ab/2a/191374c52d7be0b056cc2a04d718d2244c152f915d4a8d2db2aacc526189/rpds_py-0.20.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac2f4f7a98934c2ed6505aead07b979e6f999389f16b714448fb39bbaa86a489", size = 318369 }, + { url = "https://files.pythonhosted.org/packages/0e/6a/2c9fdcc6d235ac0d61ec4fd9981184689c3e682abd05e3caa49bccb9c298/rpds_py-0.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:220002c1b846db9afd83371d08d239fdc865e8f8c5795bbaec20916a76db3318", size = 311303 }, + { url = "https://files.pythonhosted.org/packages/d2/b2/725487d29633f64ef8f9cbf4729111a0b61702c8f8e94db1653930f52cce/rpds_py-0.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d7919548df3f25374a1f5d01fbcd38dacab338ef5f33e044744b5c36729c8db", size = 366424 }, + { url = "https://files.pythonhosted.org/packages/7a/8c/668195ab9226d01b7cf7cd9e59c1c0be1df05d602df7ec0cf46f857dcf59/rpds_py-0.20.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:758406267907b3781beee0f0edfe4a179fbd97c0be2e9b1154d7f0a1279cf8e5", size = 368359 }, + { url = "https://files.pythonhosted.org/packages/52/28/356f6a39c1adeb02cf3e5dd526f5e8e54e17899bef045397abcfbf50dffa/rpds_py-0.20.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3d61339e9f84a3f0767b1995adfb171a0d00a1185192718a17af6e124728e0f5", size = 394886 }, + { url = "https://files.pythonhosted.org/packages/a2/65/640fb1a89080a8fb6f4bebd3dafb65a2edba82e2e44c33e6eb0f3e7956f1/rpds_py-0.20.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1259c7b3705ac0a0bd38197565a5d603218591d3f6cee6e614e380b6ba61c6f6", size = 432416 }, + { url = "https://files.pythonhosted.org/packages/a7/e8/85835077b782555d6b3416874b702ea6ebd7db1f145283c9252968670dd5/rpds_py-0.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c1dc0f53856b9cc9a0ccca0a7cc61d3d20a7088201c0937f3f4048c1718a209", size = 354819 }, + { url = "https://files.pythonhosted.org/packages/4f/87/1ac631e923d65cbf36fbcfc6eaa702a169496de1311e54be142f178e53ee/rpds_py-0.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7e60cb630f674a31f0368ed32b2a6b4331b8350d67de53c0359992444b116dd3", size = 373282 }, + { url = "https://files.pythonhosted.org/packages/e4/ce/cb316f7970189e217b998191c7cf0da2ede3d5437932c86a7210dc1e9994/rpds_py-0.20.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dbe982f38565bb50cb7fb061ebf762c2f254ca3d8c20d4006878766e84266272", size = 541540 }, + { url = "https://files.pythonhosted.org/packages/90/d7/4112d7655ec8aff168ecc91d4ceb51c557336edde7e6ccf6463691a2f253/rpds_py-0.20.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:514b3293b64187172bc77c8fb0cdae26981618021053b30d8371c3a902d4d5ad", size = 547640 }, + { url = "https://files.pythonhosted.org/packages/ab/44/4f61d64dfed98cc71623f3a7fcb612df636a208b4b2c6611eaa985e130a9/rpds_py-0.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d0a26ffe9d4dd35e4dfdd1e71f46401cff0181c75ac174711ccff0459135fa58", size = 525555 }, + { url = "https://files.pythonhosted.org/packages/35/f2/a862d81eacb21f340d584cd1c749c289979f9a60e9229f78bffc0418a199/rpds_py-0.20.0-cp311-none-win32.whl", hash = "sha256:89c19a494bf3ad08c1da49445cc5d13d8fefc265f48ee7e7556839acdacf69d0", size = 199338 }, + { url = "https://files.pythonhosted.org/packages/cc/ec/77d0674f9af4872919f3738018558dd9d37ad3f7ad792d062eadd4af7cba/rpds_py-0.20.0-cp311-none-win_amd64.whl", hash = "sha256:c638144ce971df84650d3ed0096e2ae7af8e62ecbbb7b201c8935c370df00a2c", size = 213585 }, + { url = "https://files.pythonhosted.org/packages/89/b7/f9682c5cc37fcc035f4a0fc33c1fe92ec9cbfdee0cdfd071cf948f53e0df/rpds_py-0.20.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a84ab91cbe7aab97f7446652d0ed37d35b68a465aeef8fc41932a9d7eee2c1a6", size = 321468 }, + { url = "https://files.pythonhosted.org/packages/b8/ad/fc82be4eaceb8d444cb6fc1956ce972b3a0795104279de05e0e4131d0a47/rpds_py-0.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:56e27147a5a4c2c21633ff8475d185734c0e4befd1c989b5b95a5d0db699b21b", size = 313062 }, + { url = "https://files.pythonhosted.org/packages/0e/1c/6039e80b13a08569a304dc13476dc986352dca4598e909384db043b4e2bb/rpds_py-0.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2580b0c34583b85efec8c5c5ec9edf2dfe817330cc882ee972ae650e7b5ef739", size = 370168 }, + { url = "https://files.pythonhosted.org/packages/dc/c9/5b9aa35acfb58946b4b785bc8e700ac313669e02fb100f3efa6176a83e81/rpds_py-0.20.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b80d4a7900cf6b66bb9cee5c352b2d708e29e5a37fe9bf784fa97fc11504bf6c", size = 371376 }, + { url = "https://files.pythonhosted.org/packages/7b/dd/0e0dbeb70d8a5357d2814764d467ded98d81d90d3570de4fb05ec7224f6b/rpds_py-0.20.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50eccbf054e62a7b2209b28dc7a22d6254860209d6753e6b78cfaeb0075d7bee", size = 397200 }, + { url = "https://files.pythonhosted.org/packages/e4/da/a47d931eb688ccfd77a7389e45935c79c41e8098d984d87335004baccb1d/rpds_py-0.20.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:49a8063ea4296b3a7e81a5dfb8f7b2d73f0b1c20c2af401fb0cdf22e14711a96", size = 426824 }, + { url = "https://files.pythonhosted.org/packages/0f/f7/a59a673594e6c2ff2dbc44b00fd4ecdec2fc399bb6a7bd82d612699a0121/rpds_py-0.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea438162a9fcbee3ecf36c23e6c68237479f89f962f82dae83dc15feeceb37e4", size = 357967 }, + { url = "https://files.pythonhosted.org/packages/5f/61/3ba1905396b2cb7088f9503a460b87da33452da54d478cb9241f6ad16d00/rpds_py-0.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:18d7585c463087bddcfa74c2ba267339f14f2515158ac4db30b1f9cbdb62c8ef", size = 378905 }, + { url = "https://files.pythonhosted.org/packages/08/31/6d0df9356b4edb0a3a077f1ef714e25ad21f9f5382fc490c2383691885ea/rpds_py-0.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d4c7d1a051eeb39f5c9547e82ea27cbcc28338482242e3e0b7768033cb083821", size = 546348 }, + { url = "https://files.pythonhosted.org/packages/ae/15/d33c021de5cb793101df9961c3c746dfc476953dbbf5db337d8010dffd4e/rpds_py-0.20.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4df1e3b3bec320790f699890d41c59d250f6beda159ea3c44c3f5bac1976940", size = 553152 }, + { url = "https://files.pythonhosted.org/packages/70/2d/5536d28c507a4679179ab15aa0049440e4d3dd6752050fa0843ed11e9354/rpds_py-0.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2cf126d33a91ee6eedc7f3197b53e87a2acdac63602c0f03a02dd69e4b138174", size = 528807 }, + { url = "https://files.pythonhosted.org/packages/e3/62/7ebe6ec0d3dd6130921f8cffb7e34afb7f71b3819aa0446a24c5e81245ec/rpds_py-0.20.0-cp312-none-win32.whl", hash = "sha256:8bc7690f7caee50b04a79bf017a8d020c1f48c2a1077ffe172abec59870f1139", size = 200993 }, + { url = "https://files.pythonhosted.org/packages/ec/2f/b938864d66b86a6e4acadefdc56de75ef56f7cafdfd568a6464605457bd5/rpds_py-0.20.0-cp312-none-win_amd64.whl", hash = "sha256:0e13e6952ef264c40587d510ad676a988df19adea20444c2b295e536457bc585", size = 214458 }, + { url = "https://files.pythonhosted.org/packages/99/32/43b919a0a423c270a838ac2726b1c7168b946f2563fd99a51aaa9692d00f/rpds_py-0.20.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:aa9a0521aeca7d4941499a73ad7d4f8ffa3d1affc50b9ea11d992cd7eff18a29", size = 321465 }, + { url = "https://files.pythonhosted.org/packages/58/a9/c4d899cb28e9e47b0ff12462e8f827381f243176036f17bef9c1604667f2/rpds_py-0.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1f1d51eccb7e6c32ae89243cb352389228ea62f89cd80823ea7dd1b98e0b91", size = 312900 }, + { url = "https://files.pythonhosted.org/packages/8f/90/9e51670575b5dfaa8c823369ef7d943087bfb73d4f124a99ad6ef19a2b26/rpds_py-0.20.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a86a9b96070674fc88b6f9f71a97d2c1d3e5165574615d1f9168ecba4cecb24", size = 370973 }, + { url = "https://files.pythonhosted.org/packages/fc/c1/523f2a03f853fc0d4c1acbef161747e9ab7df0a8abf6236106e333540921/rpds_py-0.20.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c8ef2ebf76df43f5750b46851ed1cdf8f109d7787ca40035fe19fbdc1acc5a7", size = 370890 }, + { url = "https://files.pythonhosted.org/packages/51/ca/2458a771f16b0931de4d384decbe43016710bc948036c8f4562d6e063437/rpds_py-0.20.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b74b25f024b421d5859d156750ea9a65651793d51b76a2e9238c05c9d5f203a9", size = 397174 }, + { url = "https://files.pythonhosted.org/packages/00/7d/6e06807f6305ea2408b364efb0eef83a6e21b5e7b5267ad6b473b9a7e416/rpds_py-0.20.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57eb94a8c16ab08fef6404301c38318e2c5a32216bf5de453e2714c964c125c8", size = 426449 }, + { url = "https://files.pythonhosted.org/packages/8c/d1/6c9e65260a819a1714510a7d69ac1d68aa23ee9ce8a2d9da12187263c8fc/rpds_py-0.20.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1940dae14e715e2e02dfd5b0f64a52e8374a517a1e531ad9412319dc3ac7879", size = 357698 }, + { url = "https://files.pythonhosted.org/packages/5d/fb/ecea8b5286d2f03eec922be7173a03ed17278944f7c124348f535116db15/rpds_py-0.20.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d20277fd62e1b992a50c43f13fbe13277a31f8c9f70d59759c88f644d66c619f", size = 378530 }, + { url = "https://files.pythonhosted.org/packages/e3/e3/ac72f858957f52a109c588589b73bd2fad4a0fc82387fb55fb34aeb0f9cd/rpds_py-0.20.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:06db23d43f26478303e954c34c75182356ca9aa7797d22c5345b16871ab9c45c", size = 545753 }, + { url = "https://files.pythonhosted.org/packages/b2/a4/a27683b519d5fc98e4390a3b130117d80fd475c67aeda8aac83c0e8e326a/rpds_py-0.20.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b2a5db5397d82fa847e4c624b0c98fe59d2d9b7cf0ce6de09e4d2e80f8f5b3f2", size = 552443 }, + { url = "https://files.pythonhosted.org/packages/a1/ed/c074d248409b4432b1ccb2056974175fa0af2d1bc1f9c21121f80a358fa3/rpds_py-0.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a35df9f5548fd79cb2f52d27182108c3e6641a4feb0f39067911bf2adaa3e57", size = 528380 }, + { url = "https://files.pythonhosted.org/packages/d5/bd/04caf938895d2d78201e89c0c8a94dfd9990c34a19ff52fb01d0912343e3/rpds_py-0.20.0-cp313-none-win32.whl", hash = "sha256:fd2d84f40633bc475ef2d5490b9c19543fbf18596dcb1b291e3a12ea5d722f7a", size = 200540 }, + { url = "https://files.pythonhosted.org/packages/95/cc/109eb8b9863680411ae703664abacaa035820c7755acc9686d5dd02cdd2e/rpds_py-0.20.0-cp313-none-win_amd64.whl", hash = "sha256:9bc2d153989e3216b0559251b0c260cfd168ec78b1fac33dd485750a228db5a2", size = 214111 }, + { url = "https://files.pythonhosted.org/packages/a1/55/228f6d9a8c6940c8d5e49db5e0434ffcbad669c33509ac39cb0af061b0fa/rpds_py-0.20.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:3fde368e9140312b6e8b6c09fb9f8c8c2f00999d1823403ae90cc00480221b22", size = 319496 }, + { url = "https://files.pythonhosted.org/packages/68/61/074236253586feb550954f8b4359d38eefb45bafcbbb7d2e74062a82f386/rpds_py-0.20.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9824fb430c9cf9af743cf7aaf6707bf14323fb51ee74425c380f4c846ea70789", size = 311837 }, + { url = "https://files.pythonhosted.org/packages/03/67/ed6c2fe076bf78296934d4356145fedf3c7c2f8d490e099bcf6f31794dc0/rpds_py-0.20.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11ef6ce74616342888b69878d45e9f779b95d4bd48b382a229fe624a409b72c5", size = 367819 }, + { url = "https://files.pythonhosted.org/packages/30/25/4a9e7b89b6760ac032f375cb236e4f8e518ad1fad685c40b6a9752056d6f/rpds_py-0.20.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c52d3f2f82b763a24ef52f5d24358553e8403ce05f893b5347098014f2d9eff2", size = 368322 }, + { url = "https://files.pythonhosted.org/packages/67/17/0255bb0e564517b53343ea672ebec9fb7ad40e9083ca09a4080fbc986bb9/rpds_py-0.20.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d35cef91e59ebbeaa45214861874bc6f19eb35de96db73e467a8358d701a96c", size = 395552 }, + { url = "https://files.pythonhosted.org/packages/af/6e/77c65ccb0d7cdc39ec2be19b918a4d4fe9e2d2a1c5cab36745b36f2c1e59/rpds_py-0.20.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d72278a30111e5b5525c1dd96120d9e958464316f55adb030433ea905866f4de", size = 433735 }, + { url = "https://files.pythonhosted.org/packages/04/d8/e73d56b1908a6c0e3e5982365eb293170cd458cc25a19363f69c76e00fd2/rpds_py-0.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4c29cbbba378759ac5786730d1c3cb4ec6f8ababf5c42a9ce303dc4b3d08cda", size = 355542 }, + { url = "https://files.pythonhosted.org/packages/47/df/e72c79053b0c882b818bfd8f0ed1f1ace550bc9cdba27165cb73dddb9394/rpds_py-0.20.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6632f2d04f15d1bd6fe0eedd3b86d9061b836ddca4c03d5cf5c7e9e6b7c14580", size = 373644 }, + { url = "https://files.pythonhosted.org/packages/7f/00/3e16cb08c0cc6a233f0f61e4d009e3098cbe280ec975d14f28935bd15316/rpds_py-0.20.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d0b67d87bb45ed1cd020e8fbf2307d449b68abc45402fe1a4ac9e46c3c8b192b", size = 543139 }, + { url = "https://files.pythonhosted.org/packages/41/71/799c6b6f6031ed535f22fcf6802601cc7f981842bd28007bb7bb4bd10b2f/rpds_py-0.20.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ec31a99ca63bf3cd7f1a5ac9fe95c5e2d060d3c768a09bc1d16e235840861420", size = 548007 }, + { url = "https://files.pythonhosted.org/packages/53/58/ad03eb6718e814fa045198c72d45d2ae60180eb48338f22c9fa34bd89964/rpds_py-0.20.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:22e6c9976e38f4d8c4a63bd8a8edac5307dffd3ee7e6026d97f3cc3a2dc02a0b", size = 526102 }, + { url = "https://files.pythonhosted.org/packages/78/99/a52e5b460f2311fc8ee75ff769e8d67e76208947180eacb4f153af2d9967/rpds_py-0.20.0-cp39-none-win32.whl", hash = "sha256:569b3ea770c2717b730b61998b6c54996adee3cef69fc28d444f3e7920313cf7", size = 199391 }, + { url = "https://files.pythonhosted.org/packages/0c/7d/fd42a27fe392a69faf4a5e635870fc425edcb998485ee73afbc734ecef16/rpds_py-0.20.0-cp39-none-win_amd64.whl", hash = "sha256:e6900ecdd50ce0facf703f7a00df12374b74bbc8ad9fe0f6559947fb20f82364", size = 213205 }, + { url = "https://files.pythonhosted.org/packages/06/39/bf1f664c347c946ef56cecaa896e3693d91acc741afa78ebb3fdb7aba08b/rpds_py-0.20.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:617c7357272c67696fd052811e352ac54ed1d9b49ab370261a80d3b6ce385045", size = 319444 }, + { url = "https://files.pythonhosted.org/packages/c1/71/876135d3cb90d62468540b84e8e83ff4dc92052ab309bfdea7ea0b9221ad/rpds_py-0.20.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9426133526f69fcaba6e42146b4e12d6bc6c839b8b555097020e2b78ce908dcc", size = 311699 }, + { url = "https://files.pythonhosted.org/packages/f7/da/8ccaeba6a3dda7467aebaf893de9eafd56275e2c90773c83bf15fb0b8374/rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deb62214c42a261cb3eb04d474f7155279c1a8a8c30ac89b7dcb1721d92c3c02", size = 367825 }, + { url = "https://files.pythonhosted.org/packages/04/b6/02a54c47c178d180395b3c9a8bfb3b93906e08f9acf7b4a1067d27c3fae0/rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcaeb7b57f1a1e071ebd748984359fef83ecb026325b9d4ca847c95bc7311c92", size = 369046 }, + { url = "https://files.pythonhosted.org/packages/a7/64/df4966743aa4def8727dc13d06527c8b13eb7412c1429def2d4701bee520/rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d454b8749b4bd70dd0a79f428731ee263fa6995f83ccb8bada706e8d1d3ff89d", size = 395896 }, + { url = "https://files.pythonhosted.org/packages/6f/d9/7ff03ff3642c600f27ff94512bb158a8d815fea5ed4162c75a7e850d6003/rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d807dc2051abe041b6649681dce568f8e10668e3c1c6543ebae58f2d7e617855", size = 432427 }, + { url = "https://files.pythonhosted.org/packages/b8/c6/e1b886f7277b3454e55e85332e165091c19114eecb5377b88d892fd36ccf/rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3c20f0ddeb6e29126d45f89206b8291352b8c5b44384e78a6499d68b52ae511", size = 355403 }, + { url = "https://files.pythonhosted.org/packages/e2/62/e26bd5b944e547c7bfd0b6ca7e306bfa430f8bd298ab72a1217976a7ca8d/rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b7f19250ceef892adf27f0399b9e5afad019288e9be756d6919cb58892129f51", size = 374491 }, + { url = "https://files.pythonhosted.org/packages/c3/92/93c5a530898d3a5d1ce087455071ba714b77806ed9ffee4070d0c7a53b7e/rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:4f1ed4749a08379555cebf4650453f14452eaa9c43d0a95c49db50c18b7da075", size = 543622 }, + { url = "https://files.pythonhosted.org/packages/01/9e/d68fba289625b5d3c9d1925825d7da716fbf812bda2133ac409021d5db13/rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:dcedf0b42bcb4cfff4101d7771a10532415a6106062f005ab97d1d0ab5681c60", size = 548558 }, + { url = "https://files.pythonhosted.org/packages/bf/d6/4b2fad4898154365f0f2bd72ffd190349274a4c1d6a6f94f02a83bb2b8f1/rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:39ed0d010457a78f54090fafb5d108501b5aa5604cc22408fc1c0c77eac14344", size = 525753 }, + { url = "https://files.pythonhosted.org/packages/d2/ea/6f121d1802f3adae1981aea4209ea66f9d3c7f2f6d6b85ef4f13a61d17ef/rpds_py-0.20.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:bb273176be34a746bdac0b0d7e4e2c467323d13640b736c4c477881a3220a989", size = 213529 }, + { url = "https://files.pythonhosted.org/packages/0a/6f/7ab47005469f0d73dad89d29b733e3555d454a45146c30f5628242e56d33/rpds_py-0.20.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f918a1a130a6dfe1d7fe0f105064141342e7dd1611f2e6a21cd2f5c8cb1cfb3e", size = 320800 }, + { url = "https://files.pythonhosted.org/packages/cc/a1/bef9e0ef30f89c7516559ca7acc40e8ae70397535a0b1a4535a4a01d9ed0/rpds_py-0.20.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:f60012a73aa396be721558caa3a6fd49b3dd0033d1675c6d59c4502e870fcf0c", size = 312001 }, + { url = "https://files.pythonhosted.org/packages/31/44/9093c5dca95ee463c3669651e710af182eb6f9cd83626b15a2ebde2247b1/rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d2b1ad682a3dfda2a4e8ad8572f3100f95fad98cb99faf37ff0ddfe9cbf9d03", size = 369279 }, + { url = "https://files.pythonhosted.org/packages/6f/ac/0c36e067681fa3fe4c60a9422b011ec0ccc80c1e124f5210951f7982e887/rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:614fdafe9f5f19c63ea02817fa4861c606a59a604a77c8cdef5aa01d28b97921", size = 369716 }, + { url = "https://files.pythonhosted.org/packages/6b/78/8896e08625d46ea5bfdd526ee688b91eeafecbc3cf7223612c82ed77905b/rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa518bcd7600c584bf42e6617ee8132869e877db2f76bcdc281ec6a4113a53ab", size = 396708 }, + { url = "https://files.pythonhosted.org/packages/24/5f/d865ae460e47e46fd2b489f2aceed34439bd8f18a1ff414c299142e0e22a/rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0475242f447cc6cb8a9dd486d68b2ef7fbee84427124c232bff5f63b1fe11e5", size = 433356 }, + { url = "https://files.pythonhosted.org/packages/bd/8b/04031937ffa565021f934a9acf44bb6b1b60ea19fa9e58950b32357e85a1/rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f90a4cd061914a60bd51c68bcb4357086991bd0bb93d8aa66a6da7701370708f", size = 356157 }, + { url = "https://files.pythonhosted.org/packages/3a/64/1f0471b1e688704a716e07340b85f4145109359951feb08676a1f3b8cec4/rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:def7400461c3a3f26e49078302e1c1b38f6752342c77e3cf72ce91ca69fb1bc1", size = 374826 }, + { url = "https://files.pythonhosted.org/packages/73/4e/082c0c5eba463e29dff1c6b49557f6ad0d6faae4b46832fa9c1e5b69b7ba/rpds_py-0.20.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:65794e4048ee837494aea3c21a28ad5fc080994dfba5b036cf84de37f7ad5074", size = 544549 }, + { url = "https://files.pythonhosted.org/packages/cd/ee/f4af0a62d1ba912c4a3a7f5ec04350946ddd59017f3f3d1c227b20ddf558/rpds_py-0.20.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:faefcc78f53a88f3076b7f8be0a8f8d35133a3ecf7f3770895c25f8813460f08", size = 549245 }, + { url = "https://files.pythonhosted.org/packages/59/42/34601dc773be86a85a9ca47f68301a69fdb019aaae0c1426813f265f5ac0/rpds_py-0.20.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:5b4f105deeffa28bbcdff6c49b34e74903139afa690e35d2d9e3c2c2fba18cec", size = 526722 }, + { url = "https://files.pythonhosted.org/packages/ff/4f/280745d5180c9d78df6b53b6e8b65336f8b6adeb958a8fd19c749fded637/rpds_py-0.20.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fdfc3a892927458d98f3d55428ae46b921d1f7543b89382fdb483f5640daaec8", size = 214379 }, +] + +[[package]] +name = "ruff" +version = "0.6.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/0d/6148a48dab5662ca1d5a93b7c0d13c03abd3cc7e2f35db08410e47cef15d/ruff-0.6.9.tar.gz", hash = "sha256:b076ef717a8e5bc819514ee1d602bbdca5b4420ae13a9cf61a0c0a4f53a2baa2", size = 3095355 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/8f/f7a0a0ef1818662efb32ed6df16078c95da7a0a3248d64c2410c1e27799f/ruff-0.6.9-py3-none-linux_armv6l.whl", hash = "sha256:064df58d84ccc0ac0fcd63bc3090b251d90e2a372558c0f057c3f75ed73e1ccd", size = 10440526 }, + { url = "https://files.pythonhosted.org/packages/8b/69/b179a5faf936a9e2ab45bb412a668e4661eded964ccfa19d533f29463ef6/ruff-0.6.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:140d4b5c9f5fc7a7b074908a78ab8d384dd7f6510402267bc76c37195c02a7ec", size = 10034612 }, + { url = "https://files.pythonhosted.org/packages/c7/ef/fd1b4be979c579d191eeac37b5cfc0ec906de72c8bcd8595e2c81bb700c1/ruff-0.6.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53fd8ca5e82bdee8da7f506d7b03a261f24cd43d090ea9db9a1dc59d9313914c", size = 9706197 }, + { url = "https://files.pythonhosted.org/packages/29/61/b376d775deb5851cb48d893c568b511a6d3625ef2c129ad5698b64fb523c/ruff-0.6.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645d7d8761f915e48a00d4ecc3686969761df69fb561dd914a773c1a8266e14e", size = 10751855 }, + { url = "https://files.pythonhosted.org/packages/13/d7/def9e5f446d75b9a9c19b24231a3a658c075d79163b08582e56fa5dcfa38/ruff-0.6.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eae02b700763e3847595b9d2891488989cac00214da7f845f4bcf2989007d577", size = 10200889 }, + { url = "https://files.pythonhosted.org/packages/6c/d6/7f34160818bcb6e84ce293a5966cba368d9112ff0289b273fbb689046047/ruff-0.6.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d5ccc9e58112441de8ad4b29dcb7a86dc25c5f770e3c06a9d57e0e5eba48829", size = 11038678 }, + { url = "https://files.pythonhosted.org/packages/13/34/a40ff8ae62fb1b26fb8e6fa7e64bc0e0a834b47317880de22edd6bfb54fb/ruff-0.6.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:417b81aa1c9b60b2f8edc463c58363075412866ae4e2b9ab0f690dc1e87ac1b5", size = 11808682 }, + { url = "https://files.pythonhosted.org/packages/2e/6d/25a4386ae4009fc798bd10ba48c942d1b0b3e459b5403028f1214b6dd161/ruff-0.6.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c866b631f5fbce896a74a6e4383407ba7507b815ccc52bcedabb6810fdb3ef7", size = 11330446 }, + { url = "https://files.pythonhosted.org/packages/f7/f6/bdf891a9200d692c94ebcd06ae5a2fa5894e522f2c66c2a12dd5d8cb2654/ruff-0.6.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b118afbb3202f5911486ad52da86d1d52305b59e7ef2031cea3425142b97d6f", size = 12483048 }, + { url = "https://files.pythonhosted.org/packages/a7/86/96f4252f41840e325b3fa6c48297e661abb9f564bd7dcc0572398c8daa42/ruff-0.6.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a67267654edc23c97335586774790cde402fb6bbdb3c2314f1fc087dee320bfa", size = 10936855 }, + { url = "https://files.pythonhosted.org/packages/45/87/801a52d26c8dbf73424238e9908b9ceac430d903c8ef35eab1b44fcfa2bd/ruff-0.6.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3ef0cc774b00fec123f635ce5c547dac263f6ee9fb9cc83437c5904183b55ceb", size = 10713007 }, + { url = "https://files.pythonhosted.org/packages/be/27/6f7161d90320a389695e32b6ebdbfbedde28ccbf52451e4b723d7ce744ad/ruff-0.6.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:12edd2af0c60fa61ff31cefb90aef4288ac4d372b4962c2864aeea3a1a2460c0", size = 10274594 }, + { url = "https://files.pythonhosted.org/packages/00/52/dc311775e7b5f5b19831563cb1572ecce63e62681bccc609867711fae317/ruff-0.6.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:55bb01caeaf3a60b2b2bba07308a02fca6ab56233302406ed5245180a05c5625", size = 10608024 }, + { url = "https://files.pythonhosted.org/packages/98/b6/be0a1ddcbac65a30c985cf7224c4fce786ba2c51e7efeb5178fe410ed3cf/ruff-0.6.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:925d26471fa24b0ce5a6cdfab1bb526fb4159952385f386bdcc643813d472039", size = 10982085 }, + { url = "https://files.pythonhosted.org/packages/bb/a4/c84bc13d0b573cf7bb7d17b16d6d29f84267c92d79b2f478d4ce322e8e72/ruff-0.6.9-py3-none-win32.whl", hash = "sha256:eb61ec9bdb2506cffd492e05ac40e5bc6284873aceb605503d8494180d6fc84d", size = 8522088 }, + { url = "https://files.pythonhosted.org/packages/74/be/fc352bd8ca40daae8740b54c1c3e905a7efe470d420a268cd62150248c91/ruff-0.6.9-py3-none-win_amd64.whl", hash = "sha256:785d31851c1ae91f45b3d8fe23b8ae4b5170089021fbb42402d811135f0b7117", size = 9359275 }, + { url = "https://files.pythonhosted.org/packages/3e/14/fd026bc74ded05e2351681545a5f626e78ef831f8edce064d61acd2e6ec7/ruff-0.6.9-py3-none-win_arm64.whl", hash = "sha256:a9641e31476d601f83cd602608739a0840e348bda93fec9f1ee816f8b6798b93", size = 8679879 }, +] + +[[package]] +name = "scikit-learn" +version = "1.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/37/59/44985a2bdc95c74e34fef3d10cb5d93ce13b0e2a7baefffe1b53853b502d/scikit_learn-1.5.2.tar.gz", hash = "sha256:b4237ed7b3fdd0a4882792e68ef2545d5baa50aca3bb45aa7df468138ad8f94d", size = 7001680 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/89/be41419b4bec629a4691183a5eb1796f91252a13a5ffa243fd958cad7e91/scikit_learn-1.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:299406827fb9a4f862626d0fe6c122f5f87f8910b86fe5daa4c32dcd742139b6", size = 12106070 }, + { url = "https://files.pythonhosted.org/packages/bf/e0/3b6d777d375f3b685f433c93384cdb724fb078e1dc8f8ff0950467e56c30/scikit_learn-1.5.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:2d4cad1119c77930b235579ad0dc25e65c917e756fe80cab96aa3b9428bd3fb0", size = 10971758 }, + { url = "https://files.pythonhosted.org/packages/7b/31/eb7dd56c371640753953277de11356c46a3149bfeebb3d7dcd90b993715a/scikit_learn-1.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c412ccc2ad9bf3755915e3908e677b367ebc8d010acbb3f182814524f2e5540", size = 12500080 }, + { url = "https://files.pythonhosted.org/packages/4c/1e/a7c7357e704459c7d56a18df4a0bf08669442d1f8878cc0864beccd6306a/scikit_learn-1.5.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a686885a4b3818d9e62904d91b57fa757fc2bed3e465c8b177be652f4dd37c8", size = 13347241 }, + { url = "https://files.pythonhosted.org/packages/48/76/154ebda6794faf0b0f3ccb1b5cd9a19f0a63cb9e1f3d2c61b6114002677b/scikit_learn-1.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:c15b1ca23d7c5f33cc2cb0a0d6aaacf893792271cddff0edbd6a40e8319bc113", size = 11000477 }, + { url = "https://files.pythonhosted.org/packages/ff/91/609961972f694cb9520c4c3d201e377a26583e1eb83bc5a334c893729214/scikit_learn-1.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:03b6158efa3faaf1feea3faa884c840ebd61b6484167c711548fce208ea09445", size = 12088580 }, + { url = "https://files.pythonhosted.org/packages/cd/7a/19fe32c810c5ceddafcfda16276d98df299c8649e24e84d4f00df4a91e01/scikit_learn-1.5.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:1ff45e26928d3b4eb767a8f14a9a6efbf1cbff7c05d1fb0f95f211a89fd4f5de", size = 10975994 }, + { url = "https://files.pythonhosted.org/packages/4c/75/62e49f8a62bf3c60b0e64d0fce540578ee4f0e752765beb2e1dc7c6d6098/scikit_learn-1.5.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f763897fe92d0e903aa4847b0aec0e68cadfff77e8a0687cabd946c89d17e675", size = 12465782 }, + { url = "https://files.pythonhosted.org/packages/49/21/3723de321531c9745e40f1badafd821e029d346155b6c79704e0b7197552/scikit_learn-1.5.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8b0ccd4a902836493e026c03256e8b206656f91fbcc4fde28c57a5b752561f1", size = 13322034 }, + { url = "https://files.pythonhosted.org/packages/17/1c/ccdd103cfcc9435a18819856fbbe0c20b8fa60bfc3343580de4be13f0668/scikit_learn-1.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:6c16d84a0d45e4894832b3c4d0bf73050939e21b99b01b6fd59cbb0cf39163b6", size = 11015224 }, + { url = "https://files.pythonhosted.org/packages/a4/db/b485c1ac54ff3bd9e7e6b39d3cc6609c4c76a65f52ab0a7b22b6c3ab0e9d/scikit_learn-1.5.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f932a02c3f4956dfb981391ab24bda1dbd90fe3d628e4b42caef3e041c67707a", size = 12110344 }, + { url = "https://files.pythonhosted.org/packages/54/1a/7deb52fa23aebb855431ad659b3c6a2e1709ece582cb3a63d66905e735fe/scikit_learn-1.5.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:3b923d119d65b7bd555c73be5423bf06c0105678ce7e1f558cb4b40b0a5502b1", size = 11033502 }, + { url = "https://files.pythonhosted.org/packages/a1/32/4a7a205b14c11225609b75b28402c196e4396ac754dab6a81971b811781c/scikit_learn-1.5.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f60021ec1574e56632be2a36b946f8143bf4e5e6af4a06d85281adc22938e0dd", size = 12085794 }, + { url = "https://files.pythonhosted.org/packages/c6/29/044048c5e911373827c0e1d3051321b9183b2a4f8d4e2f11c08fcff83f13/scikit_learn-1.5.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:394397841449853c2290a32050382edaec3da89e35b3e03d6cc966aebc6a8ae6", size = 12945797 }, + { url = "https://files.pythonhosted.org/packages/aa/ce/c0b912f2f31aeb1b756a6ba56bcd84dd1f8a148470526a48515a3f4d48cd/scikit_learn-1.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:57cc1786cfd6bd118220a92ede80270132aa353647684efa385a74244a41e3b1", size = 10985467 }, + { url = "https://files.pythonhosted.org/packages/a4/50/8891028437858cc510e13578fe7046574a60c2aaaa92b02d64aac5b1b412/scikit_learn-1.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9a702e2de732bbb20d3bad29ebd77fc05a6b427dc49964300340e4c9328b3f5", size = 12025584 }, + { url = "https://files.pythonhosted.org/packages/d2/79/17feef8a1c14149436083bec0e61d7befb4812e272d5b20f9d79ea3e9ab1/scikit_learn-1.5.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:b0768ad641981f5d3a198430a1d31c3e044ed2e8a6f22166b4d546a5116d7908", size = 10959795 }, + { url = "https://files.pythonhosted.org/packages/b1/c8/f08313f9e2e656bd0905930ae8bf99a573ea21c34666a813b749c338202f/scikit_learn-1.5.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:178ddd0a5cb0044464fc1bfc4cca5b1833bfc7bb022d70b05db8530da4bb3dd3", size = 12077302 }, + { url = "https://files.pythonhosted.org/packages/a7/48/fbfb4dc72bed0fe31fe045fb30e924909ad03f717c36694351612973b1a9/scikit_learn-1.5.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7284ade780084d94505632241bf78c44ab3b6f1e8ccab3d2af58e0e950f9c12", size = 13002811 }, + { url = "https://files.pythonhosted.org/packages/a5/e7/0c869f9e60d225a77af90d2aefa7a4a4c0e745b149325d1450f0f0ce5399/scikit_learn-1.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:b7b0f9a0b1040830d38c39b91b3a44e1b643f4b36e36567b80b7c6bd2202a27f", size = 10951354 }, + { url = "https://files.pythonhosted.org/packages/db/a0/e92af06a9fddd1fafbbf39cd32cbed5929b63cf99e03a438f838987e265d/scikit_learn-1.5.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:757c7d514ddb00ae249832fe87100d9c73c6ea91423802872d9e74970a0e40b9", size = 12142022 }, + { url = "https://files.pythonhosted.org/packages/1b/be/386ef63d9d5e2ddf8308f6a164e4b388d5c5aecc0504d25acc6b33d8b09e/scikit_learn-1.5.2-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:52788f48b5d8bca5c0736c175fa6bdaab2ef00a8f536cda698db61bd89c551c1", size = 11002839 }, + { url = "https://files.pythonhosted.org/packages/12/0d/94a03c006b01c1de27518d393f52ad3639705cd70184e106d24ffb3f28f6/scikit_learn-1.5.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:643964678f4b5fbdc95cbf8aec638acc7aa70f5f79ee2cdad1eec3df4ba6ead8", size = 12546017 }, + { url = "https://files.pythonhosted.org/packages/2a/9d/d332ec76e2cc442fce98bc43a44e69d3c281e6b4ede6b6db2616dc6fbec6/scikit_learn-1.5.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca64b3089a6d9b9363cd3546f8978229dcbb737aceb2c12144ee3f70f95684b7", size = 13369870 }, + { url = "https://files.pythonhosted.org/packages/45/05/74e453853c0b1b0773f46027848a17467f5dc9c5f15d096d911163d27550/scikit_learn-1.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:3bed4909ba187aca80580fe2ef370d9180dcf18e621a27c4cf2ef10d279a7efe", size = 11031380 }, +] + +[[package]] +name = "scipy" +version = "1.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/00/48c2f661e2816ccf2ecd77982f6605b2950afe60f60a52b4cbbc2504aa8f/scipy-1.13.1.tar.gz", hash = "sha256:095a87a0312b08dfd6a6155cbbd310a8c51800fc931b8c0b84003014b874ed3c", size = 57210720 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/59/41b2529908c002ade869623b87eecff3e11e3ce62e996d0bdcb536984187/scipy-1.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:20335853b85e9a49ff7572ab453794298bcf0354d8068c5f6775a0eabf350aca", size = 39328076 }, + { url = "https://files.pythonhosted.org/packages/d5/33/f1307601f492f764062ce7dd471a14750f3360e33cd0f8c614dae208492c/scipy-1.13.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d605e9c23906d1994f55ace80e0125c587f96c020037ea6aa98d01b4bd2e222f", size = 30306232 }, + { url = "https://files.pythonhosted.org/packages/c0/66/9cd4f501dd5ea03e4a4572ecd874936d0da296bd04d1c45ae1a4a75d9c3a/scipy-1.13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfa31f1def5c819b19ecc3a8b52d28ffdcc7ed52bb20c9a7589669dd3c250989", size = 33743202 }, + { url = "https://files.pythonhosted.org/packages/a3/ba/7255e5dc82a65adbe83771c72f384d99c43063648456796436c9a5585ec3/scipy-1.13.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26264b282b9da0952a024ae34710c2aff7d27480ee91a2e82b7b7073c24722f", size = 38577335 }, + { url = "https://files.pythonhosted.org/packages/49/a5/bb9ded8326e9f0cdfdc412eeda1054b914dfea952bda2097d174f8832cc0/scipy-1.13.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eccfa1906eacc02de42d70ef4aecea45415f5be17e72b61bafcfd329bdc52e94", size = 38820728 }, + { url = "https://files.pythonhosted.org/packages/12/30/df7a8fcc08f9b4a83f5f27cfaaa7d43f9a2d2ad0b6562cced433e5b04e31/scipy-1.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:2831f0dc9c5ea9edd6e51e6e769b655f08ec6db6e2e10f86ef39bd32eb11da54", size = 46210588 }, + { url = "https://files.pythonhosted.org/packages/b4/15/4a4bb1b15bbd2cd2786c4f46e76b871b28799b67891f23f455323a0cdcfb/scipy-1.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:27e52b09c0d3a1d5b63e1105f24177e544a222b43611aaf5bc44d4a0979e32f9", size = 39333805 }, + { url = "https://files.pythonhosted.org/packages/ba/92/42476de1af309c27710004f5cdebc27bec62c204db42e05b23a302cb0c9a/scipy-1.13.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:54f430b00f0133e2224c3ba42b805bfd0086fe488835effa33fa291561932326", size = 30317687 }, + { url = "https://files.pythonhosted.org/packages/80/ba/8be64fe225360a4beb6840f3cbee494c107c0887f33350d0a47d55400b01/scipy-1.13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e89369d27f9e7b0884ae559a3a956e77c02114cc60a6058b4e5011572eea9299", size = 33694638 }, + { url = "https://files.pythonhosted.org/packages/36/07/035d22ff9795129c5a847c64cb43c1fa9188826b59344fee28a3ab02e283/scipy-1.13.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a78b4b3345f1b6f68a763c6e25c0c9a23a9fd0f39f5f3d200efe8feda560a5fa", size = 38569931 }, + { url = "https://files.pythonhosted.org/packages/d9/10/f9b43de37e5ed91facc0cfff31d45ed0104f359e4f9a68416cbf4e790241/scipy-1.13.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45484bee6d65633752c490404513b9ef02475b4284c4cfab0ef946def50b3f59", size = 38838145 }, + { url = "https://files.pythonhosted.org/packages/4a/48/4513a1a5623a23e95f94abd675ed91cfb19989c58e9f6f7d03990f6caf3d/scipy-1.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:5713f62f781eebd8d597eb3f88b8bf9274e79eeabf63afb4a737abc6c84ad37b", size = 46196227 }, + { url = "https://files.pythonhosted.org/packages/f2/7b/fb6b46fbee30fc7051913068758414f2721003a89dd9a707ad49174e3843/scipy-1.13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5d72782f39716b2b3509cd7c33cdc08c96f2f4d2b06d51e52fb45a19ca0c86a1", size = 39357301 }, + { url = "https://files.pythonhosted.org/packages/dc/5a/2043a3bde1443d94014aaa41e0b50c39d046dda8360abd3b2a1d3f79907d/scipy-1.13.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:017367484ce5498445aade74b1d5ab377acdc65e27095155e448c88497755a5d", size = 30363348 }, + { url = "https://files.pythonhosted.org/packages/e7/cb/26e4a47364bbfdb3b7fb3363be6d8a1c543bcd70a7753ab397350f5f189a/scipy-1.13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:949ae67db5fa78a86e8fa644b9a6b07252f449dcf74247108c50e1d20d2b4627", size = 33406062 }, + { url = "https://files.pythonhosted.org/packages/88/ab/6ecdc526d509d33814835447bbbeedbebdec7cca46ef495a61b00a35b4bf/scipy-1.13.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de3ade0e53bc1f21358aa74ff4830235d716211d7d077e340c7349bc3542e884", size = 38218311 }, + { url = "https://files.pythonhosted.org/packages/0b/00/9f54554f0f8318100a71515122d8f4f503b1a2c4b4cfab3b4b68c0eb08fa/scipy-1.13.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2ac65fb503dad64218c228e2dc2d0a0193f7904747db43014645ae139c8fad16", size = 38442493 }, + { url = "https://files.pythonhosted.org/packages/3e/df/963384e90733e08eac978cd103c34df181d1fec424de383cdc443f418dd4/scipy-1.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:cdd7dacfb95fea358916410ec61bbc20440f7860333aee6d882bb8046264e949", size = 45910955 }, + { url = "https://files.pythonhosted.org/packages/7f/29/c2ea58c9731b9ecb30b6738113a95d147e83922986b34c685b8f6eefde21/scipy-1.13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:436bbb42a94a8aeef855d755ce5a465479c721e9d684de76bf61a62e7c2b81d5", size = 39352927 }, + { url = "https://files.pythonhosted.org/packages/5c/c0/e71b94b20ccf9effb38d7147c0064c08c622309fd487b1b677771a97d18c/scipy-1.13.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:8335549ebbca860c52bf3d02f80784e91a004b71b059e3eea9678ba994796a24", size = 30324538 }, + { url = "https://files.pythonhosted.org/packages/6d/0f/aaa55b06d474817cea311e7b10aab2ea1fd5d43bc6a2861ccc9caec9f418/scipy-1.13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d533654b7d221a6a97304ab63c41c96473ff04459e404b83275b60aa8f4b7004", size = 33732190 }, + { url = "https://files.pythonhosted.org/packages/35/f5/d0ad1a96f80962ba65e2ce1de6a1e59edecd1f0a7b55990ed208848012e0/scipy-1.13.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637e98dcf185ba7f8e663e122ebf908c4702420477ae52a04f9908707456ba4d", size = 38612244 }, + { url = "https://files.pythonhosted.org/packages/8d/02/1165905f14962174e6569076bcc3315809ae1291ed14de6448cc151eedfd/scipy-1.13.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a014c2b3697bde71724244f63de2476925596c24285c7a637364761f8710891c", size = 38845637 }, + { url = "https://files.pythonhosted.org/packages/3e/77/dab54fe647a08ee4253963bcd8f9cf17509c8ca64d6335141422fe2e2114/scipy-1.13.1-cp39-cp39-win_amd64.whl", hash = "sha256:392e4ec766654852c25ebad4f64e4e584cf19820b980bc04960bca0b0cd6eaa2", size = 46227440 }, +] + +[[package]] +name = "six" +version = "1.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053 }, +] + +[[package]] +name = "snowballstemmer" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/7b/af302bebf22c749c56c9c3e8ae13190b5b5db37a33d9068652e8f73b7089/snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1", size = 86699 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/dc/c02e01294f7265e63a7315fe086dd1df7dacb9f840a804da846b96d01b96/snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a", size = 93002 }, +] + +[[package]] +name = "soupsieve" +version = "2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/ce/fbaeed4f9fb8b2daa961f90591662df6a86c1abf25c548329a86920aedfb/soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb", size = 101569 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/c2/fe97d779f3ef3b15f05c94a2f1e3d21732574ed441687474db9d342a7315/soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9", size = 36186 }, +] + +[[package]] +name = "sphinx" +version = "7.4.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils" }, + { name = "imagesize" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/be/50e50cb4f2eff47df05673d361095cafd95521d2a22521b920c67a372dcb/sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe", size = 8067911 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/ef/153f6803c5d5f8917dbb7f7fcf6d34a871ede3296fa89c2c703f5f8a6c8e/sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239", size = 3401624 }, +] + +[[package]] +name = "sphinx-argparse" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/0b/d98f799d4283e8b6c403fd8102acf2b552ad78e947b6899a1344521e9d86/sphinx_argparse-0.4.0.tar.gz", hash = "sha256:e0f34184eb56f12face774fbc87b880abdb9017a0998d1ec559b267e9697e449", size = 15020 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/98/d32f45b19b60e52b4ddc714dee139a92c6ea8fa9115f994884d321c3454d/sphinx_argparse-0.4.0-py3-none-any.whl", hash = "sha256:73bee01f7276fae2bf621ccfe4d167af7306e7288e3482005405d9f826f9b037", size = 12625 }, +] + +[[package]] +name = "sphinx-rtd-theme" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "sphinx" }, + { name = "sphinxcontrib-jquery" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/f8/2667f9cab89827528596588dd9de6f937f52e5c6e87e6f28ecb866955551/sphinx_rtd_theme-3.0.0.tar.gz", hash = "sha256:905d67de03217fd3d76fbbdd992034ac8e77044ef8063a544dda1af74d409e08", size = 7620317 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/11/81e5bfffdbd6dd3173d5ee29b4629a03ba80d38d4a250e7a8504af22d5c2/sphinx_rtd_theme-3.0.0-py2.py3-none-any.whl", hash = "sha256:1ffe1539957775bfa0a7331370de7dc145b6eac705de23365dc55c5d94bb08e7", size = 7655495 }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300 }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530 }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705 }, +] + +[[package]] +name = "sphinxcontrib-jquery" +version = "4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/f3/aa67467e051df70a6330fe7770894b3e4f09436dea6881ae0b4f3d87cad8/sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a", size = 122331 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/85/749bd22d1a68db7291c89e2ebca53f4306c3f205853cf31e9de279034c3c/sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae", size = 121104 }, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071 }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743 }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072 }, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "executing" }, + { name = "pure-eval" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521 }, +] + +[[package]] +name = "tabulate" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252 }, +] + +[[package]] +name = "threadpoolctl" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/55/b5148dcbf72f5cde221f8bfe3b6a540da7aa1842f6b491ad979a6c8b84af/threadpoolctl-3.5.0.tar.gz", hash = "sha256:082433502dd922bf738de0d8bcc4fdcbf0979ff44c42bd40f5af8a282f6fa107", size = 41936 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/2c/ffbf7a134b9ab11a67b0cf0726453cedd9c5043a4fe7a35d1cefa9a1bcfb/threadpoolctl-3.5.0-py3-none-any.whl", hash = "sha256:56c1e26c150397e58c4926da8eeee87533b1e32bef131bd4bf6a2f45f3185467", size = 18414 }, +] + +[[package]] +name = "tinycss2" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/6f/38d2335a2b70b9982d112bb177e3dbe169746423e33f718bf5e9c7b3ddd3/tinycss2-1.3.0.tar.gz", hash = "sha256:152f9acabd296a8375fbca5b84c961ff95971fcfc32e79550c8df8e29118c54d", size = 67360 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/4d/0db5b8a613d2a59bbc29bc5bb44a2f8070eb9ceab11c50d477502a8a0092/tinycss2-1.3.0-py3-none-any.whl", hash = "sha256:54a8dbdffb334d536851be0226030e9505965bb2f30f21a4a82c55fb2a80fae7", size = 22532 }, +] + +[[package]] +name = "tomli" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/b9/de2a5c0144d7d75a57ff355c0c24054f965b2dc3036456ae03a51ea6264b/tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed", size = 16096 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/db/ce8eda256fa131af12e0a76d481711abe4681b6923c27efb9a255c9e4594/tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38", size = 13237 }, +] + +[[package]] +name = "tornado" +version = "6.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/66/398ac7167f1c7835406888a386f6d0d26ee5dbf197d8a571300be57662d3/tornado-6.4.1.tar.gz", hash = "sha256:92d3ab53183d8c50f8204a51e6f91d18a15d5ef261e84d452800d4ff6fc504e9", size = 500623 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/d9/c33be3c1a7564f7d42d87a8d186371a75fd142097076767a5c27da941fef/tornado-6.4.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:163b0aafc8e23d8cdc3c9dfb24c5368af84a81e3364745ccb4427669bf84aec8", size = 435924 }, + { url = "https://files.pythonhosted.org/packages/2e/0f/721e113a2fac2f1d7d124b3279a1da4c77622e104084f56119875019ffab/tornado-6.4.1-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6d5ce3437e18a2b66fbadb183c1d3364fb03f2be71299e7d10dbeeb69f4b2a14", size = 433883 }, + { url = "https://files.pythonhosted.org/packages/13/cf/786b8f1e6fe1c7c675e79657448178ad65e41c1c9765ef82e7f6f765c4c5/tornado-6.4.1-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e20b9113cd7293f164dc46fffb13535266e713cdb87bd2d15ddb336e96cfc4", size = 437224 }, + { url = "https://files.pythonhosted.org/packages/e4/8e/a6ce4b8d5935558828b0f30f3afcb2d980566718837b3365d98e34f6067e/tornado-6.4.1-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ae50a504a740365267b2a8d1a90c9fbc86b780a39170feca9bcc1787ff80842", size = 436597 }, + { url = "https://files.pythonhosted.org/packages/22/d4/54f9d12668b58336bd30defe0307e6c61589a3e687b05c366f804b7faaf0/tornado-6.4.1-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:613bf4ddf5c7a95509218b149b555621497a6cc0d46ac341b30bd9ec19eac7f3", size = 436797 }, + { url = "https://files.pythonhosted.org/packages/cf/3f/2c792e7afa7dd8b24fad7a2ed3c2f24a5ec5110c7b43a64cb6095cc106b8/tornado-6.4.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25486eb223babe3eed4b8aecbac33b37e3dd6d776bc730ca14e1bf93888b979f", size = 437516 }, + { url = "https://files.pythonhosted.org/packages/71/63/c8fc62745e669ac9009044b889fc531b6f88ac0f5f183cac79eaa950bb23/tornado-6.4.1-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:454db8a7ecfcf2ff6042dde58404164d969b6f5d58b926da15e6b23817950fc4", size = 436958 }, + { url = "https://files.pythonhosted.org/packages/94/d4/f8ac1f5bd22c15fad3b527e025ce219bd526acdbd903f52053df2baecc8b/tornado-6.4.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a02a08cc7a9314b006f653ce40483b9b3c12cda222d6a46d4ac63bb6c9057698", size = 436882 }, + { url = "https://files.pythonhosted.org/packages/4b/3e/a8124c21cc0bbf144d7903d2a0cadab15cadaf683fa39a0f92bc567f0d4d/tornado-6.4.1-cp38-abi3-win32.whl", hash = "sha256:d9a566c40b89757c9aa8e6f032bcdb8ca8795d7c1a9762910c722b1635c9de4d", size = 438092 }, + { url = "https://files.pythonhosted.org/packages/d9/2f/3f2f05e84a7aff787a96d5fb06821323feb370fe0baed4db6ea7b1088f32/tornado-6.4.1-cp38-abi3-win_amd64.whl", hash = "sha256:b24b8982ed444378d7f21d563f4180a2de31ced9d8d84443907a0a64da2072e7", size = 438532 }, +] + +[[package]] +name = "traitlets" +version = "5.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359 }, +] + +[[package]] +name = "triqler" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "scipy" }, + { name = "threadpoolctl" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/4a/ddf639d80698d98199d6dbadaa6dddb3eb57f0492b2663f85586d9ef7296/triqler-0.6.3-py2.py3-none-any.whl", hash = "sha256:1818579192485145af6798a47a3db4dd889cecf6404f51987b339e545a62266b", size = 55727 }, +] + +[[package]] +name = "typeguard" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/e1/3178b3e5369a98239ed7301e3946747048c66f4023163d55918f11b82d4e/typeguard-4.3.0.tar.gz", hash = "sha256:92ee6a0aec9135181eae6067ebd617fd9de8d75d714fb548728a4933b1dea651", size = 73374 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/de/be0ba39ee73760bf33329b7c6f95bc67e96593c69c881671e312538e24bb/typeguard-4.3.0-py3-none-any.whl", hash = "sha256:4d24c5b39a117f8a895b9da7a9b3114f04eb63bade45a4492de49b175b6f7dfa", size = 35385 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] + +[[package]] +name = "tzdata" +version = "2024.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/34/943888654477a574a86a98e9896bae89c7aa15078ec29f490fef2f1e5384/tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc", size = 193282 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/ab/7e5f53c3b9d14972843a647d8d7a853969a58aecc7559cb3267302c94774/tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd", size = 346586 }, +] + +[[package]] +name = "urllib3" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 }, +] + +[[package]] +name = "virtualenv" +version = "20.26.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/40/abc5a766da6b0b2457f819feab8e9203cbeae29327bd241359f866a3da9d/virtualenv-20.26.6.tar.gz", hash = "sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48", size = 9372482 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/90/57b8ac0c8a231545adc7698c64c5a36fa7cd8e376c691b9bde877269f2eb/virtualenv-20.26.6-py3-none-any.whl", hash = "sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2", size = 5999862 }, +] + +[[package]] +name = "wcwidth" +version = "0.2.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 }, +] + +[[package]] +name = "webencodings" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774 }, +] + +[[package]] +name = "wheel" +version = "0.44.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/a0/95e9e962c5fd9da11c1e28aa4c0d8210ab277b1ada951d2aee336b505813/wheel-0.44.0.tar.gz", hash = "sha256:a29c3f2817e95ab89aa4660681ad547c0e9547f20e75b0562fe7723c9a2a9d49", size = 100733 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/d1/9babe2ccaecff775992753d8686970b1e2755d21c8a63be73aba7a4e7d77/wheel-0.44.0-py3-none-any.whl", hash = "sha256:2376a90c98cc337d18623527a97c31797bd02bad0033d41547043a1cbfbe448f", size = 67059 }, +] + +[[package]] +name = "zipp" +version = "3.20.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/bf/5c0000c44ebc80123ecbdddba1f5dcd94a5ada602a9c225d84b5aaa55e86/zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29", size = 24199 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/8b/5ba542fa83c90e09eac972fc9baca7a88e7e7ca4b221a89251954019308b/zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350", size = 9200 }, +]