From 4e29b12d8aa81778fafce5ca3746f96d49a99984 Mon Sep 17 00:00:00 2001 From: Aaron Meyer Date: Sun, 23 Aug 2026 20:17:37 -0700 Subject: [PATCH 1/2] Add MkDocs documentation site and complete docstring coverage Adds a MkDocs + Material + mkdocstrings site (docs/) with an API reference generated from docstrings and two runnable tutorials (CPTAC lung cancer clustering, EBDT kinase-inhibitor clustering), published to GitHub Pages via a new Actions workflow on every push to main. Also fills in missing docstrings across the core library and figures/common.py: every function/method now has a purpose description plus documented arguments and return values, and every module has a top-of-file summary of its contents. Adds a handful of type annotations needed for strict docstring builds, with matching @overload signatures where a plain annotation lost ty's per-call return-type narrowing. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01CQsZJTWRf2sCC9hVH5Zv2Y --- .github/workflows/docs.yml | 37 ++ README.md | 2 + ddmc/__init__.py | 18 +- ddmc/binomial.py | 155 ++++++- ddmc/clustering.py | 177 ++++++-- ddmc/datasets.py | 157 ++++++- ddmc/figures/__init__.py | 4 + ddmc/figures/common.py | 157 ++++++- ddmc/logistic_regression.py | 91 +++- ddmc/motifs.py | 200 ++++++++- ddmc/pam250.py | 50 ++- docs/index.md | 112 +++++ docs/reference/clustering.md | 9 + docs/reference/datasets.md | 12 + docs/reference/distances.md | 26 ++ docs/reference/figures_common.md | 17 + docs/reference/logistic_regression.md | 13 + docs/reference/motifs.md | 16 + docs/tutorials/cptac_clustering.md | 166 ++++++++ docs/tutorials/ebdt_clustering.md | 98 +++++ makefile | 6 + mkdocs.yml | 61 +++ pyproject.toml | 5 + uv.lock | 575 +++++++++++++++++++++++--- 24 files changed, 2002 insertions(+), 162 deletions(-) create mode 100644 .github/workflows/docs.yml create mode 100644 ddmc/figures/__init__.py create mode 100644 docs/index.md create mode 100644 docs/reference/clustering.md create mode 100644 docs/reference/datasets.md create mode 100644 docs/reference/distances.md create mode 100644 docs/reference/figures_common.md create mode 100644 docs/reference/logistic_regression.md create mode 100644 docs/reference/motifs.md create mode 100644 docs/tutorials/cptac_clustering.md create mode 100644 docs/tutorials/ebdt_clustering.md create mode 100644 mkdocs.yml diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..b0e07512 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,37 @@ +name: Docs + +on: + push: + branches: [main] + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v3 + - name: Install dependencies + run: uv sync --group docs + - name: Build docs + run: uv run mkdocs build --strict --site-dir _site + - uses: actions/configure-pages@v5 + - uses: actions/upload-pages-artifact@v3 + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v4 diff --git a/README.md b/README.md index cdefc51a..bf62a9a2 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ Clusters peptides based on both sequence similarity and phosphorylation signal a This repository contains the implementation of dual data and motif clustering (DDMC), described in: +Full documentation, including an API reference and tutorials, is published at [meyer-lab.github.io/DDMC](https://meyer-lab.github.io/DDMC/). + > Creixell M, Meyer AS. [Dual data and motif clustering improves the modeling and interpretation of phosphoproteomic data](https://pubmed.ncbi.nlm.nih.gov/35360705/). *Cell Rep Methods*. 2022 Feb 28;2(2):100167. doi: [10.1016/j.crmeth.2022.100167](https://doi.org/10.1016/j.crmeth.2022.100167) **Abstract:** Cell signaling is orchestrated in part through a network of protein kinases and phosphatases. Dysregulation of kinase signaling is widespread in diseases such as cancer and is readily targetable through inhibitors. Mass spectrometry-based analysis can provide a global view of kinase regulation, but mining these data is complicated by its stochastic coverage of the proteome, measurement of substrates rather than kinases, and the scale of the data. Here, we implement a dual data and motif clustering (DDMC) strategy that simultaneously clusters peptides into similarly regulated groups based on their variation and their sequence profile. We show that this can help to identify putative upstream kinases and supply more robust clustering. We apply this clustering to clinical proteomic profiling of lung cancer and identify conserved proteomic signatures of tumorigenicity, genetic mutations, and immune infiltration. We propose that DDMC provides a general and flexible clustering strategy for the analysis of phosphoproteomic data. diff --git a/ddmc/__init__.py b/ddmc/__init__.py index b52ad464..f1b7072e 100644 --- a/ddmc/__init__.py +++ b/ddmc/__init__.py @@ -1,4 +1,9 @@ -"""This is the __init__.py file.""" +"""Package entry point for `ddmc`. + +Sets `__version__`, and patches scikit-learn's `check_array` so the +unmaintained `fancyimpute` dependency (used by `ddmc.clustering.DDMC` for +missing-value imputation) keeps working against modern scikit-learn. +""" import sklearn.utils @@ -12,6 +17,17 @@ def _check_array_compat(X, **kwargs): + """Translate the removed `force_all_finite` kwarg to `ensure_all_finite` and + delegate to the original `sklearn.utils.check_array`. + + Args: + X: The array-like to validate; forwarded unchanged. + **kwargs: Keyword arguments for `check_array`. If `force_all_finite` + is present, it is renamed to `ensure_all_finite`. + + Returns: + The validated array, as returned by the original `check_array`. + """ if "force_all_finite" in kwargs: kwargs["ensure_all_finite"] = kwargs.pop("force_all_finite") return _sklearn_check_array(X, **kwargs) diff --git a/ddmc/binomial.py b/ddmc/binomial.py index 09cd4d7f..62ac6617 100644 --- a/ddmc/binomial.py +++ b/ddmc/binomial.py @@ -1,7 +1,23 @@ -"""Binomial probability calculation to compute sequence distance between sequences and clusters.""" +"""Binomial sequence-distance model used by `ddmc.clustering.DDMC`. + +Contains: + - `AAfreq` / `AAlist`: reference amino acid frequencies and the fixed + amino acid ordering used throughout the package. + - Position weight matrix helpers (`position_weight_matrix`, + `fast_position_weight_matrix`, `frequencies`, `GenerateBinarySeqID`). + - Background phosphosite sequence sampling from PhosphoSitePlus + (`BackgroundSeqs`, `BackgProportions`, `CountPsiteTypes`, and their + cached loaders). + - The `Binomial` class: for each cluster, models how enriched each + amino acid is at each position (relative to the background) using the + binomial-probability approach of Schwartz & Gygi, *Nat Biotechnol* + 2005 (doi:10.1038/nbt1146), and scores every peptide sequence against + each cluster's model. +""" from collections import OrderedDict from functools import lru_cache +from typing import Any import numpy as np import pandas as pd @@ -44,15 +60,36 @@ _AAbyteLookup[ord(_aa)] = _i -def position_weight_matrix(seqs, pseudoC=AAfreq): - """Build PWM of a given set of sequences.""" +def position_weight_matrix( + seqs: list[str], pseudoC: OrderedDict[str, float] = AAfreq +) -> Any: + """Build a position weight matrix (PWM) of a given set of same-length sequences. + + Args: + seqs: Sequences (all the same length) to build the PWM from. + pseudoC: Per-amino-acid pseudocounts to add before normalizing, + keyed by one-letter amino acid code. Defaults to `AAfreq`. + + Returns: + A Biopython `PositionWeightMatrix` (amino acid frequency per + position, normalized to sum to 1 down each column) of shape + (len(AAlist), sequence length). + """ return frequencies(seqs).normalize(pseudocounts=pseudoC) def fast_position_weight_matrix(seqs: list[str]) -> np.ndarray: """Build a (len(AAlist), seq_length) PWM of a given set of same-length sequences, equivalent to `position_weight_matrix` but without the - overhead of Biopython's general-purpose alignment machinery.""" + overhead of Biopython's general-purpose alignment machinery. + + Args: + seqs: Sequences, all of the same length, to build the PWM from. + + Returns: + Array of shape (len(AAlist), sequence length) giving the + pseudocount-smoothed frequency of each amino acid at each position. + """ seq_len = len(seqs[0]) # Convert to fixed-width bytes and view as a 2D uint8 array so the # char->index lookup is a single vectorized gather instead of a nested @@ -70,13 +107,30 @@ def fast_position_weight_matrix(seqs: list[str]) -> np.ndarray: ) -def frequencies(seqs: list[str]): - """Build counts matrix of a given set of sequences.""" +def frequencies(seqs: list[str]) -> Any: + """Build a per-position amino acid counts matrix of a given set of same-length sequences. + + Args: + seqs: Sequences, all of the same length, to count. + + Returns: + A Biopython `FrequencyPositionMatrix` giving the raw count of each + amino acid at each position across `seqs`. + """ return motifs.create(seqs, alphabet="".join(AAlist)).counts -def GenerateBinarySeqID(seqs) -> np.ndarray: - """Build matrix with 0s and 1s to identify residue/position pairs for every sequence""" +def GenerateBinarySeqID(seqs: list[str] | np.ndarray) -> np.ndarray: + """Build a one-hot encoding of amino acid identity at each position, for every sequence. + + Args: + seqs: Length-11 peptide sequences to encode. + + Returns: + Boolean array of shape (len(seqs), len(AAlist), 11), where + `result[i, j, k]` is True if sequence `i` has amino acid `AAlist[j]` + at position `k`. + """ res = np.zeros((len(seqs), len(AAlist), 11), dtype=bool) for ii, seq in enumerate(seqs): for pos, aa in enumerate(seq): @@ -85,12 +139,24 @@ def GenerateBinarySeqID(seqs) -> np.ndarray: def BackgroundSeqs(forseqs: np.ndarray) -> list[str]: - """Build Background data set with the same proportion of pY, pT, and pS motifs as in the foreground set of sequences. + """Build a background data set of length-11 phosphosite motifs sampled from + PhosphoSitePlus, matching the proportion of pY, pT, and pS sites found in + the foreground set of sequences. + Note this PsP data set contains 51976 pY, 226131 pS, 81321 pT Source: https://www.phosphosite.org/staticDownloads.action - Phosphorylation_site_dataset.gz - Last mod: Wed Dec 04 14:56:35 EST 2019 Cite: Hornbeck PV, Zhang B, Murray B, Kornhauser JM, Latham V, Skrzypek E PhosphoSitePlus, 2014: mutations, - PTMs and recalibrations. Nucleic Acids Res. 2015 43:D512-20. PMID: 25514926""" + PTMs and recalibrations. Nucleic Acids Res. 2015 43:D512-20. PMID: 25514926 + + Args: + forseqs: The foreground peptide sequences whose pY/pS/pT proportions + the background set should match. + + Returns: + Length-11 background peptide sequences sampled from PhosphoSitePlus, + with the phosphoacceptor lowercased, in pY/pS/pT order. + """ # Get porportion of psite types in foreground set forw_pYn, forw_pSn, forw_pTn = CountPsiteTypes(forseqs) forw_tot = forw_pYn + forw_pSn + forw_pTn @@ -124,7 +190,14 @@ def BackgroundSeqs(forseqs: np.ndarray) -> list[str]: def _load_reference_seqs() -> tuple[tuple[str, ...], int]: """Load and filter the PhosphoSitePlus background sequences. This file never changes at runtime, so cache it instead of re-reading and - re-filtering the CSV on every `BackgroundSeqs` call.""" + re-filtering the CSV on every `BackgroundSeqs` call. + + Returns: + A tuple of `(refseqs, backg_pYn)`, where `refseqs` are the raw + +/-7 AA PhosphoSitePlus reference sequences (ambiguous entries + containing "_" or "X" removed) and `backg_pYn` is the number of + pY sites among them. + """ PsP = pd.read_csv( "./ddmc/data/Sequence_analysis/pX_dataset_PhosphoSitePlus2019.csv" ) @@ -137,12 +210,35 @@ def _load_reference_seqs() -> tuple[tuple[str, ...], int]: @lru_cache(maxsize=32) def _cached_background_proportions(pYn: int, pSn: int, pTn: int) -> tuple[str, ...]: + """Memoized wrapper around `BackgProportions` over the cached reference + sequences, keyed by the requested pY/pS/pT counts. + + Args: + pYn: Number of pY background motifs to include. + pSn: Number of pS background motifs to include. + pTn: Number of pT background motifs to include. + + Returns: + The length-11 background motifs, in pY/pS/pT order. + """ refseqs, _ = _load_reference_seqs() return tuple(BackgProportions(list(refseqs), pYn, pSn, pTn)) def BackgProportions(refseqs: list[str], pYn: int, pSn: int, pTn: int) -> list[str]: - """Provided the proportions, add peptides to background set.""" + """Slice length-11 motifs out of the +/-7 AA reference sequences, keeping + up to the requested number of pY, pS, and pT sites. + + Args: + refseqs: Raw +/-7 AA PhosphoSitePlus reference sequences. + pYn: Maximum number of pY motifs to keep. + pSn: Maximum number of pS motifs to keep. + pTn: Maximum number of pT motifs to keep. + + Returns: + The length-11 background motifs (phosphoacceptor lowercased), + concatenated in pY, pS, pT order. + """ y_seqs: list[str] = [] s_seqs: list[str] = [] t_seqs: list[str] = [] @@ -171,9 +267,31 @@ def BackgProportions(refseqs: list[str], pYn: int, pSn: int, pTn: int) -> list[s class Binomial: - """Definition of the binomial sequence distance distribution.""" + """Binomial sequence-distance model, used by `ddmc.clustering.DDMC` when + `distance_method="Binomial"`. + + For each cluster, scores how enriched each amino acid is at each + position of a peptide's sequence relative to a background distribution + of phosphosites, following Schwartz & Gygi, *Nat Biotechnol* 2005 + (doi:10.1038/nbt1146). + + Attributes: + background: Background PWM (amino acid frequency per position) of + shape (len(AAlist), n_pos), built from `BackgroundSeqs(seqs)`. + n_aa: Number of amino acids (len(AAlist)). + n_pos: Number of sequence positions (11). + foreground_flat: Flattened one-hot encoding of `seqs`, of shape + (n_seqs, n_aa * n_pos). + logWeights: Log-probability of each sequence under each cluster's + current binomial model, of shape (n_seqs, n_clusters). Set to + the scalar `0.0` until `from_summaries` is first called. + """ def __init__(self, seqs: np.ndarray): + """ + Args: + seqs: The length-11 peptide sequences being clustered. + """ # Background sequences self.background = fast_position_weight_matrix(BackgroundSeqs(seqs)) foreground: np.ndarray = GenerateBinarySeqID(seqs) @@ -189,8 +307,15 @@ def __init__(self, seqs: np.ndarray): assert np.all(np.isfinite(self.background)) assert np.all(np.isfinite(self.foreground_flat)) - def from_summaries(self, weightsIn: np.ndarray): - """Update the underlying distribution.""" + def from_summaries(self, weightsIn: np.ndarray) -> None: + """Refit each cluster's binomial model from the current soft cluster + assignments, and update `self.logWeights` with each sequence's + log-probability under its (updated) cluster model. + + Args: + weightsIn: Soft cluster assignments (responsibilities) of shape + (n_seqs, n_clusters), i.e. `exp(log_resp)` from the EM E step. + """ k_flat = weightsIn.T.astype(np.float32) @ self.foreground_flat k = k_flat.reshape(-1, self.n_aa, self.n_pos) betaA = np.sum(weightsIn, axis=0)[:, None, None] - k diff --git a/ddmc/clustering.py b/ddmc/clustering.py index 012b403a..5f011813 100644 --- a/ddmc/clustering.py +++ b/ddmc/clustering.py @@ -1,4 +1,10 @@ -"""Clustering functions.""" +"""Dual data and motif clustering (DDMC). + +Contains the `DDMC` model itself — a `sklearn.mixture.GaussianMixture` +subclass that jointly clusters peptides on their phosphorylation signal and +their sequence motif — and `get_pspl_pssm_distances`, the helper it uses to +compare cluster motifs against kinase specificity profiles. +""" import warnings from collections.abc import Sequence @@ -17,17 +23,53 @@ class DDMC(GaussianMixture): """Cluster peptides by both sequence similarity and condition-wise phosphorylation following an - expectation-maximization algorithm.""" + expectation-maximization algorithm. + + `DDMC` subclasses `sklearn.mixture.GaussianMixture` and reuses its EM + loop, but scores each peptide against each cluster using both the usual + Gaussian mixture log-probability over its phosphorylation signal and a + sequence-motif term (weighted by `seq_weight`), and refits both the + Gaussian mixture parameters and the per-cluster sequence motif at every + M step. See `ddmc.binomial.Binomial` and `ddmc.pam250.PAM250` for the + two available motif models. + + Attributes set by `fit`: + p_signal: The `p_signal` DataFrame passed to `fit`. + sequences: `p_signal.index`, as an upper-cased numpy array. + seq_dist: The fitted `Binomial` or `PAM250` sequence-distance model. + scores_: Per-peptide, per-cluster responsibilities (soft cluster + assignments) of shape (n_peptides, n_components). + seq_scores_: Per-peptide, per-cluster weighted sequence + log-probabilities (`seq_weight * seq_dist.logWeights`) from the + last E step, of shape (n_peptides, n_components). + """ def __init__( self, n_components: int, seq_weight: float, distance_method: Literal["PAM250", "Binomial"] = "Binomial", - random_state=None, - max_iter=200, - tol=1e-4, + random_state: int | np.random.RandomState | None = None, + max_iter: int = 200, + tol: float = 1e-4, ): + """ + Args: + n_components: The number of clusters to fit. + seq_weight: Weight applied to the sequence-motif log-probability + relative to the Gaussian mixture log-probability when + scoring each peptide against each cluster. `0` reduces + `DDMC` to an ordinary Gaussian mixture model. + distance_method: Which sequence-distance model to use for the + motif term: `"Binomial"` (`ddmc.binomial.Binomial`) or + `"PAM250"` (`ddmc.pam250.PAM250`). + random_state: Seed or `numpy.random.RandomState` controlling the + random initialization of the underlying Gaussian mixture, + for reproducibility. + max_iter: Maximum number of EM iterations to run. + tol: Convergence threshold on the change in per-sample average + log-likelihood between EM iterations. + """ super().__init__( n_components=n_components, covariance_type="diag", @@ -39,7 +81,15 @@ def __init__( self.distance_method = distance_method self.seq_weight = seq_weight - def _gen_peptide_distances(self, sequences, distance_method): + def _gen_peptide_distances(self, sequences, distance_method) -> None: + """Build `self.seq_dist`, the sequence-distance model used for the + motif term of the E and M steps. + + Args: + sequences: The length-11 peptide sequences being clustered. + distance_method: Which sequence-distance model to construct: + `"Binomial"` or `"PAM250"`. + """ sequences = np.asarray(sequences, dtype=str) sequences = np.char.upper(sequences) self.sequences = sequences @@ -50,8 +100,22 @@ def _gen_peptide_distances(self, sequences, distance_method): else: raise ValueError("Wrong distance type.") - def _estimate_log_prob(self, X: np.ndarray, xp=None): - """Estimate the log-probability of each point in each cluster.""" + def _estimate_log_prob(self, X: np.ndarray, xp=None) -> np.ndarray: + """EM E-step helper. Estimate the log-probability of each peptide + under each cluster, combining the Gaussian mixture log-probability + over `X` with the weighted sequence-motif log-probability. + + Args: + X: Phosphorylation signal of shape (n_samples, n_features), with + any missing values already imputed. + xp: Array-API namespace to use, forwarded to + `GaussianMixture._estimate_log_prob` (unused directly here). + + Returns: + Combined log-probability of each sample under each cluster, of + shape (n_samples, n_components). Also stored as + `self.seq_scores_` (the sequence-only term). + """ logp = super()._estimate_log_prob(X, xp=xp) # Do the regular work # Add in the sequence effect @@ -60,14 +124,21 @@ def _estimate_log_prob(self, X: np.ndarray, xp=None): return logp - def _m_step(self, X: np.ndarray, log_resp: np.ndarray, xp=None): - """M step. - Parameters - ---------- - X : array-like of shape (n_samples, n_features) - log_resp : array-like of shape (n_samples, n_components) - Logarithm of the posterior probabilities (or responsibilities) of - the point of each sample in X. + def _m_step(self, X: np.ndarray, log_resp: np.ndarray, xp=None) -> None: + """EM M-step. Impute missing values from the current cluster + centers, then refit both the Gaussian mixture parameters and the + sequence-motif model from the current responsibilities. + + Args: + X: Phosphorylation signal of shape (n_samples, n_features). If + `self._missing`, entries at `self.missing_d` are overwritten + in place with each peptide's assigned cluster's center + before the regular Gaussian mixture M step runs. + log_resp: Logarithm of the posterior probabilities (or + responsibilities) of each sample in `X`, of shape + (n_samples, n_components). + xp: Array-API namespace to use, forwarded to + `GaussianMixture._m_step` (unused directly here). """ if self._missing: labels = np.argmax(log_resp, axis=1) @@ -82,7 +153,7 @@ def _m_step(self, X: np.ndarray, log_resp: np.ndarray, xp=None): # Do sequence m step self.seq_dist.from_summaries(np.exp(log_resp)) - def fit(self, p_signal: pd.DataFrame): # ty: ignore[invalid-method-override] + def fit(self, p_signal: pd.DataFrame) -> "DDMC": # ty: ignore[invalid-method-override] """ Compute EM clustering. @@ -91,6 +162,9 @@ def fit(self, p_signal: pd.DataFrame): # ty: ignore[invalid-method-override] containing the phosphorylation signal. `p_signal.index` contains the length-11 AA sequence of each peptide, containing the phosphoacceptor in the middle and five AAs flanking it. + + Returns: + self, fit to `p_signal`. """ assert isinstance(p_signal, pd.DataFrame), ( "`p_signal` must be a pandas dataframe." @@ -132,7 +206,11 @@ def fit(self, p_signal: pd.DataFrame): # ty: ignore[invalid-method-override] assert np.all(np.isfinite(self.seq_scores_)) return self - def transform(self, as_df=False) -> np.ndarray | pd.DataFrame: + @overload + def transform(self, as_df: Literal[False] = False) -> np.ndarray: ... + @overload + def transform(self, as_df: Literal[True]) -> pd.DataFrame: ... + def transform(self, as_df: bool = False) -> np.ndarray | pd.DataFrame: """ Return cluster centers. @@ -157,6 +235,10 @@ def impute(self) -> pd.DataFrame: """ Imputes missing values in the dataset passed in fit() and returns the imputed dataset. + + Returns: + A copy of the `p_signal` passed to `fit`, with each peptide's + missing samples filled in from its assigned cluster's center. """ p_signal = self.p_signal.copy() labels = self.labels() # cluster assignments @@ -168,8 +250,16 @@ def impute(self) -> pd.DataFrame: assert np.all(np.isfinite(p_signal)) return p_signal + @overload + def get_pssms( + self, PsP_background: bool = False, clusters: None = None + ) -> tuple[np.ndarray, np.ndarray]: ... + @overload def get_pssms( - self, PsP_background=False, clusters: list[int] | None = None + self, PsP_background: bool = False, *, clusters: list[int] + ) -> np.ndarray: ... + def get_pssms( + self, PsP_background: bool = False, clusters: list[int] | None = None ) -> tuple[np.ndarray, np.ndarray] | np.ndarray: """ Compute position-specific scoring matrix of each cluster. @@ -253,10 +343,21 @@ def get_pssms( def predict_upstream_kinases( self, - PsP_background=True, + PsP_background: bool = True, ) -> pd.DataFrame: """Compute matrix-matrix similarity between kinase specificity profiles - and cluster PSSMs to identify upstream kinases regulating clusters.""" + and cluster PSSMs to identify upstream kinases regulating clusters. + + Args: + PsP_background: Whether or not PhosphoSitePlus should be used + for the background amino acid frequency when building each + cluster's PSSM (see `get_pssms`). + + Returns: + DataFrame of shape (n_kinases, n_nonempty_clusters) with a + Frobenius distance between each kinase's specificity profile and + each cluster's PSSM; smaller values indicate a better match. + """ kinases, pspls = get_pspls() clusters, pssms = self.get_pssms(PsP_background=PsP_background) distances = get_pspl_pssm_distances( @@ -269,26 +370,52 @@ def predict_upstream_kinases( return distances def get_nonempty_clusters(self) -> np.ndarray: + """List the clusters that at least one peptide is assigned to. + + Returns: + Sorted array of the distinct cluster indices present in + `self.labels()`; shorter than `n_components` if any clusters + are empty. + """ return np.unique(self.labels()) def has_empty_clusters(self) -> bool: """ Checks whether the most recent call to fit() resulted in empty clusters. + + Returns: + True if any of the `n_components` clusters has no peptides + assigned to it. """ check_is_fitted(self, ["scores_"]) return self.get_nonempty_clusters().size != self.n_components def predict(self) -> np.ndarray: # ty: ignore[invalid-method-override] - """Provided the current model parameters, predict the cluster each peptide belongs to.""" + """Provided the current model parameters, predict the cluster each peptide belongs to. + + Returns: + Array of shape (n_peptides,) giving the index of the + highest-likelihood cluster for each peptide in `self.p_signal`. + """ check_is_fitted(self, ["scores_"]) return np.argmax(self.scores_, axis=1) def labels(self) -> np.ndarray: - """Find cluster assignment with highest likelihood for each peptide.""" + """Find cluster assignment with highest likelihood for each peptide. + + Returns: + Array of shape (n_peptides,) giving each peptide's cluster + index. Equivalent to `predict()`. + """ return self.predict() def score(self) -> float: # ty: ignore[invalid-method-override] - """Generate score of the fitting.""" + """Generate score of the fitting. + + Returns: + The lower bound on the log-likelihood of the fitted model + (`self.lower_bound_`, set by `GaussianMixture.fit`). + """ check_is_fitted(self, ["lower_bound_"]) return self.lower_bound_ @@ -312,7 +439,7 @@ def get_pspl_pssm_distances( def get_pspl_pssm_distances( pspls: np.ndarray, pssms: np.ndarray, - as_df=False, + as_df: bool = False, pssm_names: Sequence | np.ndarray | None = None, kinases: Sequence | np.ndarray | None = None, ) -> np.ndarray | pd.DataFrame: diff --git a/ddmc/datasets.py b/ddmc/datasets.py index 02e4cb7d..81f3f3cc 100644 --- a/ddmc/datasets.py +++ b/ddmc/datasets.py @@ -1,6 +1,21 @@ +"""Loaders for the mass-spec datasets bundled with the package. + +Contains: + - `CPTAC`: the CPTAC lung cancer clinical phosphoproteomics cohort, plus + accompanying clinical metadata (mutation calls, tumor/NAT status, + hot/cold immune infiltration labels) used in the DDMC paper. + - `EBDT`: the MCF7 kinase-inhibitor phosphoproteomics dataset from + Hijazi et al., *Nat Biotechnol* 2020, remapped onto DDMC's length-11 + sequence-motif representation. + - `filter_incomplete_peptides` / `select_peptide_subset`: preprocessing + helpers for filtering a `p_signal` DataFrame by missingness or down + to a random subset of peptides, for use before `ddmc.clustering.DDMC.fit`. +""" + import re from collections.abc import Sequence from pathlib import Path +from typing import Literal, overload import numpy as np import pandas as pd @@ -15,7 +30,7 @@ def filter_incomplete_peptides( sample_presence_ratio: float | None = None, min_experiments: int | None = None, sample_to_experiment: np.ndarray | None = None, -): +) -> pd.DataFrame: """ Filters out missing values from p-signal array. @@ -58,10 +73,23 @@ def filter_incomplete_peptides( def select_peptide_subset( - p_signal: pd.DataFrame, keep_ratio: float | None = None, keep_num: int | None = None -): + p_signal: pd.DataFrame, + keep_ratio: float | None = None, + keep_num: int | None = None, +) -> pd.DataFrame: """ Selects a random subset of peptides from p_signal. + + Args: + p_signal: Phosphorylation signal, indexed by peptide sequence. + keep_ratio: Fraction of peptides to keep; if given, overrides + `keep_num` with `int(p_signal.shape[0] * keep_ratio)`. + keep_num: Number of peptides to keep. Required if `keep_ratio` is + not given. + + Returns: + A random subset of the rows of `p_signal` (sampled with + replacement), of shape (keep_num, p_signal.shape[1]). """ if keep_ratio is not None: keep_num = int(p_signal.shape[0] * keep_ratio) @@ -69,15 +97,49 @@ def select_peptide_subset( class CPTAC: + """Loader for the CPTAC lung cancer clinical phosphoproteomics cohort and + its accompanying clinical metadata. + + Sample columns throughout this dataset are patient IDs, with tumor + samples given plain (e.g. `"C3L.00001"`) and their matched adjacent + normal tissue (NAT) samples suffixed with `".N"` (e.g. `"C3L.00001.N"`). + """ + data_dir = DATA_DIR / "MS" / "CPTAC" - def get_sample_to_experiment(self, as_df=False): + @overload + def get_sample_to_experiment(self, as_df: Literal[False] = False) -> np.ndarray: ... + @overload + def get_sample_to_experiment(self, as_df: Literal[True]) -> pd.DataFrame: ... + def get_sample_to_experiment(self, as_df: bool = False) -> np.ndarray | pd.DataFrame: + """Load the mapping from sample to the TMT experiment it was run in. + + Args: + as_df: If True, return the raw DataFrame read from + `IDtoExperiment.csv` instead of just the experiment column. + + Returns: + If `as_df`, the full `IDtoExperiment.csv` DataFrame. Otherwise, an + array of shape `(n_samples,)` giving each sample's experiment + identifier, aligned to that CSV's row order. + """ sample_to_experiment = pd.read_csv(self.data_dir / "IDtoExperiment.csv") if as_df: return sample_to_experiment return sample_to_experiment.iloc[:, 1].values - def get_p_signal(self, min_experiments=2) -> pd.DataFrame: + def get_p_signal(self, min_experiments: int = 2) -> pd.DataFrame: + """Load the CPTAC phosphorylation signal matrix. + + Args: + min_experiments: The minimum number of TMT experiments a + peptide must be observed in to be kept; passed to + `filter_incomplete_peptides`. + + Returns: + DataFrame of phosphorylation signal, indexed by the length-11 + peptide sequence, with one column per sample. + """ p_signal = pd.read_csv(self.data_dir / "CPTAC-preprocessedMotifs.csv").iloc[ :, 1: ] @@ -92,6 +154,17 @@ def get_p_signal(self, min_experiments=2) -> pd.DataFrame: def get_patients_with_nat_and_tumor(self, samples) -> np.ndarray: """ Get patients that have both NAT and tumor samples. + + Args: + samples (Sequence[str] | numpy.ndarray): Sample identifiers to + consider (tumor samples plain, NAT samples suffixed with + `".N"`). Pooled internal-reference channels (containing + `"IR"`, e.g. `"Tumor.Only.IR"`) are ignored, as they are not + real patient samples. + + Returns: + Sorted array of patient IDs (the tumor-sample form, without + `".N"`) present in `samples` as both a tumor and a NAT sample. """ samples = np.asarray(samples, dtype=str) samples = samples[np.char.find(samples, "IR") == -1] @@ -104,6 +177,18 @@ def get_patients_with_nat_and_tumor(self, samples) -> np.ndarray: def get_mutations( self, mutation_names: Sequence[str] | None = None ) -> pd.DataFrame: + """Load per-patient genetic mutation calls. + + Args: + mutation_names: If given, restrict the result to these mutation + columns (as named in `Patient_Mutations.csv`, e.g. + `"EGFR.mutation.status"`). Defaults to all mutation columns. + + Returns: + Boolean DataFrame indexed by patient ID (restricted to patients + with both a tumor and NAT sample), with one column per + mutation, True where that patient carries the mutation. + """ mutations = pd.read_csv(self.data_dir / "Patient_Mutations.csv") mutations = mutations.set_index("Sample.ID") patients = self.get_patients_with_nat_and_tumor(mutations.index.values) @@ -113,6 +198,17 @@ def get_mutations( return mutations.astype(bool) def get_hot_cold_labels(self) -> pd.Series: + """Load per-patient immune infiltration ("hot"/"cold" tumor) labels. + + Tumor samples labeled "NAT enriched" (ambiguous/mixed signal) are + dropped, as are NAT samples themselves (this label only applies to + tumor samples). + + Returns: + Boolean Series indexed by patient ID, True for immunologically + "hot" tumors ("Hot-tumor enriched") and False for "cold" tumors + ("Cold-tumor enriched"). + """ hot_cold = ( pd.read_csv(self.data_dir / "Hot_Cold.csv") .dropna(axis=1) @@ -126,17 +222,42 @@ def get_hot_cold_labels(self) -> pd.Series: hot_cold = hot_cold.dropna() return np.squeeze(hot_cold).astype(bool) - def get_tumor_or_nat(self, samples: Sequence[str]) -> np.ndarray: + def get_tumor_or_nat(self, samples: Sequence[str] | pd.Index) -> np.ndarray: """ Get tumor vs NAT for each of samples. Returned array contains True if tumor. + + Args: + samples: Sample identifiers (tumor samples plain, NAT samples + suffixed with `".N"`). + + Returns: + Boolean array of shape `(len(samples),)`, aligned to `samples`, + True where the sample is a tumor sample (not NAT). """ return ~np.array([sample.endswith(".N") for sample in samples]) # MCF7 mass spec data set from EBDT (Hijazi et al Nat Biotech 2020) class EBDT: + """Loader for the MCF7 kinase-inhibitor phosphoproteomics dataset from + Hijazi et al., *Nat Biotechnol* 2020. Each sample column is the + fold-change in phosphorylation signal for MCF7 cells treated with a + given kinase inhibitor, relative to control. + """ + def get_p_signal(self) -> pd.DataFrame: + """Load the EBDT phosphorylation fold-change matrix. + + Reads the raw per-site CSV, maps each site onto the human proteome + to build DDMC's length-11 sequence-motif representation (via + `pos_to_motif`), and drops any site that fails to map. + + Returns: + DataFrame of phosphorylation fold-change, indexed by the + length-11 peptide sequence, with one column per inhibitor + treatment. + """ p_signal = ( pd.read_csv(DATA_DIR / "Validations" / "Computational" / "ebdt_mcf7.csv") .drop("FDR", axis=1) @@ -161,8 +282,28 @@ def get_p_signal(self) -> pd.DataFrame: p_signal = p_signal.set_index("Sequence") return p_signal - def pos_to_motif(self, genes, pos): - """Map p-site sequence position to uniprot's proteome and extract motifs.""" + def pos_to_motif( + self, genes: Sequence[str], pos: Sequence[str] + ) -> tuple[list[str], list[list[str]]]: + """Map p-site sequence position to uniprot's proteome and extract motifs. + + Args: + genes: Gene name for each phosphosite (used to look up the + protein sequence in the UniProt proteome). + pos: Phosphosite position for each entry, formatted as the + phosphoacceptor residue letter followed by its 1-indexed + position in the protein (e.g. `"S104"`). + + Returns: + A tuple `(motifs, del_ids)`: + motifs: The length-11 sequence motif (5 AAs flanking the + phosphoacceptor on each side, phosphoacceptor + lowercased) for each successfully mapped site. + del_ids: `[gene, pos]` pairs that could not be mapped + (gene missing from the proteome, position out of range, + or the residue at that position isn't S/T/Y), to be + dropped from the corresponding `p_signal` rows. + """ proteome = open(DATA_DIR / "Sequence_analysis" / "proteome_uniprot2019.fa") motif_size = 5 ProteomeDict = get_proteome_name_to_seq(proteome, n="gene") diff --git a/ddmc/figures/__init__.py b/ddmc/figures/__init__.py new file mode 100644 index 00000000..2056ddac --- /dev/null +++ b/ddmc/figures/__init__.py @@ -0,0 +1,4 @@ +"""Scripts that reproduce each figure in the DDMC paper, plus `common.py`'s +shared plotting helpers. Each `figureM*.py` module exposes a `makeFigure()` +function, invoked by the `fbuild` console script (`ddmc.figures.common.genFigure`). +""" diff --git a/ddmc/figures/common.py b/ddmc/figures/common.py index 14dcbd8d..1bb9b423 100644 --- a/ddmc/figures/common.py +++ b/ddmc/figures/common.py @@ -1,5 +1,19 @@ -""" -This file contains functions that are used in multiple figures. +"""Shared plotting and figure-assembly helpers used across `ddmc/figures/figureM*.py`. + +Contains: + - `getSetup` / `subplotLabel` / `overlayCartoon`: build a labeled + multi-panel matplotlib figure and overlay static SVG cartoons onto it. + - `genFigure`: the `fbuild` console-script entry point (see + `pyproject.toml`) that generates a given `figureM*.py` module's + figure and saves it to `./output/`. + - `plot_motifs`: sequence-logo plot of a `DDMC` cluster's PSSM. + - `plot_cluster_kinase_distances`: strip plot of kinase-vs-cluster PSSM + distances, annotated with the top kinase hit(s) per cluster. + - `get_pvals_across_clusters` / `plot_p_signal_across_clusters_and_binary_feature`: + statistically compare cluster centers between two groups of samples + and plot the result as an annotated violin plot. + - `plot_pca_on_cluster_centers`: PCA scores/loadings plot of cluster + centers. """ import importlib @@ -30,9 +44,24 @@ def getSetup( figsize: tuple[int, int], gridd: tuple[int, int], multz: None | dict = None, - labels=True, + labels: bool = True, ) -> tuple: - """Establish figure set-up with subplots.""" + """Establish figure set-up with subplots. + + Args: + figsize: Figure size in inches, as `(width, height)`. + gridd: Subplot grid shape, as `(n_rows, n_cols)`. + multz: Maps a subplot's flat grid index to how many extra + consecutive grid cells it should span (e.g. `{0: 2}` makes the + subplot at index 0 span indices 0-2). Spanned indices are + skipped when placing subsequent subplots. + labels: Whether to add bold uppercase letter labels (A, B, C, ...) + to each subplot via `subplotLabel`. + + Returns: + A tuple `(ax, f)` of the list of created `Axes` (in grid order) and + the parent `Figure`. + """ sns.set( style="whitegrid", font_scale=0.7, @@ -65,8 +94,12 @@ def getSetup( return (ax, f) -def subplotLabel(axs: list[axes.Axes]): - """Place subplot labels on the list of axes.""" +def subplotLabel(axs: list[axes.Axes]) -> None: + """Place bold uppercase letter labels (A, B, C, ...) above each axes. + + Args: + axs: Axes to label, in the order they should be lettered. + """ for ii, ax in enumerate(axs): ax.text( -0.2, @@ -81,8 +114,16 @@ def subplotLabel(axs: list[axes.Axes]): def overlayCartoon( figFile: str, cartoonFile: str, x: float, y: float, scalee: float = 1.0 -): - """Add cartoon to a figure file.""" +) -> None: + """Overlay a static SVG cartoon onto a saved figure, in place. + + Args: + figFile: Path to the SVG figure to overlay onto and overwrite. + cartoonFile: Path to the SVG cartoon to overlay. + x: X position (in SVG units) to place the cartoon's origin. + y: Y position (in SVG units) to place the cartoon's origin. + scalee: Uniform scale factor applied to the cartoon. + """ # Overlay Figure cartoons template = st.fromfile(figFile) @@ -94,8 +135,16 @@ def overlayCartoon( template.save(figFile) -def genFigure(): - """Main figure generation function.""" +def genFigure() -> None: + """Console-script entry point (`fbuild`, see `pyproject.toml`) for + generating one paper figure. + + Reads the figure name suffix from `sys.argv[1]` (e.g. `"M2"`), imports + the corresponding `ddmc.figures.figureM2` module, calls its + `makeFigure()`, and saves the result to `./output/figureM2.svg`. Some + figures (`M2`, `M5`) additionally get a static SVG cartoon overlaid via + `overlayCartoon` after saving. + """ start = time.time() nameOut = "figure" + sys.argv[1] @@ -130,8 +179,20 @@ def genFigure(): print(f"Figure {sys.argv[1]} is done after {time.time() - start} seconds.\n") -def plot_motifs(pssm, ax: axes.Axes, titles=False, yaxis=False): - """Generate logo plots of a list of PSSMs""" +def plot_motifs(pssm, ax: axes.Axes, titles=False, yaxis=False) -> None: + """Draw a sequence-logo plot of a single cluster's PSSM. + + Args: + pssm (numpy.ndarray | pandas.DataFrame): Position-specific scoring + matrix of shape (20, 11) or (20, 9), e.g. one entry from + `ddmc.clustering.DDMC.get_pssms`. + ax: Axes to plot onto. + titles (str | bool): If given (and not `False`), used as the axes + title (with `" Motif"` appended); otherwise defaults to + `"Motif Cluster 1"`. + yaxis (Sequence[float] | bool): If given (and not `False`), a + `[ymin, ymax]` pair to set the y-axis limits to. + """ pssm = pssm.T pssm = pd.DataFrame(pssm) if pssm.shape[0] == 11: @@ -159,8 +220,26 @@ def plot_motifs(pssm, ax: axes.Axes, titles=False, yaxis=False): def plot_cluster_kinase_distances( - distances: pd.DataFrame, pssms: np.ndarray, ax, num_hits=1 -): + distances: pd.DataFrame, pssms: np.ndarray, ax: axes.Axes, num_hits: int = 1 +) -> None: + """Strip plot of kinase-vs-cluster PSSM distances, annotated with the + top predicted kinase(s) per cluster. + + For each cluster, restricts candidate kinases to those whose known + phosphoacceptor (`ddmc.motifs.KinToPhosphotypeDict`) matches that + cluster's most frequent phosphoacceptor, then annotates the closest + `num_hits` of those. + + Args: + distances: Kinase-by-cluster Frobenius distance matrix (kinases as + rows, cluster/PSSM names as columns), as returned by + `ddmc.clustering.DDMC.predict_upstream_kinases`. + pssms: The PSSMs corresponding to `distances`'s columns, of shape + (n_clusters, 20, 11), used to determine each cluster's dominant + phosphoacceptor. + ax: Axes to plot onto. + num_hits: Number of top kinase hits to annotate per cluster. + """ pssm_names = distances.columns # these centering lines make no sense, but they were used in the original @@ -212,6 +291,22 @@ def plot_cluster_kinase_distances( def get_pvals_across_clusters( label: pd.Series | np.ndarray, centers: pd.DataFrame | np.ndarray ) -> np.ndarray: + """Test whether each cluster's center differs between two groups of samples. + + Runs a Mann-Whitney U test per cluster between the samples where + `label` is True and where it's False, then corrects for multiple + testing across clusters. + + Args: + label: Boolean mask of shape (n_samples,) splitting samples into + two groups (e.g. tumor vs. NAT). + centers: Cluster centers of shape (n_samples, n_components), + aligned to `label`. + + Returns: + Multiple-testing-corrected p-value for each cluster, of shape + (n_components,). + """ pvals = [] if isinstance(centers, pd.DataFrame): centers = centers.values @@ -226,8 +321,20 @@ def plot_p_signal_across_clusters_and_binary_feature( feature: pd.Series | np.ndarray, centers: pd.DataFrame, label_name: str, - ax, + ax: axes.Axes, ) -> None: + """Violin-plot cluster centers split by a binary sample feature, with a + significance marker on each cluster whose center differs between the + two groups (see `get_pvals_across_clusters`). + + Args: + feature: Boolean mask of shape (n_samples,) splitting samples into + two groups (e.g. tumor vs. NAT), aligned to `centers`'s rows. + centers: Cluster centers of shape (n_samples, n_components), e.g. + from `ddmc.clustering.DDMC.transform(as_df=True)`. + label_name: Name to use for `feature` in the plot legend. + ax: Axes to plot onto. + """ centers = centers.copy() centers_labeled = centers.copy() centers_labeled[label_name] = feature @@ -260,12 +367,28 @@ def plot_p_signal_across_clusters_and_binary_feature( def plot_pca_on_cluster_centers( centers: pd.DataFrame, - axes, + axes: Sequence, hue_scores: Sequence | np.ndarray | None = None, hue_scores_title: str | None = None, hue_loadings: Sequence | np.ndarray | None = None, hue_loadings_title: str | None = None, -): +) -> None: + """Plot a 2-component PCA of cluster centers, as a scores plot (one + point per sample) and a loadings plot (one point per cluster). + + Args: + centers: Cluster centers of shape (n_samples, n_components), e.g. + from `ddmc.clustering.DDMC.transform(as_df=True)`. + axes: A length-2 sequence of Axes: `axes[0]` for the scores plot, + `axes[1]` for the loadings plot. + hue_scores: Per-sample values to color the scores plot points by. + hue_scores_title: If given, shown as the scores plot's legend title. + hue_loadings: Per-cluster values to color the loadings plot points + by. + hue_loadings_title: If given, adds a `"p < 0.01"`-titled legend to + the loadings plot (its entries come from `hue_loadings`, not + this string). + """ # run PCA on cluster centers pca = PCA(n_components=2) scores = pca.fit_transform(centers) # sample by PCA component diff --git a/ddmc/logistic_regression.py b/ddmc/logistic_regression.py index 0a928f64..80d0fdcc 100644 --- a/ddmc/logistic_regression.py +++ b/ddmc/logistic_regression.py @@ -1,4 +1,17 @@ -"""Logistic Regression Model functions to predict clinical features of CPTAC patients given their clustered phosphoproteomes.""" +"""Logistic Regression Model functions to predict clinical features of CPTAC patients given their clustered phosphoproteomes. + +Contains: + - `normalize_cluster_centers`: mean-centers `DDMC` cluster centers along + the patient dimension, for use as classifier features. + - `get_highest_weighted_clusters`: picks out the clusters a fitted + classifier weighted most heavily. + - `plot_cluster_regression_coefficients` / `plot_roc`: plotting helpers + for a classifier's per-cluster coefficients and its cross-validated + ROC curve. +""" + +from collections.abc import Sequence +from typing import Any import matplotlib.pyplot as plt import numpy as np @@ -13,12 +26,37 @@ from ddmc.clustering import DDMC -def normalize_cluster_centers(centers: np.ndarray): +def normalize_cluster_centers(centers: np.ndarray) -> np.ndarray: + """Mean-center cluster centers along the patient/sample dimension. + + Args: + centers: Cluster centers of shape (n_samples, n_components), e.g. + from `DDMC.transform()`. + + Returns: + `centers` with each cluster's (column's) values shifted to have + zero mean across samples, same shape as `centers`. + """ # normalize centers along along patient dimension return StandardScaler(with_std=False).fit_transform(centers.T).T -def get_highest_weighted_clusters(model: DDMC, coefficients: np.ndarray, n_clusters=3): +def get_highest_weighted_clusters( + model: DDMC, coefficients: np.ndarray, n_clusters: int = 3 +) -> list[int]: + """Pick out the (nonempty) clusters a fitted classifier weighted most heavily. + + Args: + model: The fitted `DDMC` model the classifier's features came from + (used to exclude empty clusters). + coefficients: Per-cluster classifier coefficients, e.g. + `lr.coef_`, of shape (1, n_components) or (n_components,). + n_clusters: Maximum number of top clusters to return. + + Returns: + Up to `n_clusters` nonempty cluster indices, ordered by decreasing + absolute coefficient magnitude. + """ top_clusters = np.flip(np.argsort(np.abs(coefficients.squeeze()))) top_clusters = [ cluster for cluster in top_clusters if cluster in model.get_nonempty_clusters() @@ -26,8 +64,22 @@ def get_highest_weighted_clusters(model: DDMC, coefficients: np.ndarray, n_clust return top_clusters[:n_clusters] -def plot_cluster_regression_coefficients(ax: Axes, lr, hue=None, title=False): - """Plot LR coeficients of clusters.""" +def plot_cluster_regression_coefficients( + ax: Axes, lr: Any, hue: Sequence[str] | None = None, title=False +) -> None: + """Plot LR coeficients of clusters. + + Args: + ax: Axes to plot onto. + lr: A fitted scikit-learn linear classifier exposing `coef_` of + shape (1, n_components). + hue: If given, per-cluster-run labels formatted as + `"{cluster}_{sample}"` (split on `"_"`) to group/color bars by + sample when coefficients from multiple runs are concatenated; + not used by any current figure (all call `plot_roc` with the + default `hue=None`, one bar per cluster). + title (str | bool): If given (and not `False`), set as the axes title. + """ coefs_ = pd.DataFrame(lr.coef_.T, columns=["LR Coefficient"]) if hue: coefs_["Cluster"] = [label.split("_")[0] for label in hue] @@ -52,16 +104,37 @@ def plot_cluster_regression_coefficients(ax: Axes, lr, hue=None, title=False): def plot_roc( - classifier, + classifier: Any, X: np.ndarray, y: np.ndarray | pd.Series, cv_folds: int = 4, title=False, return_mAUC: bool = False, - kfold="Stratified", + kfold: str = "Stratified", ax: Axes | None = None, -): - """Plot Receiver Operating Characteristc with cross-validation folds of a given classifier model.""" +) -> float | None: + """Plot Receiver Operating Characteristc with cross-validation folds of a given classifier model. + + Fits a fresh copy of `classifier` on each cross-validation fold, plots + the mean ROC curve (+/- 1 SEM band) across folds, and optionally + returns just the mean AUC instead of plotting. + + Args: + classifier: A scikit-learn-compatible classifier exposing `fit`. + X: Feature matrix of shape (n_samples, n_features). + y: Binary target labels of shape (n_samples,). + cv_folds: Number of cross-validation folds. + title (str | bool): If given (and not `False`), set as the axes title. + return_mAUC: If True, skip plotting and just return the mean AUC. + kfold: Cross-validation strategy: `"Stratified"` + (`StratifiedKFold`) or `"Repeated"` (`RepeatedKFold`, 10 + repeats). + ax: Axes to plot onto; defaults to the current axes (`plt.gca()`). + + Returns: + The mean AUC across folds if `return_mAUC` is True, else `None` + (the ROC curve is plotted onto `ax` instead). + """ X = np.asarray(X) y = np.asarray(y) if kfold == "Stratified": diff --git a/ddmc/motifs.py b/ddmc/motifs.py index 938aa411..194088d7 100644 --- a/ddmc/motifs.py +++ b/ddmc/motifs.py @@ -1,7 +1,28 @@ -"""Mapping to Uniprot's Proteome To Generate +/-5AA p-site Motifs.""" +"""Mapping to Uniprot's Proteome To Generate +/-5AA p-site Motifs. + +Contains: + - `get_proteome_name_to_seq`: parses a UniProt FASTA proteome into a + `{protein name: sequence}` dictionary; used by `ddmc.datasets.EBDT`. + - `get_pspls`: loads kinase specificity profiles (position-specific + peptide libraries) from `ddmc/data/PSPL/`, used by + `ddmc.clustering.DDMC.predict_upstream_kinases`. + - `compute_control_pssm`: builds a background PSSM from a set of + sequences, used by `ddmc.clustering.DDMC.get_pssms`. + - `KinToPhosphotypeDict`: maps each kinase named in the PSPL data set to + the phosphoacceptor type(s) it targets (S/T or Y). + - `match_protein_names`, `find_motif`, `make_motif`, + `generate_kinase_motifs`, `get_keys_by_value`: an older + proteome-mapping pipeline for turning MS peptide hits into + sequence motifs. Not currently called elsewhere in this package + (`ddmc.datasets.EBDT.pos_to_motif` reimplements the same idea more + simply) but kept here in case new datasets need the same matching + logic. +""" import glob import re +from collections.abc import Sequence +from typing import IO import numpy as np import pandas as pd @@ -10,8 +31,21 @@ from .binomial import AAlist -def get_proteome_name_to_seq(X, n): - """To generate proteom's dictionary""" +def get_proteome_name_to_seq(X: IO[str], n: str) -> dict[str, str]: + """Parse a UniProt FASTA proteome into a name-to-sequence dictionary. + + Args: + X: An open file handle to a UniProt FASTA file. + n: Which identifier to key the dictionary by: `"full"` for the + full human-readable protein name (parsed out of the FASTA + description between `"HUMAN "` and `" OS"`), or `"gene"` for + the gene symbol (parsed out of the `GN=` field). Records + without a `GN=` field are skipped when `n == "gene"`. + + Returns: + Dictionary mapping protein name or gene symbol to its amino acid + sequence. + """ DictProtToSeq_UP = {} for rec2 in SeqIO.parse(X, "fasta"): UP_seq = str(rec2.seq) @@ -27,8 +61,17 @@ def get_proteome_name_to_seq(X, n): return DictProtToSeq_UP -def get_keys_by_value(dictionary, value): - """Find the key of a given value within a dictionary.""" +def get_keys_by_value(dictionary: dict, value: str) -> list: + """Find every key whose value contains a given substring. + + Args: + dictionary: Dictionary to search, e.g. a protein-name-to-sequence + map from `get_proteome_name_to_seq`. + value: Substring to search for within each dictionary value. + + Returns: + The keys whose value contains `value` (empty if none match). + """ listOfKeys = list() listOfItems = dictionary.items() for item in listOfItems: @@ -37,8 +80,30 @@ def get_keys_by_value(dictionary, value): return listOfKeys -def match_protein_names(ProteomeDict, MS_names, MS_seqs): - """Match protein names of MS and Uniprot's proteome.""" +def match_protein_names( + ProteomeDict: dict[str, str], MS_names: Sequence[str], MS_seqs: Sequence[str] +) -> tuple[list[str], list[str], list[int]]: + """Match protein names of MS and Uniprot's proteome. + + For each MS peptide, first tries its given protein name directly; if + that name isn't in the proteome (or the peptide sequence doesn't occur + in that entry), falls back to searching the whole proteome for the + peptide sequence. + + Args: + ProteomeDict: Protein-name-to-sequence dictionary, as returned by + `get_proteome_name_to_seq`. + MS_names: Protein name reported for each MS peptide. + MS_seqs: The corresponding MS peptide sequence for each entry. + + Returns: + A tuple `(matchedNames, seqs, Xidx)` of the resolved protein name, + original sequence, and original index for each peptide that could + be matched to the proteome. + + Raises: + AssertionError: If any peptide could not be matched to the proteome. + """ matchedNames, seqs, Xidx = [], [], [] counter = 0 for i, MS_seq in enumerate(MS_seqs): @@ -65,9 +130,30 @@ def match_protein_names(ProteomeDict, MS_names, MS_seqs): return matchedNames, seqs, Xidx -def find_motif(MS_seq, MS_name, ProteomeDict, motif_size): +def find_motif( + MS_seq: str, MS_name: str, ProteomeDict: dict[str, str], motif_size: int +) -> tuple[str, str]: """For a given MS peptide, finds it in the ProteomeDict, and maps the +/-5 AA from the p-site, accounting - for peptides phosphorylated multiple times concurrently.""" + for peptides phosphorylated multiple times concurrently. + + Args: + MS_seq: The MS peptide sequence, with its primary phosphoacceptor + lowercased (and any additional, concurrently phosphorylated + residues also lowercased). + MS_name: The protein name to look `MS_seq` up under in + `ProteomeDict`. + ProteomeDict: Protein-name-to-sequence dictionary, as returned by + `get_proteome_name_to_seq`. + motif_size: Number of residues to include on each side of the + phosphoacceptor in the extracted motif. + + Returns: + A tuple `(pos, mappedMotif)`: + pos: The phosphosite position(s) in the full protein sequence, + formatted as `"{residue}{1-indexed position}-p"`, joined + with `";"` if there are multiple concurrent phosphosites. + mappedMotif: The extracted sequence motif (see `make_motif`). + """ MS_seqU = MS_seq.upper() try: UP_seq = ProteomeDict[MS_name] @@ -124,8 +210,25 @@ def find_motif(MS_seq, MS_name, ProteomeDict, motif_size): return pos, mappedMotif -def generate_kinase_motifs(names, seqs): - """Main function to generate motifs using 'findmotif'.""" +def generate_kinase_motifs( + names: Sequence[str], seqs: Sequence[str] +) -> tuple[list[str], list[str], list[str], list[int]]: + """Main function to generate motifs using 'findmotif'. + + Loads the bundled UniProt proteome, matches each peptide to it (via + `match_protein_names`), and extracts a sequence motif for each (via + `find_motif`). Must be run with the repository root as the working + directory (loads `./data/Sequence_analysis/proteome_uniprot2019.fa`). + + Args: + names: Protein name reported for each MS peptide. + seqs: The corresponding MS peptide sequence for each entry. + + Returns: + A tuple `(MS_names, mapped_motifs, uni_pos, Xidx)` of the resolved + protein name, extracted motif, phosphosite position, and original + index for each peptide that could be matched to the proteome. + """ motif_size = 5 proteome = open("./data/Sequence_analysis/proteome_uniprot2019.fa") ProteomeDict = get_proteome_name_to_seq(proteome, n="gene") @@ -150,8 +253,43 @@ def generate_kinase_motifs(names, seqs): return MS_names, mapped_motifs, uni_pos, Xidx -def make_motif(UP_seq, MS_seq, motif_size, ps_protein_idx, center_motif_idx, DoS_idx): - """Make a motif out of the matched sequences.""" +def make_motif( + UP_seq: str, + MS_seq: str, + motif_size: int, + ps_protein_idx: int, + center_motif_idx: int, + DoS_idx: Sequence[re.Match] | None, +) -> tuple[str, list[str]]: + """Make a motif out of the matched sequences. + + Slices `motif_size` residues on each side of the phosphosite out of the + full protein sequence (padding with `"-"` if the phosphosite is near a + sequence end), lowercases the phosphoacceptor, and lowercases any other + concurrently phosphorylated residues that fall within the motif. + + Args: + UP_seq: The full UniProt protein sequence. + MS_seq: The MS peptide sequence (used to locate concurrent + phosphosites' original characters). + motif_size: Number of residues to include on each side of the + phosphoacceptor. + ps_protein_idx: 0-indexed position of the primary phosphoacceptor + within `UP_seq`. + center_motif_idx: 0-indexed position of the primary phosphoacceptor + within `MS_seq`. + DoS_idx: Regex match objects locating any additional, concurrently + phosphorylated residues within `MS_seq` (or `None`/empty if + there are none). + + Returns: + A tuple `(motif, pidx)`: + motif: The length `2 * motif_size + 1` sequence motif, with + each phosphorylated residue lowercased. + pidx: The phosphosite position(s), formatted as + `"{residue}{1-indexed position}-p"`, for the primary site + and any concurrent site that falls within the motif. + """ UP_seq_copy = list( UP_seq[max(0, ps_protein_idx - motif_size) : ps_protein_idx + motif_size + 1] ) @@ -192,7 +330,21 @@ def make_motif(UP_seq, MS_seq, motif_size, ps_protein_idx, center_motif_idx, DoS def get_pspls() -> tuple[np.ndarray, np.ndarray]: - """Generate dictionary with kinase name-specificity profile pairs""" + """Load kinase specificity profiles (PSPLs) bundled in `ddmc/data/PSPL/`. + + Reads both the individual per-kinase CSVs in that directory and the + combined NetPhores results file (`pssm_data.csv`), log2-transforming + and clipping each into a consistent (20 amino acids x 9 positions) + specificity profile. Must be run with the repository root as the + working directory. + + Returns: + A tuple `(kinases, pspls)`: + kinases: Kinase name for each profile, of shape (n_kinases,). + pspls: Specificity profile for each kinase, of shape + (n_kinases, 20, 9), aligned to `kinases` and to `AAlist` + along the amino acid axis. + """ pspls_arr = [] kinases = [] # individual files @@ -226,8 +378,20 @@ def get_pspls() -> tuple[np.ndarray, np.ndarray]: return np.array(kinases), np.array(pspls_arr) -def compute_control_pssm(bg_sequences) -> np.ndarray: - """Generate PSSM.""" +def compute_control_pssm(bg_sequences: Sequence[str]) -> np.ndarray: + """Build a background position-specific scoring matrix (PSSM) from a set + of (typically random/background) sequences, for use as the normalizing + background in `ddmc.clustering.DDMC.get_pssms`. + + Args: + bg_sequences: Length-11 background peptide sequences, e.g. from + `ddmc.binomial.BackgroundSeqs`. + + Returns: + Array of shape (len(AAlist), 11) giving the log2 amino acid + enrichment at each position, normalized per-position across + residues. + """ back_pssm = np.zeros((len(AAlist), 11), dtype=float) for _, seq in enumerate(bg_sequences): for kk, aa in enumerate(seq): @@ -238,6 +402,10 @@ def compute_control_pssm(bg_sequences) -> np.ndarray: return np.nan_to_num(back_pssm) +# Maps each kinase named in the PSPL data (ddmc/data/PSPL/) to the +# phosphoacceptor type(s) it targets, used by +# ddmc.figures.common.plot_cluster_kinase_distances to filter kinase +# predictions to those matching a cluster's dominant phosphoacceptor. KinToPhosphotypeDict = { "ABL": "Y", "AKT": "S/T", diff --git a/ddmc/pam250.py b/ddmc/pam250.py index f8aa7e52..e99d82c6 100644 --- a/ddmc/pam250.py +++ b/ddmc/pam250.py @@ -1,26 +1,68 @@ -"""PAM250 matrix to compute sequence distance between sequences and clusters.""" +"""PAM250 sequence-distance model used by `ddmc.clustering.DDMC`. + +Contains the `PAM250` class, which scores peptide sequences against each +cluster by their average PAM250 substitution-matrix similarity to the other +sequences currently assigned to that cluster, and `get_pam250_scores`, which +precomputes the full pairwise PAM250 similarity matrix used to do so. +""" import numpy as np from Bio.Align import substitution_matrices class PAM250: + """PAM250 sequence-distance model, used by `ddmc.clustering.DDMC` when + `distance_method="PAM250"`. + + Scores each peptide sequence against a cluster by its (responsibility + weighted) average pairwise PAM250 substitution score against every + other sequence, using the fixed set of pairwise scores computed once at + construction time. + + Attributes: + background: Pairwise PAM250 similarity matrix between all input + sequences, of shape (n_seqs, n_seqs). + logWeights: Log-probability (average PAM250 score) of each sequence + under each cluster's current model, of shape + (n_seqs, n_clusters). Set to the scalar `0.0` until + `from_summaries` is first called. + """ + def __init__(self, seqs: list[str]): + """ + Args: + seqs: The length-11 peptide sequences being clustered. + """ # Compute all pairwise distances. Cast to float32 once here rather # than in from_summaries, which runs every EM iteration and would # otherwise re-convert this (potentially large) int8 matrix each time. self.background = get_pam250_scores(seqs).astype(np.float32) self.logWeights = 0.0 - def from_summaries(self, weightsIn: np.ndarray): - """Update the underlying distribution.""" + def from_summaries(self, weightsIn: np.ndarray) -> None: + """Update `self.logWeights` with each sequence's responsibility + weighted average PAM250 similarity to all sequences, per cluster. + + Args: + weightsIn: Soft cluster assignments (responsibilities) of shape + (n_seqs, n_clusters), i.e. `exp(log_resp)` from the EM E step. + """ sums = np.sum(weightsIn, axis=0) sums = np.clip(sums, 0.00001, np.inf) # Avoid empty cluster divide by 0 self.logWeights = (self.background @ weightsIn) / sums def get_pam250_scores(seqs: list[str]) -> np.ndarray: - """Calculate and store all pairwise pam250 distances before starting.""" + """Compute the full pairwise PAM250 similarity matrix between sequences. + + Args: + seqs: Sequences (all the same length) to score pairwise. + + Returns: + Symmetric array of shape (len(seqs), len(seqs)), where entry + `[i, j]` is the summed PAM250 substitution score between + `seqs[i]` and `seqs[j]` (aligned position-by-position, no gaps). + """ pam250 = substitution_matrices.load("PAM250") seq_idx = np.array( [[pam250.alphabet.find(aa) for aa in seq] for seq in seqs], diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..1f65cfb5 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,112 @@ +# DDMC: dual data and motif clustering + +DDMC clusters phosphoproteomic mass spectrometry data by jointly considering +two signals for each phosphopeptide: + +1. **Data similarity** — how similar its phosphorylation signal is across + samples/conditions to other peptides (the same objective a standard + Gaussian mixture model clusters on). +2. **Motif similarity** — how similar the amino acid sequence surrounding the + phosphosite is to other peptides in the same cluster, which acts as a + proxy for being regulated by the same upstream kinase. + +Combining both lets DDMC produce clusters that are both more robust to the +heavy missingness typical of mass-spec data and more directly interpretable +in terms of upstream kinase biology, compared to clustering on either signal +alone. + +DDMC is described in: + +> Creixell M, Meyer AS. [Dual data and motif clustering improves the modeling +> and interpretation of phosphoproteomic +> data](https://pubmed.ncbi.nlm.nih.gov/35360705/). *Cell Rep Methods*. 2022 +> Feb 28;2(2):100167. doi: +> [10.1016/j.crmeth.2022.100167](https://doi.org/10.1016/j.crmeth.2022.100167) + +**Abstract:** Cell signaling is orchestrated in part through a network of +protein kinases and phosphatases. Dysregulation of kinase signaling is +widespread in diseases such as cancer and is readily targetable through +inhibitors. Mass spectrometry-based analysis can provide a global view of +kinase regulation, but mining these data is complicated by its stochastic +coverage of the proteome, measurement of substrates rather than kinases, and +the scale of the data. Here, we implement a dual data and motif clustering +(DDMC) strategy that simultaneously clusters peptides into similarly +regulated groups based on their variation and their sequence profile. We +show that this can help to identify putative upstream kinases and supply +more robust clustering. We apply this clustering to clinical proteomic +profiling of lung cancer and identify conserved proteomic signatures of +tumorigenicity, genetic mutations, and immune infiltration. We propose that +DDMC provides a general and flexible clustering strategy for the analysis of +phosphoproteomic data. + +## How it works + +`DDMC` (in [`ddmc.clustering`][ddmc.clustering]) subclasses scikit-learn's +`sklearn.mixture.GaussianMixture` and runs the same +expectation-maximization algorithm, with two changes: + +- In the **E step**, the log-probability of each peptide belonging to each + cluster is the usual Gaussian mixture log-probability *plus* a sequence + term: `seq_weight * seq_dist.logWeights`, where `seq_dist` scores how well + a peptide's sequence matches each cluster's current motif. +- In the **M step**, in addition to the normal Gaussian mixture update + (recomputing each cluster's mean and variance across samples), the + per-cluster sequence motif is refit from the current soft cluster + assignments (responsibilities). + +Two sequence-distance methods are available (`distance_method=`): + +- `"Binomial"` (default) — for each cluster, scores how enriched each + amino acid is at each position relative to a background phosphosite + distribution, using the binomial approach of + [Schwartz & Gygi, *Nat Biotechnol* 2005](https://doi.org/10.1038/nbt1146). + See [`ddmc.binomial.Binomial`][ddmc.binomial.Binomial]. +- `"PAM250"` — scores sequences by average PAM250 substitution-matrix + similarity to the other sequences currently assigned to a cluster. See + [`ddmc.pam250.PAM250`][ddmc.pam250.PAM250]. + +The `seq_weight` argument controls the relative contribution of the sequence +term: `seq_weight=0` reduces `DDMC` to an ordinary Gaussian mixture model +over the data alone (this is a useful sanity check — see +`ddmc/tests/test_cluster.py`), while larger values weight the motif more +heavily. Missing values (common in mass-spec data due to its stochastic +proteome coverage) are handled by imputing them from the current cluster +centers between EM iterations (via `SoftImpute`), so `DDMC.fit` accepts data +with `NaN`s directly. + +Once fit, a model can be used to: + +- get cluster centers across samples (`transform`) and per-peptide cluster + assignments (`labels`) +- fill in an imputed version of the input data (`impute`) +- build a position-specific scoring matrix (PSSM) per cluster (`get_pssms`) +- compare cluster PSSMs against a library of kinase specificity profiles to + predict likely upstream kinases per cluster (`predict_upstream_kinases`) + +## Installation + +DDMC targets Python 3.12+ and is managed with [uv](https://docs.astral.sh/uv/). +Clone the repository and install the project along with its dependencies: + +```sh +git clone https://github.com/meyer-lab/DDMC.git +cd DDMC +uv sync +``` + +Run the test suite with: + +```sh +make test +``` + +## Where to go next + +- Read the [CPTAC lung cancer tutorial](tutorials/cptac_clustering.md) to see + DDMC applied to the clinical proteomics dataset used in the paper, + including handling missing values and predicting upstream kinases. +- Read the [kinase-inhibitor tutorial](tutorials/ebdt_clustering.md) to see + DDMC applied to a small, complete (no missing values) drug-perturbation + dataset. +- Browse the [API reference](reference/clustering.md) for details on every + public function and class. diff --git a/docs/reference/clustering.md b/docs/reference/clustering.md new file mode 100644 index 00000000..9fbf03ce --- /dev/null +++ b/docs/reference/clustering.md @@ -0,0 +1,9 @@ +# `ddmc.clustering` + +The core `DDMC` model and its supporting functions. + +::: ddmc.clustering + options: + members: + - DDMC + - get_pspl_pssm_distances diff --git a/docs/reference/datasets.md b/docs/reference/datasets.md new file mode 100644 index 00000000..61040848 --- /dev/null +++ b/docs/reference/datasets.md @@ -0,0 +1,12 @@ +# `ddmc.datasets` + +Loaders for the mass-spec datasets bundled with the package, plus +preprocessing helpers for filtering peptides by missingness. + +::: ddmc.datasets + options: + members: + - CPTAC + - EBDT + - filter_incomplete_peptides + - select_peptide_subset diff --git a/docs/reference/distances.md b/docs/reference/distances.md new file mode 100644 index 00000000..c1a80bea --- /dev/null +++ b/docs/reference/distances.md @@ -0,0 +1,26 @@ +# Sequence distance models + +DDMC supports two interchangeable ways of scoring how well a peptide +sequence matches a cluster's motif, selected via `DDMC(..., distance_method=...)`. + +## `ddmc.binomial` + +::: ddmc.binomial + options: + members: + - Binomial + - BackgroundSeqs + - BackgProportions + - CountPsiteTypes + - position_weight_matrix + - fast_position_weight_matrix + - frequencies + - GenerateBinarySeqID + +## `ddmc.pam250` + +::: ddmc.pam250 + options: + members: + - PAM250 + - get_pam250_scores diff --git a/docs/reference/figures_common.md b/docs/reference/figures_common.md new file mode 100644 index 00000000..f2f31bda --- /dev/null +++ b/docs/reference/figures_common.md @@ -0,0 +1,17 @@ +# `ddmc.figures.common` + +Shared plotting and figure-assembly helpers used across the paper's +`ddmc/figures/figureM*.py` reproduction scripts. + +::: ddmc.figures.common + options: + members: + - getSetup + - subplotLabel + - overlayCartoon + - genFigure + - plot_motifs + - plot_cluster_kinase_distances + - get_pvals_across_clusters + - plot_p_signal_across_clusters_and_binary_feature + - plot_pca_on_cluster_centers diff --git a/docs/reference/logistic_regression.md b/docs/reference/logistic_regression.md new file mode 100644 index 00000000..dadf96cd --- /dev/null +++ b/docs/reference/logistic_regression.md @@ -0,0 +1,13 @@ +# `ddmc.logistic_regression` + +Helpers for using DDMC cluster centers as features in a logistic regression +classifier, to predict clinical/genetic features of CPTAC patients (e.g. +mutation status, tumor vs. NAT, hot/cold immune infiltration). + +::: ddmc.logistic_regression + options: + members: + - normalize_cluster_centers + - get_highest_weighted_clusters + - plot_cluster_regression_coefficients + - plot_roc diff --git a/docs/reference/motifs.md b/docs/reference/motifs.md new file mode 100644 index 00000000..c1f465f4 --- /dev/null +++ b/docs/reference/motifs.md @@ -0,0 +1,16 @@ +# `ddmc.motifs` + +Helpers for mapping peptides to their surrounding sequence motif via a +reference proteome, and for loading kinase specificity profiles (PSPLs) +used by `DDMC.predict_upstream_kinases`. + +::: ddmc.motifs + options: + members: + - get_proteome_name_to_seq + - match_protein_names + - find_motif + - generate_kinase_motifs + - get_pspls + - compute_control_pssm + - KinToPhosphotypeDict diff --git a/docs/tutorials/cptac_clustering.md b/docs/tutorials/cptac_clustering.md new file mode 100644 index 00000000..09a37a6b --- /dev/null +++ b/docs/tutorials/cptac_clustering.md @@ -0,0 +1,166 @@ +# Tutorial: clustering the CPTAC lung cancer dataset + +This tutorial reproduces the core workflow from the DDMC paper: clustering a +clinical phosphoproteomics dataset with heavy missingness, imputing missing +values from the fit, generating cluster motifs, and predicting the upstream +kinases likely responsible for each cluster. + +The dataset is the CPTAC lung squamous cell/adenocarcinoma phosphoproteomics +cohort bundled with the package (`ddmc/data/MS/CPTAC/`), pairing each tumor +sample with its adjacent normal (NAT) sample where available. + +!!! note + Some of the functions used here (`predict_upstream_kinases`, and the + background phosphosite set used by the `"Binomial"` distance) read data + files using paths relative to the repository root. Run this tutorial + with the repository root as your working directory, e.g. from a script + or notebook launched with `uv run`. + +## 1. Load and filter the data + +`CPTAC.get_p_signal` returns a DataFrame of phosphorylation signal indexed +by the length-11 peptide sequence (5 residues flanking the phosphosite on +each side, phosphoacceptor lowercased), with one column per sample. It's +built from TMT experiments, so `min_experiments` drops peptides seen in +fewer than that many experiments: + +```python +from ddmc.datasets import CPTAC, filter_incomplete_peptides + +cptac = CPTAC() +p_signal = cptac.get_p_signal(min_experiments=6) +print(p_signal.shape) # (peptides, samples) +``` + +Mass spec data is never fully complete, but `DDMC.fit` can already handle +`NaN`s directly (see [How it works](../index.md#how-it-works)). For this +tutorial we additionally drop peptides that are missing in more than 10% of +samples, both to speed up fitting and to keep the imputation step in +step 3 meaningful: + +```python +p_signal = filter_incomplete_peptides(p_signal, sample_presence_ratio=0.9) +print(p_signal.shape) +``` + +## 2. Fit DDMC + +`DDMC` takes the number of clusters (`n_components`) and how strongly to +weight sequence motif similarity relative to data similarity (`seq_weight`). +There's no universally correct choice of either — the +[paper](../index.md) explores this via imputation accuracy (see +`ddmc/figures/figureM2.py`) — but a moderate cluster count and weight work +well as a starting point: + +```python +from ddmc.clustering import DDMC + +model = DDMC( + n_components=20, + seq_weight=100, + distance_method="Binomial", + random_state=0, +).fit(p_signal) +``` + +Once fit, `transform` gives the cluster centers (mean phosphorylation signal +per sample, per cluster) and `labels` gives each peptide's assigned cluster: + +```python +centers = model.transform(as_df=True) # samples x clusters +print(centers.shape) + +labels = model.labels() # one cluster index per peptide, aligned to p_signal.index +print(pd.Series(labels).value_counts().head()) +``` + +## 3. Impute missing values + +Because DDMC already estimates a center for every cluster on every sample, +it can fill in a peptide's missing samples using its cluster's center. This +is the imputation approach benchmarked in the paper against +mean/zero/PCA imputation: + +```python +imputed = model.impute() +assert imputed.isna().sum().sum() == 0 +``` + +## 4. Build cluster motifs (PSSMs) + +`get_pssms` computes a position-specific scoring matrix per cluster, +describing which amino acids are enriched at each position relative to a +background distribution of phosphosites: + +```python +cluster_names, pssms = model.get_pssms(PsP_background=True) +print(pssms.shape) # (n_nonempty_clusters, 20 amino acids, 11 positions) +``` + +You can visualize a cluster's motif as a sequence logo with the plotting +helper used throughout `ddmc/figures/`: + +```python +import matplotlib.pyplot as plt +from ddmc.figures.common import plot_motifs + +fig, ax = plt.subplots(figsize=(4, 2)) +plot_motifs(pssms[0], ax=ax, titles=f"Cluster {cluster_names[0]}") +fig.savefig("cluster_0_motif.svg") +``` + +## 5. Predict upstream kinases + +Comparing each cluster's PSSM to a library of experimentally derived kinase +specificity profiles (position-specific peptide libraries, PSPLs) suggests +which kinase(s) are most likely responsible for phosphorylating peptides in +that cluster: + +```python +kinase_distances = model.predict_upstream_kinases(PsP_background=True) +print(kinase_distances.shape) # kinases x clusters + +# Smaller Frobenius distance = better match; show the top hit per cluster. +print(kinase_distances.idxmin(axis=0)) +``` + +## 6. Relate clusters to a clinical feature + +Cluster centers can be used as compact per-patient features. As an example, +compare cluster signal between tumor samples with and without an EGFR +mutation, using the mutation calls bundled with the CPTAC dataset: + +```python +import numpy as np + +mutations = cptac.get_mutations(["EGFR.mutation.status"]) + +# Restrict to tumor samples (no ".N" suffix) with a known mutation call. +tumor_cols = [c for c in p_signal.columns if not c.endswith(".N") and c in mutations.index] +egfr_mutant = mutations.loc[tumor_cols, "EGFR.mutation.status"].to_numpy() + +centers_tumor = centers.loc[tumor_cols] +from scipy.stats import mannwhitneyu + +for cluster in centers_tumor.columns: + values = centers_tumor[cluster].to_numpy() + _, pval = mannwhitneyu(values[egfr_mutant], values[~egfr_mutant]) + if pval < 0.05: + print(f"Cluster {cluster}: p={pval:.3g}") +``` + +This is the same pattern (Mann-Whitney U tests across clusters, then +multiple-testing correction) used by `get_pvals_across_clusters` in +`ddmc/figures/common.py` and the logistic-regression classifiers in +[`ddmc.logistic_regression`](../reference/logistic_regression.md), which use +cluster centers to predict mutation status, tumor-vs-NAT, and hot/cold +immune infiltration across the whole cohort. + +## Next steps + +- Try `distance_method="PAM250"` and compare the resulting clusters. +- Sweep `seq_weight` from `0` (pure Gaussian mixture) upward and see how + cluster motifs sharpen — `ddmc/figures/figureM2.py` and `figureM3.py` do + this systematically via imputation error. +- See the [kinase-inhibitor tutorial](ebdt_clustering.md) for an example + with a smaller, fully observed dataset. diff --git a/docs/tutorials/ebdt_clustering.md b/docs/tutorials/ebdt_clustering.md new file mode 100644 index 00000000..dbc8a420 --- /dev/null +++ b/docs/tutorials/ebdt_clustering.md @@ -0,0 +1,98 @@ +# Tutorial: clustering a kinase-inhibitor perturbation dataset + +The CPTAC tutorial works with a large, heavily-missing clinical dataset. This +tutorial instead uses the small, fully observed MCF7 kinase-inhibitor +dataset from [Hijazi et al., *Nat Biotechnol* 2020](https://www.nature.com/articles/s41587-019-0391-9) +(bundled as `ddmc/data/Validations/Computational/ebdt_mcf7.csv`), which is +convenient for quickly comparing clustering settings since there's no +missing-value handling to think about. + +Each column is the fold-change in phosphorylation signal for the MCF7 breast +cancer cell line treated with a given kinase inhibitor, relative to control; +each row is a phosphopeptide. + +## 1. Load the data + +`EBDT.get_p_signal` maps the dataset's peptide identifiers onto the human +proteome to build the same length-11 sequence representation +(`ddmc/motifs.py`) that `CPTAC.get_p_signal` uses, so the resulting +DataFrame is a drop-in match for `DDMC.fit`: + +```python +from ddmc.datasets import EBDT + +p_signal = EBDT().get_p_signal() +print(p_signal.shape) # (peptides, inhibitors) +print(p_signal.isna().sum().sum()) # 0 — no missing values in this dataset +``` + +## 2. Fit with both distance methods + +Because this dataset is small, it's cheap to compare DDMC's two sequence +distance methods directly: + +```python +from ddmc.clustering import DDMC + +model_binomial = DDMC( + n_components=15, seq_weight=50, distance_method="Binomial", random_state=0 +).fit(p_signal) + +model_pam250 = DDMC( + n_components=15, seq_weight=50, distance_method="PAM250", random_state=0 +).fit(p_signal) + +print(model_binomial.score(), model_pam250.score()) +``` + +`seq_weight=0` disables the sequence term entirely, making `DDMC` equivalent +to a plain `sklearn.mixture.GaussianMixture` fit on `p_signal.values` — a +useful sanity check when tuning `seq_weight` upward from zero (this is +exactly what `ddmc/tests/test_cluster.py::test_wins` checks). + +## 3. Inspect which inhibitors distinguish each cluster + +Since columns here are inhibitors rather than patient samples, cluster +centers directly show which inhibitors most shift each cluster's +phosphorylation: + +```python +centers = model_binomial.transform(as_df=True) # inhibitors x clusters + +# Inhibitors with the strongest (most negative/positive) effect on cluster 0 +print(centers[0].sort_values().head()) +print(centers[0].sort_values().tail()) +``` + +For example, clusters most suppressed by PI3K/AKT inhibitors (columns like +`MCF7.GDC0941.fold`, `MCF7.MK2206.fold`) are candidates for being downstream +of PI3K/AKT signaling — which `predict_upstream_kinases` (see the +[CPTAC tutorial](cptac_clustering.md#5-predict-upstream-kinases)) can help +confirm from the sequence motif side, independent of the perturbation data. + +## 4. Compare cluster assignments between the two distance methods + +Since both models were fit with the same `n_components` and `random_state`, +you can directly compare how much sequence information changes cluster +membership: + +```python +import numpy as np + +agreement = np.mean(model_binomial.labels() == model_pam250.labels()) +print(f"Fraction of peptides with the same cluster label: {agreement:.2f}") +``` + +Exact label agreement isn't expected to be high — cluster *indices* aren't +aligned between independent fits — but comparing the resulting motifs +(`get_pssms`) or projecting both sets of centers with +`plot_pca_on_cluster_centers` from `ddmc/figures/common.py` is a more +meaningful way to compare the two distance methods' structure. + +## Next steps + +- Increase `seq_weight` and watch clusters become more homogeneous in + sequence motif at the cost of separation in the inhibitor-response data — + the same tradeoff explored on CPTAC data in `ddmc/figures/figureM2.py`. +- Combine this with `predict_upstream_kinases` to check whether a cluster's + predicted kinase matches the inhibitor(s) that most affect it. diff --git a/makefile b/makefile index 0c7299d4..7d7d8fa0 100644 --- a/makefile +++ b/makefile @@ -25,3 +25,9 @@ lint: typecheck: uv run ty check ddmc + +docs: + uv run mkdocs serve + +docs-build: + uv run mkdocs build --strict diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..1dc99139 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,61 @@ +site_name: DDMC +site_description: Dual data and motif clustering for phosphoproteomic data +repo_url: https://github.com/meyer-lab/DDMC +repo_name: meyer-lab/DDMC +edit_uri: edit/main/docs/ + +theme: + name: material + palette: + - media: "(prefers-color-scheme: light)" + scheme: default + primary: indigo + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: indigo + toggle: + icon: material/brightness-4 + name: Switch to light mode + features: + - navigation.sections + - navigation.top + - content.code.copy + - content.action.edit + +nav: + - Home: index.md + - Tutorials: + - Clustering CPTAC lung cancer data: tutorials/cptac_clustering.md + - Clustering a kinase-inhibitor dataset: tutorials/ebdt_clustering.md + - API reference: + - DDMC: reference/clustering.md + - Datasets: reference/datasets.md + - Motifs: reference/motifs.md + - Sequence distances: reference/distances.md + - Logistic regression: reference/logistic_regression.md + - Figure plotting helpers: reference/figures_common.md + +markdown_extensions: + - admonition + - pymdownx.details + - pymdownx.superfences + - pymdownx.highlight + - tables + - toc: + permalink: true + +plugins: + - search + - mkdocstrings: + handlers: + python: + options: + docstring_style: google + show_source: true + show_root_heading: true + merge_init_into_class: true + show_signature_annotations: true + separate_signature: true diff --git a/pyproject.toml b/pyproject.toml index 93069d1a..14405d61 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,11 @@ dev = [ "ruff>=0.15", "ty>=0.0.1a0", ] +docs = [ + "mkdocs>=1.6.1", + "mkdocs-material>=9.7.7", + "mkdocstrings[python]>=1.0.6", +] [build-system] requires = ["hatchling"] diff --git a/uv.lock b/uv.lock index bbce34c5..b4163249 100644 --- a/uv.lock +++ b/uv.lock @@ -37,6 +37,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, ] +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "backrefs" +version = "8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/56/4744bcd0c82184e80c52b0ac4076c261a8ffa1f1b343ff2f6e89ce0e1cef/backrefs-8.0.tar.gz", hash = "sha256:b556cd7d36c3a3a2f256b89590b176b8eddfb73bcfaee3a3ddd84ea66d21ce50", size = 7013081, upload-time = "2026-07-26T19:54:24.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/fd/9bf53b6a6f6f519ffaac765df2f2a25e5c2fc6d32cfd2b2747099e72c911/backrefs-8.0-py310-none-any.whl", hash = "sha256:4a627b817fd2dce43b79ab48da63613340509381cd8ce0897078a0bce79a2ab8", size = 380377, upload-time = "2026-07-26T19:54:17.457Z" }, + { url = "https://files.pythonhosted.org/packages/e1/29/4bd7ae72a2634da00379c2b3bcc5439e7c94620235c6afea8af15229a973/backrefs-8.0-py311-none-any.whl", hash = "sha256:f0c35cf0102ba6b6070c12a492be3c1c1d3f5839529784b9a9565d6d04569a01", size = 392169, upload-time = "2026-07-26T19:54:18.782Z" }, + { url = "https://files.pythonhosted.org/packages/29/13/232505664e8e2a0c7a2eb0c505cfade9d715538f89a5d62bc4c272968f62/backrefs-8.0-py312-none-any.whl", hash = "sha256:87f0fae8c5f207fe9f4b2887efc71d42f4900ac78faa1af08d675ef303692dc5", size = 398084, upload-time = "2026-07-26T19:54:19.954Z" }, + { url = "https://files.pythonhosted.org/packages/8a/69/47a3dc20abc4fa5486655fde681bd55e63211b46c886d8c02223d6468431/backrefs-8.0-py313-none-any.whl", hash = "sha256:601ce68ca12385dbda06ce264406b4c4210cf5b79fd0fd627592365c92f29a88", size = 400040, upload-time = "2026-07-26T19:54:21.194Z" }, + { url = "https://files.pythonhosted.org/packages/1c/cf/e5f9b68a5b0e939a2fb933a66c20180d0c9241bf8927f7a47fa48c1675e9/backrefs-8.0-py314-none-any.whl", hash = "sha256:9ec96efa080938be92323e8e730e57718c9c88eb15ad70bbef4e1766df591408", size = 411903, upload-time = "2026-07-26T19:54:23.221Z" }, +] + [[package]] name = "bioinfokit" version = "2.1.4" @@ -203,6 +225,137 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360, upload-time = "2026-08-03T21:21:13.636Z" }, ] +[[package]] +name = "charset-normalizer" +version = "3.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/3f/143b048436775b0f76ac3eec145c019e8173ccc2885c8f20319b996d5e83/charset_normalizer-3.5.1.tar.gz", hash = "sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3", size = 171764, upload-time = "2026-08-15T08:20:44.807Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/27/78873dc8b6a56357517b74b6bb9568b80450e7bb4f6ef7e3fa9d22aa0bd7/charset_normalizer-3.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5b6d1386bf0096d26d3a863dc0a487a5b4eb9aa93cf5ba69683d29dde6b9d60f", size = 344456, upload-time = "2026-08-15T08:17:10.072Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4c/be49ada26b1f0232d57aa89bbebf997a5cc2332a5616b6eca26ff680044d/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4582c27e8c889d64811987b5967fbd3ae0c823fe1fd933b543d55ac20bb475fa", size = 238530, upload-time = "2026-08-15T08:17:11.563Z" }, + { url = "https://files.pythonhosted.org/packages/76/84/6f1290fa07ae6978d3960caa3eb1b8019bf9284ab7c2297b00c099ef4250/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d1c7a53a6c2103925cdd6d7229f8c567379f211c869793df679f2e9f738c369", size = 230200, upload-time = "2026-08-15T08:17:12.919Z" }, + { url = "https://files.pythonhosted.org/packages/e7/a0/47b18adeed31c8f16ba9700f32c1b18594cfa09f47eb672a488c273c22bf/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e6621fb2a4988d6e53eedc455e5903e2679f3967b8acb3d639f1b63c14a2e893", size = 262222, upload-time = "2026-08-15T08:17:14.571Z" }, + { url = "https://files.pythonhosted.org/packages/38/fe/341861ac118dae06f3ec0eb487488af52128f2ef2faf0b11003944d22259/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c0c10730342b0c9b35dd1d619beb8214e520bd96a1f870f452680b238aab3e0", size = 258951, upload-time = "2026-08-15T08:17:16.158Z" }, + { url = "https://files.pythonhosted.org/packages/6f/89/bb5108dc6c3651dca963f2b0a3ba19bbcb370c94e1b6d3e0e844a58e6dca/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08", size = 248801, upload-time = "2026-08-15T08:17:17.683Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ba/ef83ae3aca816393decfa3530976f38a79812d707b80b580ac33b83f9877/charset_normalizer-3.5.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9f8405c2c758532c74fed975dbee57be1f31a6e865c031870c79a6ed3212ada", size = 244070, upload-time = "2026-08-15T08:17:19.191Z" }, + { url = "https://files.pythonhosted.org/packages/f6/0b/c5292a2462d69b7378ea89793bbb5b2b6fcf6f7dd6d1667f9619094ad553/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:96fef3e886d6a9874b14f27fc193fbdc69d5d8035783d86aa4e1cea594e695f9", size = 240110, upload-time = "2026-08-15T08:17:20.547Z" }, + { url = "https://files.pythonhosted.org/packages/46/22/111e5be3b740d5c2a5bfcedb3d237b6591e5c2e82ae9d6ffcb121fe0909c/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5d8531a6569d025f68e2321e7638fb7978f23db58e5f69f56913837aae03816e", size = 232836, upload-time = "2026-08-15T08:17:21.895Z" }, + { url = "https://files.pythonhosted.org/packages/f9/d2/d2aad6fe0dbb44b194bf3becb60f5a0ac48446ade999a47fe7bb41eb09a7/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aae2ee51122d3ae968a3837d97dc24a0aeebb0dea23694422cd172bd30017cd6", size = 262712, upload-time = "2026-08-15T08:17:23.727Z" }, + { url = "https://files.pythonhosted.org/packages/35/5a/337e4663a5eae6de99db940ee8066d4145caafb61327db62deda15313cce/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7235dc28fc6dd9d832ac7c7bce95367dedb85929f17368a0c2bee1e080b9acbf", size = 242977, upload-time = "2026-08-15T08:17:25.157Z" }, + { url = "https://files.pythonhosted.org/packages/ca/85/f82f8a92e31c7519410e2e1afdc630f28ec47490ce2c09a11c1a43cbb459/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4abdc5f9ad448c1ecbfae2974b820535d6bc6e7eef63babbab3d81cf46968c71", size = 260207, upload-time = "2026-08-15T08:17:26.602Z" }, + { url = "https://files.pythonhosted.org/packages/b7/52/643d11ffd60e9ac2fd1fb87e167a19285b9eefeff4a40e63c87cbfbeab36/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba501e667c17d8411f98e67a022d9604ef179aff0e459b7e292c796837c13573", size = 250562, upload-time = "2026-08-15T08:17:27.971Z" }, + { url = "https://files.pythonhosted.org/packages/62/16/46556278c2168d12df9da7fede5dc6fc70e60301b26a82bbeec238c9cfe3/charset_normalizer-3.5.1-cp312-cp312-win32.whl", hash = "sha256:cfa1c0cc3a8f9f53f1243a5a99ac36fd003880199383b37672e86ddda9cb07e2", size = 178507, upload-time = "2026-08-15T08:17:29.277Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7a/4c6c298171e6b3e745633180ff59350fc0ca0db1ffd28df1e369e0579f71/charset_normalizer-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3617ac3cfd8b9888f145ad89dd6e692285834b0201c6074a5eeaad3fd4d668c2", size = 200551, upload-time = "2026-08-15T08:17:30.668Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d7/eb95a042f0dd22e304b0b6472b154f3546a1a039a9ee89ccb2a7f61591fc/charset_normalizer-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:88e85ab89cb822c1e635f51d6d32e488f94e002e70e2f492bdb8b945543f345a", size = 180700, upload-time = "2026-08-15T08:17:32.028Z" }, + { url = "https://files.pythonhosted.org/packages/bc/61/2cb6ad133dbbb449fa2d37ccae973232f4827e799af258d15e589a3d1e9e/charset_normalizer-3.5.1-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:4f298bdadb8f0b9e5672877f647d1be9373ef5320c9e2f049795e26cad28b6a9", size = 211584, upload-time = "2026-08-15T08:17:33.597Z" }, + { url = "https://files.pythonhosted.org/packages/18/57/a305c968be1ca13f3dd1b32f445877e97addf55d80b65c7cb35fac82b777/charset_normalizer-3.5.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:88ca277405c2d3b71c4e1c2ee0e7966e807bcba86a69d11e19ba199d18ae4491", size = 223359, upload-time = "2026-08-15T08:17:35.022Z" }, + { url = "https://files.pythonhosted.org/packages/09/0a/d3646670292ce8d8f8cc11ac067d44885e697a5591f57a9221128da5e7b3/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9362dd90aa7dab48c0054a21187791ccf05473f7dba5d92b8033ae62164675e7", size = 194464, upload-time = "2026-08-15T08:17:36.452Z" }, + { url = "https://files.pythonhosted.org/packages/de/93/d51ec556e01042fed6f993ea859311bc7917b466684182fbbceb6ca24762/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:977cdbd483a9cff38179bea4fd754289a6f2195c7abd414aba85410b3e66cc5e", size = 197676, upload-time = "2026-08-15T08:17:37.819Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a0/562247944386f7d4ef94467e84876600cc1e0f1b93239aaa9213d2bc3cbd/charset_normalizer-3.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e90251c0c7bdd54a100a0dce3c07b7e637278c93af29dbf78ebb89a58c4bac7d", size = 340473, upload-time = "2026-08-15T08:17:39.303Z" }, + { url = "https://files.pythonhosted.org/packages/31/e7/1d994be1b93d41e9502b8b0460eaa88a1dd8df335df415db87d6c3e91ab2/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d78ecec2605a8d0398b0f365d5f12a63248438516f5dac536a5eff7337df4a", size = 240156, upload-time = "2026-08-15T08:17:40.66Z" }, + { url = "https://files.pythonhosted.org/packages/09/53/27923ce5cc6cbccb832037b27dca98882d9c53e9b69e866bbbef4aae7fc8/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d59b75732e9b6f27388e10c14b0259cc5f2e48c78627d185e6a177b58ad3cffe", size = 228246, upload-time = "2026-08-15T08:17:42.003Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/5a97e84d63af1d55c07439cb80e56d99a8efb4295700eb4e18c0d1615d2c/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d929fc574b4d6fd9e7c0f5c2ede8716a41911923aa7fa5fce38e0818aa4a1ac", size = 263660, upload-time = "2026-08-15T08:17:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c2/071575791dcc88316c0a9a65ce38897a82e4cfe4a325f0f7fe1b1ac47bcf/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:394fea06235c8543390050ed5f529187074b029fb027213f6c46ac11ab5d950e", size = 260354, upload-time = "2026-08-15T08:17:45.094Z" }, + { url = "https://files.pythonhosted.org/packages/fb/af/63240b0c0248c075c2535a1f1bd992821d8251b9f173abc13329661d09e4/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62b55f6722735a6c472f88361cde6640608773d9443cebdbb51abf436a1fcdd3", size = 250638, upload-time = "2026-08-15T08:17:46.496Z" }, + { url = "https://files.pythonhosted.org/packages/4d/66/70dfad64f15be09c15ccfee81330a7e515895dbe296dd23114e9a231268a/charset_normalizer-3.5.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa48b1b63d639f9483e0633e092f5851e2348c352f1f9bb6c8182f87884ef876", size = 244583, upload-time = "2026-08-15T08:17:47.963Z" }, + { url = "https://files.pythonhosted.org/packages/c0/24/ef36367d38b9ddd4bccbf72888c342e8de1f5ae506fa0b2dcf970e2732a1/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c71fb0d56c920c269cd3e2e3fe7c610e3f1fdb21a6ce60efa6430ff63676cea6", size = 242038, upload-time = "2026-08-15T08:17:49.481Z" }, + { url = "https://files.pythonhosted.org/packages/db/ab/55e683ba0fff2e43adafc10daa3001eac90fdaa419a97227d5a7067eedde/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:485a0d363cafefcd2538a73c7c838daa2035f09b2c9f9b5e3133f80c6aeb84c2", size = 233677, upload-time = "2026-08-15T08:17:50.845Z" }, + { url = "https://files.pythonhosted.org/packages/bd/67/0f40eaf8d1b6e7cf15e82382a2965efaca787fc1c2794b7021d37aaf5036/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0ea61a470e070686aa30892fed79e297d2c8d0ab46b8bcdf027d38c51da591", size = 264491, upload-time = "2026-08-15T08:17:52.61Z" }, + { url = "https://files.pythonhosted.org/packages/5c/64/12b4c2a11ee8df4fcc518c78b0d93e3a92bd3d5253d1617ce74ff0e8c7ef/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90b7481fb62fbe172c558bc6fd1c4c98d82004a54a7551f20e11ac9bf0b8708c", size = 245196, upload-time = "2026-08-15T08:17:54.023Z" }, + { url = "https://files.pythonhosted.org/packages/37/2e/651d910af6d0fba325eee1cda37ec5443462ed25360e666c144166eb6091/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:35fe081843b35aad20ffeccec3eeffbe637b15d14f3fb22cc1b59cd8ec17e93c", size = 261660, upload-time = "2026-08-15T08:17:55.491Z" }, + { url = "https://files.pythonhosted.org/packages/90/c6/b09e05e6db7f64338e0dc067c79577b1138da86c1e38369096851d96be88/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd0350afdc3aabd5576f60ea109228bd5538139713c7b094c5cd27c73a98bc6f", size = 252618, upload-time = "2026-08-15T08:17:57.025Z" }, + { url = "https://files.pythonhosted.org/packages/76/4e/362d4f9fdcdf5556fb2aa3ce7d4a58ebce03ed1ff03aa1d9aca8d02f13f3/charset_normalizer-3.5.1-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:9d9a0dc7cbe9bec24c3f767c9122c41fe5a1bc43f47cd099d00d393e09769de4", size = 140362, upload-time = "2026-08-15T08:17:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d4/703be739b26acce318bd29eb3b25b7209e1b1f527f9eae3d1f1f01fdde2b/charset_normalizer-3.5.1-cp313-cp313-win32.whl", hash = "sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3", size = 177755, upload-time = "2026-08-15T08:18:00.037Z" }, + { url = "https://files.pythonhosted.org/packages/8a/33/56d97ade41c8db611e727168c52ae46c9224c362ec28d4b65d7e9869e8da/charset_normalizer-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6", size = 199295, upload-time = "2026-08-15T08:18:01.506Z" }, + { url = "https://files.pythonhosted.org/packages/5b/75/5b20dd1e6573a01a08158fe104104fa2c8abf941745596954185726cd46c/charset_normalizer-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0", size = 179856, upload-time = "2026-08-15T08:18:02.929Z" }, + { url = "https://files.pythonhosted.org/packages/29/cd/2b812ce5e888f1ce69a5350281e58aab07ae64a958ecae8912f30865718e/charset_normalizer-3.5.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8", size = 212318, upload-time = "2026-08-15T08:18:04.403Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4a/a6ee107430768a5334e6d63f31f148a04a1a491ef161a1ac9415a73f2fa8/charset_normalizer-3.5.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102", size = 224897, upload-time = "2026-08-15T08:18:05.997Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d9/35ae3f64f29d0179c35c3baefe575904df2913dde519129c7f75995a2b1d/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5", size = 194848, upload-time = "2026-08-15T08:18:07.397Z" }, + { url = "https://files.pythonhosted.org/packages/74/76/f2fc7380f056cc273a53af37f50d08ad54b2c59f61078f31432edcf1c2bd/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3", size = 198163, upload-time = "2026-08-15T08:18:08.989Z" }, + { url = "https://files.pythonhosted.org/packages/e9/40/095ce62fa078483cccc1fa2b36e6bc9580b85422a20ee9f925341c50e44f/charset_normalizer-3.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c", size = 341823, upload-time = "2026-08-15T08:18:10.458Z" }, + { url = "https://files.pythonhosted.org/packages/f1/5a/0e58b1c04a1596e0256f407274a92d5fb2ee21324409d1fab1da48a65b5b/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0", size = 242458, upload-time = "2026-08-15T08:18:11.989Z" }, + { url = "https://files.pythonhosted.org/packages/22/95/b4618ce912e6db0b1aae89ba788e38e8a7eba0f3025cc66e8c0699f977b2/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96", size = 226717, upload-time = "2026-08-15T08:18:13.401Z" }, + { url = "https://files.pythonhosted.org/packages/8a/76/c681192bbda3d55356db5dadd64381d5202b37c6b598fcda5282e88b5d3d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc", size = 266111, upload-time = "2026-08-15T08:18:14.961Z" }, + { url = "https://files.pythonhosted.org/packages/88/be/55127bfca72c0cff6c022488d140d7c5b04c771e3b72e9bdb4836d54979d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f", size = 263128, upload-time = "2026-08-15T08:18:16.515Z" }, + { url = "https://files.pythonhosted.org/packages/e0/91/39c3af510b0aa32bbda03374259200f28430febfd1bf5e511fe765282ce5/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90", size = 251240, upload-time = "2026-08-15T08:18:18.127Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a5/cbe418bbc6ecdfc3e05a0116002897c4b403a5e838d697e64c78e9f0190d/charset_normalizer-3.5.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506", size = 245282, upload-time = "2026-08-15T08:18:19.625Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a4/689bb42e8e7cd492f3cb64907c6bc00ad247ec9a3628cd3f8eed126e8ae1/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5", size = 244597, upload-time = "2026-08-15T08:18:21.121Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ce/9962938e179cf9f699d3f1e7b3114b5d7642dee6a893745229f9dd04f274/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e", size = 231376, upload-time = "2026-08-15T08:18:22.57Z" }, + { url = "https://files.pythonhosted.org/packages/85/54/46000450ada53bd9eac5429a2c8c54cd2d9b39c0c255f229aea9af0948a5/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5", size = 266715, upload-time = "2026-08-15T08:18:24.235Z" }, + { url = "https://files.pythonhosted.org/packages/3d/bb/618749d70f792b44252a777bf89bfb86823b9bbc1ea13fe8ce759b07f38a/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3", size = 245848, upload-time = "2026-08-15T08:18:25.726Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3f/ffb64458527c7668031d5eb095d978de561958dc9f5b53f8e488a533e603/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3", size = 264521, upload-time = "2026-08-15T08:18:27.193Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ab/74a55fd803916a35ac461daf002708191aac19b546b80dc8cabfedc63d98/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36", size = 253054, upload-time = "2026-08-15T08:18:28.568Z" }, + { url = "https://files.pythonhosted.org/packages/a0/2a/6a9034b7d3c60b17499afb482df5878bf9fa20b50cc3887d5ef017a833db/charset_normalizer-3.5.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7", size = 140580, upload-time = "2026-08-15T08:18:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/f3/46/1d362e1a00d035d66b9869e1281eee115907f7e390a16a07824ab5737360/charset_normalizer-3.5.1-cp314-cp314-win32.whl", hash = "sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b", size = 180325, upload-time = "2026-08-15T08:18:31.877Z" }, + { url = "https://files.pythonhosted.org/packages/7a/7c/4938c329b6a9d446f6a59aa2092ff7118f274209b5ed0e26893d1d30a63c/charset_normalizer-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b", size = 204175, upload-time = "2026-08-15T08:18:33.466Z" }, + { url = "https://files.pythonhosted.org/packages/ac/33/eeb384dbd8dec570661354592f4f2e1b2fcc92585624d146a000caf53841/charset_normalizer-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687", size = 184123, upload-time = "2026-08-15T08:18:34.913Z" }, + { url = "https://files.pythonhosted.org/packages/1c/6c/c73fa9d5a85f6ab05395de61c5f6984e0a9ff40bb5ff888d46dff02526c6/charset_normalizer-3.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348", size = 381682, upload-time = "2026-08-15T08:18:36.349Z" }, + { url = "https://files.pythonhosted.org/packages/30/c7/63565f860921457feba93bae6c86fb7746deb4cffeed2f375cb845318146/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef", size = 240826, upload-time = "2026-08-15T08:18:37.887Z" }, + { url = "https://files.pythonhosted.org/packages/06/ae/7ae8807410dfa33f8e6f1715740adeaafa8a816cc4cb33508f54b1f7c896/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885", size = 227861, upload-time = "2026-08-15T08:18:39.315Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a3/887c1642f0da26000b0e0652d91071113c0e72cea33952e225cf589f49a9/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375", size = 260758, upload-time = "2026-08-15T08:18:40.88Z" }, + { url = "https://files.pythonhosted.org/packages/3e/11/e6f5b9a3d0e55b0ef7505cd3765cdd48f22db89994c947b316f52f801fd8/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1", size = 259950, upload-time = "2026-08-15T08:18:42.351Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ee/e4e10a94d51cd1ee638aa7e00b65399e6b2a4e8376ab6d2eac9f95586671/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65", size = 249329, upload-time = "2026-08-15T08:18:43.914Z" }, + { url = "https://files.pythonhosted.org/packages/c4/25/d5f4198819e6059735a84e8d0bfb72dc33976da67b97adcd3fb5a5e07ec6/charset_normalizer-3.5.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5", size = 243137, upload-time = "2026-08-15T08:18:45.368Z" }, + { url = "https://files.pythonhosted.org/packages/a5/e9/e925ca7569cf9fb9701fd82503fee73eea5268fdb856bdd64947092d3daa/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af", size = 242820, upload-time = "2026-08-15T08:18:46.842Z" }, + { url = "https://files.pythonhosted.org/packages/34/17/672c251a888ed2aebcdd2fe830ad0104e25ff83c43f5c4f9c15e9fc6853c/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1", size = 230504, upload-time = "2026-08-15T08:18:48.353Z" }, + { url = "https://files.pythonhosted.org/packages/3f/fc/f6a85abebd42ce4da2f1db0aa56cc6a0df1995e318b3875d14401b8381d1/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9", size = 263087, upload-time = "2026-08-15T08:18:49.859Z" }, + { url = "https://files.pythonhosted.org/packages/98/66/7c42677e739ba66746b297e2046918d793078094dc239e1e72768cffccc6/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a", size = 243269, upload-time = "2026-08-15T08:18:51.601Z" }, + { url = "https://files.pythonhosted.org/packages/de/d8/a50b79237f417af10f8c2a501ce8d1ca87829a22e69117891ca4ba20a69e/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032", size = 258766, upload-time = "2026-08-15T08:18:53.23Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1d/0fc91aeaeb3c83b748f532399ce67cf84604b48297405d740000f7a9e786/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e", size = 250814, upload-time = "2026-08-15T08:18:54.768Z" }, + { url = "https://files.pythonhosted.org/packages/ae/10/3d8c777cf9024615295aa1b808324ad5b4a77855869c00824bad74ffaf8a/charset_normalizer-3.5.1-cp314-cp314t-win32.whl", hash = "sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4", size = 191074, upload-time = "2026-08-15T08:18:56.305Z" }, + { url = "https://files.pythonhosted.org/packages/4d/81/ae557d3c44d1a1d688696d60563413a0866a91b7ebc50f20df838be3d8c8/charset_normalizer-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00", size = 216476, upload-time = "2026-08-15T08:18:57.889Z" }, + { url = "https://files.pythonhosted.org/packages/27/e9/61c01fb8b804692569c036b3fc50495814502dcf13a60649c6055390b02c/charset_normalizer-3.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f", size = 194115, upload-time = "2026-08-15T08:18:59.418Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4e/8544831ef59d8f27ce92c80871380fdacc8076a8a56ed62f82e54f991333/charset_normalizer-3.5.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9085f87b0e38a2b92b8923059b4e8789fe40d9279712d15dcc670048d77079af", size = 342048, upload-time = "2026-08-15T08:19:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/7f/a6/e3b46852424246065355644f4fb6dbccc0239a42a2eee27ecfc8957f0bcd/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2679de311c7946dde5d3b6f44941844133ff5c7cb86099c0061ab1e8901c20a8", size = 242997, upload-time = "2026-08-15T08:19:02.492Z" }, + { url = "https://files.pythonhosted.org/packages/03/3b/0cc9a26777334ab2f2e3089b948bbf4e4fe72ea70b897715ef6415043ec8/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:baf3775a2635e5a11fbd5e4e64ee69c7e86875d224a5c72aca4c141064589a90", size = 237014, upload-time = "2026-08-15T08:19:03.943Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c2/027335f0aa337a2a2e121bac1ad88c4f02ba6053ea0926802784f3db11af/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ac8c94b6539074e0f40899301273ac8402b9b3e01c7b7ba269ff30340aaaf20", size = 266174, upload-time = "2026-08-15T08:19:05.598Z" }, + { url = "https://files.pythonhosted.org/packages/86/d3/e367787febe4e74769dec0f406f2c3c8d1b955fce5aee1fd0f94e8367a45/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe532b3c966d1fb794e0698e4589d0444017ae77fc0b31edea13c0e35bcc449", size = 263361, upload-time = "2026-08-15T08:19:07.251Z" }, + { url = "https://files.pythonhosted.org/packages/af/3d/391b193eb9f3e84b02f9314088c386debdc0debee843535aaea2e2c6715d/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c84bec0ab5ae0c64bfe73a7d2adcb5ce73b467523fc27fd6a28ab2aa6cbe35a", size = 252143, upload-time = "2026-08-15T08:19:08.816Z" }, + { url = "https://files.pythonhosted.org/packages/2e/57/de221f1745a90d418199761967e2776bfe2c275a1194220985e8c1d37833/charset_normalizer-3.5.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:854066be00447fa8de2ccbbe893e2ffc4b123ef16d897af794c1e18bd4a714b0", size = 252086, upload-time = "2026-08-15T08:19:10.255Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e3/d119f86a01f9331e8186175f24873b1d74a7ee9e2e4b4d68f9947dae5afd/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:21b82d8082f6f5e7f456ef0bd16323d08de1266efbfeb476e64b2a91d1471a4e", size = 245231, upload-time = "2026-08-15T08:19:11.807Z" }, + { url = "https://files.pythonhosted.org/packages/26/de/d8e48c135ae480879539cdb179c8d3b50c7879497d75dd899b5763b69cee/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:838648accb3a7fd9803fd45c87bce8509648eb0c11bc34e216141300977244f2", size = 241546, upload-time = "2026-08-15T08:19:13.416Z" }, + { url = "https://files.pythonhosted.org/packages/67/c4/217755fd1abc50d326c252922cd642002758095a81ff45010337b8b3ef65/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:195ce897c6153c0700078142cf8efe3e6454ca4cf4357499e4078dfd83396626", size = 267033, upload-time = "2026-08-15T08:19:14.981Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d7/34d8e404e358d2adcc5a228c2134643af00104c8fb0bf525f3688d756f05/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:978eab16f55b4ab2c2a745be9a0a840bf8f09a7f227d9c76eb30214d078865a5", size = 252045, upload-time = "2026-08-15T08:19:16.618Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fa/40414471acf0aa0692ca77305aa00e434fcd8288f0941c93c30e9a5f8f2f/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:cc0329df4caaceb950d2f580b5ac716a377f7059624a0bafaeaf8a218c6ed774", size = 264866, upload-time = "2026-08-15T08:19:18.101Z" }, + { url = "https://files.pythonhosted.org/packages/32/90/fcc850bae791abd2e0c041847f13e270aa08692a79f3e00de6d2dce1cb50/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:687c9ca3035544b113bea2055e180af96fb63c0c476e22a9180f51925186e7b7", size = 253932, upload-time = "2026-08-15T08:19:19.734Z" }, + { url = "https://files.pythonhosted.org/packages/af/af/53afe99068b3c10b4cbae592a52ef72a7c92c0188440e83ee3a078fd8f75/charset_normalizer-3.5.1-cp315-cp315-win32.whl", hash = "sha256:706bfd38730a5ac7a365793269a00f4e988178cec121391f4248d84ad8c972e9", size = 180320, upload-time = "2026-08-15T08:19:21.37Z" }, + { url = "https://files.pythonhosted.org/packages/c9/bc/f46a132041b29e4a8779ed712d3df1bf112e94ca8de58b66d7ec2c0cf8b9/charset_normalizer-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:92caef967d287a407085d61176fce4012b1dd62daed4eb6d5ceb26d3d2538712", size = 204174, upload-time = "2026-08-15T08:19:23.088Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5d/9ed554480eda8e447b673648628fdc29574d23dbad01fe11837adedd1cae/charset_normalizer-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:5fc45d653ea8c9a20479167e11d4a0f8cb2fa3470737ab6f9c827532313187b7", size = 184126, upload-time = "2026-08-15T08:19:24.471Z" }, + { url = "https://files.pythonhosted.org/packages/3b/32/9b8929bf384061ee1fe5d9c27c6f9776d3d824039ad4e14c88ec00c7808e/charset_normalizer-3.5.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:59171c6e45bf07d0d5cab3b0bf81d945035530f6873398b3b531c31184d46663", size = 381441, upload-time = "2026-08-15T08:19:26.038Z" }, + { url = "https://files.pythonhosted.org/packages/96/10/e9aa7923d3ddac652c99a1c5f7be494e737e151566a44abe018daf757f2c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9dbdd9205662134957cf0c324f639bdc5031c0ca056e2369e238db75187c0f11", size = 241742, upload-time = "2026-08-15T08:19:27.532Z" }, + { url = "https://files.pythonhosted.org/packages/28/53/a2d249ebddf47b889a100c0bdcb61a2f9dbb8bc24ef325cc062e4f476877/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4b018dc5a0eee4676e38fe84a47a427816c590b93b55d9025274ec4d6ffc2dc", size = 235298, upload-time = "2026-08-15T08:19:29.274Z" }, + { url = "https://files.pythonhosted.org/packages/7d/07/469f78af590f7d5cd48e20d8dbfa3d66deeff9ba37768c04d886b5afd45c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced3fdd71aaa83ce593746c2edb42b7a59cb4c19c8b5c407781c72e493aae55a", size = 262500, upload-time = "2026-08-15T08:19:30.955Z" }, + { url = "https://files.pythonhosted.org/packages/55/66/3bb56a47f7dcba014055b1a1d33c6f08bbe9c1e74dba154cfa25f90ae885/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:19a3dd5aa73cef1c99687c4fc57db016a9c17104ae1185da88ba566a5d3bebe4", size = 258888, upload-time = "2026-08-15T08:19:32.458Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c1/2adc2800903fb013210349313b710a5376856578d9e33e6b9a1d8b36714a/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc5d36d96478aa9c60654bd932525bf32964c62a7281eafdf16d85003a8d6004", size = 250243, upload-time = "2026-08-15T08:19:33.94Z" }, + { url = "https://files.pythonhosted.org/packages/95/b5/a18d0dd1157ab655cc2cb14a545f4a4784bbad70ab3502412e36097502d9/charset_normalizer-3.5.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04368edf83514385ffc3e1cfd4546e595f4f1272dd23ba437a93a9cc3741d47b", size = 249871, upload-time = "2026-08-15T08:19:35.413Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c3/525f508cd1e58d0450ac55ed40ac75bc3a97482c59def5278456a5fbf03c/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5db6052055d34d41230fb78d7c439c23dc536a9896f6cb039e8dd92cfc1263", size = 243580, upload-time = "2026-08-15T08:19:36.886Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c1/49a91fe7e97c8140094ca5c64161ab623a70d9f636bf834eace14048acb5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:252d099029bcbea642f2a06c4ed5046bdf8b5a8150b64afa5e027e88b106e5ee", size = 239807, upload-time = "2026-08-15T08:19:38.392Z" }, + { url = "https://files.pythonhosted.org/packages/d3/58/56a48c296601274c4689b864a8e2dfb209b81dfcb39472753ce95eea662b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6199d5606e2bbf2b096cf64d03f8b6790c91081d5ac866b8e7bb6422738cc60c", size = 264083, upload-time = "2026-08-15T08:19:39.856Z" }, + { url = "https://files.pythonhosted.org/packages/10/4c/dc48409274a1817ff349711d26c62aa0c597df865d4d69ef79160c859193/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:77efcff2b23071c349402ac1066667a3d011f62398d81408c9b88ad991747c9e", size = 250317, upload-time = "2026-08-15T08:19:41.53Z" }, + { url = "https://files.pythonhosted.org/packages/81/58/d325912115caec62d6bdd77bbab5e0b7da5d234a9f20affdffcbcb530d0b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:a5cbd90ecf0fc62e64726917ad083b73001f0563657a87ec3c0b504e277dc90d", size = 258173, upload-time = "2026-08-15T08:19:43.07Z" }, + { url = "https://files.pythonhosted.org/packages/34/f7/b13b1ccae2c8ec63980d13be1890eb73f8aeabbfce02a24aabc0908788f5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:4d26f14f041e83dd8edfd61f4cd4fa7285d31798b5bf1f28e70c367ba6c41d61", size = 251960, upload-time = "2026-08-15T08:19:44.587Z" }, + { url = "https://files.pythonhosted.org/packages/1e/25/ed3f9919c5aef8cc818be1f972f565f7610d7b2076b8ebb98839516ffc3c/charset_normalizer-3.5.1-cp315-cp315t-win32.whl", hash = "sha256:ac13b004224fb341e1e25a1ed5e19d32f57cdb2a403e01f003b46f051a550f6f", size = 191186, upload-time = "2026-08-15T08:19:46.293Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/43c2b3e9d8267092b913eb8b0603f0f71993c395632886bd37a7223f96cf/charset_normalizer-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:35aea775dc2bd5f54cd84a1cd2696cc3207c479cb9cf0bd346f0d343e4300ddb", size = 215947, upload-time = "2026-08-15T08:19:47.853Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/9aad3e9c8865e5e0efa9a7f6f81c37a67635a985145ecd44528a81e088ee/charset_normalizer-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:fb78f6e7fcd8ad785d28cd577168bc1aaee827b25bb8755638f694794ea98f0a", size = 193909, upload-time = "2026-08-15T08:19:49.383Z" }, + { url = "https://files.pythonhosted.org/packages/5b/97/fb4e82231aba271ffd775a1b4993b0defc4e3059f286ae41d9433409fe85/charset_normalizer-3.5.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2", size = 331467, upload-time = "2026-08-15T08:19:50.959Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2f/fe3f187327aac18e2d54e9d2b08e15d27bf9b642d9e51c219f130fc34d1a/charset_normalizer-3.5.1-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99", size = 253057, upload-time = "2026-08-15T08:19:52.654Z" }, + { url = "https://files.pythonhosted.org/packages/d7/c7/9e48cee5c161fe24da823b61bf381921d77cb994a0a4de148e95018c1984/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2", size = 240930, upload-time = "2026-08-15T08:19:54.163Z" }, + { url = "https://files.pythonhosted.org/packages/49/e0/716601f3cc69be7b198951150c75ead1ece33c3c8036ff6ffa46029659a0/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235", size = 230822, upload-time = "2026-08-15T08:19:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/d3/05/71bfc5caa0abcc45aea1f6a4d50ac68e59605ddc7666fe8494f4cd229665/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598", size = 260037, upload-time = "2026-08-15T08:19:57.312Z" }, + { url = "https://files.pythonhosted.org/packages/c3/92/de7e32ed05341e7a9c4c877c318418197b7f2d66a3b68d561bf2ac57ca3e/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96", size = 255097, upload-time = "2026-08-15T08:19:59.056Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7b/ade0a122600319dfa0b1000ab0f9731c94a817904cf3c5de408c73a4ede7/charset_normalizer-3.5.1-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962", size = 250166, upload-time = "2026-08-15T08:20:00.612Z" }, + { url = "https://files.pythonhosted.org/packages/75/9c/019fbb9f4834491a160951349b1a3714439376f66e5f7cf18b4f18f0c7aa/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3", size = 241821, upload-time = "2026-08-15T08:20:02.321Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/11d4840bfc99330cc7fbcc2681ee5a044553a6e77655508d8f9b2bff7b34/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950", size = 232529, upload-time = "2026-08-15T08:20:04.008Z" }, + { url = "https://files.pythonhosted.org/packages/18/96/2b3a21492d9f65171ac75d872f5018260013d00bfa0ff70ec9f179148cbd/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8", size = 260348, upload-time = "2026-08-15T08:20:05.877Z" }, + { url = "https://files.pythonhosted.org/packages/d6/aa/a69a2028e8bd052476c245460ab19d7de595de084dd968f2d75cd50c3e25/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031", size = 247234, upload-time = "2026-08-15T08:20:07.487Z" }, + { url = "https://files.pythonhosted.org/packages/35/8a/3d130aeabcaf3d2466af76b7b141c08d9e89c9016ab4b7cdd0f7dc2d1c62/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072", size = 256917, upload-time = "2026-08-15T08:20:09.142Z" }, + { url = "https://files.pythonhosted.org/packages/80/c2/a7379b840292d0c1ab9fbd17d1f3967aa81794dc95bc74be8999d7fedcf7/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d", size = 254846, upload-time = "2026-08-15T08:20:10.727Z" }, + { url = "https://files.pythonhosted.org/packages/01/65/d43b714731bb2f40d4053dfa00ecfc1c5a301f8e3316c5db3a09af59fe94/charset_normalizer-3.5.1-cp37-abi3-win32.whl", hash = "sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc", size = 174216, upload-time = "2026-08-15T08:20:12.334Z" }, + { url = "https://files.pythonhosted.org/packages/35/4f/b911ed898b26a09789eba9c9200c999aff6c61b4bafaf4838e56d1a1e1a3/charset_normalizer-3.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959", size = 199764, upload-time = "2026-08-15T08:20:13.908Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a7/920baf467bfd9bf689f3b318340f37aee4572a71f162bd8db51da55ba4fa/charset_normalizer-3.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e", size = 287318, upload-time = "2026-08-15T08:20:15.551Z" }, + { url = "https://files.pythonhosted.org/packages/cc/61/d01fc49b8dea277640b55a9e15960dbca9fdc8c9fde18e572d39c59f4019/charset_normalizer-3.5.1-py3-none-any.whl", hash = "sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6", size = 68658, upload-time = "2026-08-15T08:20:43.306Z" }, +] + [[package]] name = "clarabel" version = "0.11.1" @@ -516,6 +669,11 @@ dev = [ { name = "ruff" }, { name = "ty" }, ] +docs = [ + { name = "mkdocs" }, + { name = "mkdocs-material" }, + { name = "mkdocstrings", extra = ["python"] }, +] [package.metadata] requires-dist = [ @@ -541,6 +699,11 @@ dev = [ { name = "ruff", specifier = ">=0.15" }, { name = "ty", specifier = ">=0.0.1a0" }, ] +docs = [ + { name = "mkdocs", specifier = ">=1.6.1" }, + { name = "mkdocs-material", specifier = ">=9.7.7" }, + { name = "mkdocstrings", extras = ["python"], specifier = ">=1.0.6" }, +] [[package]] name = "fancyimpute" @@ -597,6 +760,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/47/c99d5268f354002ce80f8d029cd9d7d872969da1de8b93d32de4dc56d6f4/fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d", size = 1164562, upload-time = "2026-05-14T12:04:29.092Z" }, ] +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, +] + +[[package]] +name = "griffelib" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/b4/a767e91c606deefc447a96eaf59edd77397960b1d677dffd833ee8449831/griffelib-2.2.0.tar.gz", hash = "sha256:e1bc36fe9cd21d4b6b659b456346755e4cfdc5676c0a5214083126ee12612b3c", size = 227048, upload-time = "2026-08-16T14:04:58.383Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/b6/f65ac785d4ac90dcf7c831ac6256f5dd4a19780f4e1575b2c0d6eeebe319/griffelib-2.2.0-py3-none-any.whl", hash = "sha256:d71c3bc2bbed9f958488634fe788b843a9f705d6d2838ca32cd6c25eeb64dfc4", size = 166779, upload-time = "2026-08-16T14:04:54.365Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -934,6 +1118,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/31/a8/237d8de1d77085cfd41d0c6049a044d8d01886f3afb7f1eda2f43d900a96/lxml-6.1.2-cp315-cp315t-win_arm64.whl", hash = "sha256:f16a407766bac51c65d605b06d900821751a79aa20e12185f273f14a17180e7b", size = 3822823, upload-time = "2026-08-19T05:05:04.63Z" }, ] +[[package]] +name = "markdown" +version = "3.10.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/6f/da4c6aea59b3001f2e8c0ec7497475aadaf3b021c10cab5b2858f0f32b26/markdown-3.10.3.tar.gz", hash = "sha256:3589362618f743188b4d955b874402bc814f4f83f544dc207719f4baa7d9c45f", size = 372596, upload-time = "2026-07-30T19:05:29.005Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/69/4a5af2bc115a9a33fefe51709749de8262be3f9ba063d1753a837cdbc49c/markdown-3.10.3-py3-none-any.whl", hash = "sha256:fa6c92a00a4a3c98b22728c64a935ae1928250ae65058a6ded814d2cc29a4cea", size = 110757, upload-time = "2026-07-30T19:05:27.883Z" }, +] + [[package]] name = "markupsafe" version = "3.0.3" @@ -1062,6 +1255,134 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/e8/f7/47bddf95492f4d1370ed7164d2b16407805e8eeb38231361de65d387a562/matplotlib-venn-1.1.2.tar.gz", hash = "sha256:6f2b07a03e9bb5a62de2f32f965216739e175176f9d654dd19e7af2c22ec36e3", size = 40821, upload-time = "2025-02-25T10:44:24.294Z" } +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, +] + +[[package]] +name = "mkdocs-autorefs" +version = "1.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/c0/f641843de3f612a6b48253f39244165acff36657a91cc903633d456ae1ac/mkdocs_autorefs-1.4.4.tar.gz", hash = "sha256:d54a284f27a7346b9c38f1f852177940c222da508e66edc816a0fa55fc6da197", size = 56588, upload-time = "2026-02-10T15:23:55.105Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl", hash = "sha256:834ef5408d827071ad1bc69e0f39704fa34c7fc05bc8e1c72b227dfdc5c76089", size = 25530, upload-time = "2026-02-10T15:23:53.817Z" }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/25/b3cccb187655b9393572bde9b09261d267c3bf2f2cdabe347673be5976a6/mkdocs_get_deps-0.2.2.tar.gz", hash = "sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1", size = 11047, upload-time = "2026-03-10T02:46:33.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl", hash = "sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650", size = 9555, upload-time = "2026-03-10T02:46:32.256Z" }, +] + +[[package]] +name = "mkdocs-material" +version = "9.7.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "backrefs" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/cd/c05d3a530ba7934f144fb45f7203cd236adc25c7bdcc34673d202f4b0278/mkdocs_material-9.7.7.tar.gz", hash = "sha256:c0649c065b1b0512d60aad8c10f947f8e455284475239b364b610f2deb4d0855", size = 4097923, upload-time = "2026-07-17T16:21:33.156Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/21/17c1bc9e6f47c972ad66fb2ac2568f99f90f1207eeb6fc3b34d094dba7b5/mkdocs_material-9.7.7-py3-none-any.whl", hash = "sha256:8ea9bb1737a5b524a5f9dcf2e1b4ebda8274ae3008aa7845720a97083bef708f", size = 9305438, upload-time = "2026-07-17T16:21:30.017Z" }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, +] + +[[package]] +name = "mkdocstrings" +version = "1.0.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, + { name = "mkdocs-autorefs" }, + { name = "pymdown-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/71/f85bdf13355073ae15a7375f09879375a830553552e58c1c4b7e0bbc5c8b/mkdocstrings-1.0.6.tar.gz", hash = "sha256:a0b8c2bdd29a6416c80d717aa369bbf7831946bd9f23c2a66db1b1dbe7693dbd", size = 100649, upload-time = "2026-07-11T19:38:05.732Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/5b/4c1902e8bdd5c4db63284e9d101dece4038d4025d6d88850ffe0a1578980/mkdocstrings-1.0.6-py3-none-any.whl", hash = "sha256:2703708697487d1b6d6d7b412e176fa436edf120c1bf81dc9e126b12d00893c7", size = 35787, upload-time = "2026-07-11T19:38:04.417Z" }, +] + +[package.optional-dependencies] +python = [ + { name = "mkdocstrings-python" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "2.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffelib" }, + { name = "mkdocs-autorefs" }, + { name = "mkdocstrings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/5d/1be1c7a49d8fa13dc80f66a85f53333d52cf5206911412006ffdff8fb9a0/mkdocstrings_python-2.0.7.tar.gz", hash = "sha256:8c49faf66d243072d7590a1b5dea028d9d7425fac191f54f096123a4a9c1a783", size = 201598, upload-time = "2026-08-17T16:56:18.239Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/6d/77546d8c26f038fce314a507106954f76270f6c182488bcf9ac9721175df/mkdocstrings_python-2.0.7-py3-none-any.whl", hash = "sha256:1fce5fbfe4ffa6e8136a35351cdc97c3bf55219c7efbd3f92a82260f93235d60", size = 105387, upload-time = "2026-08-17T16:56:16.813Z" }, +] + [[package]] name = "mygene" version = "3.2.2" @@ -1209,6 +1530,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, ] +[[package]] +name = "paginate" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, +] + [[package]] name = "pandas" version = "3.0.5" @@ -1268,6 +1598,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bf/dd/49f5d0aa985c14218da8759ec4a93390b683c92fd9081d5b96383d48db6e/panflute-2.3.1-py3-none-any.whl", hash = "sha256:e44afd875b7b17ffebbbe58282849df06d9f1b20a45a2f933cd51bdcf4e89130", size = 36714, upload-time = "2024-03-20T05:47:38.375Z" }, ] +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + [[package]] name = "patsy" version = "1.0.2" @@ -1351,6 +1690,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" }, ] +[[package]] +name = "platformdirs" +version = "4.11.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/d7/e7bfbc86e9f99ff7807e24de7703f032e9c9ba80bb355cf26e0e9bc5a75e/platformdirs-4.11.3.tar.gz", hash = "sha256:66a73d38a849810252df809a3d8bcbda8e26f6c189920e7535ad608a48dbb5ab", size = 33050, upload-time = "2026-08-13T22:43:27.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/a9/c34aebedd3a4c9afe5101b1b8713710b3fec18087c8a36c35d2f909861bd/platformdirs-4.11.3-py3-none-any.whl", hash = "sha256:5ed065d443751de711da036041a7a214122efc4a4de393b3f4137ba5576540e7", size = 23491, upload-time = "2026-08-13T22:43:26.121Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -1378,6 +1726,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, ] +[[package]] +name = "pymdown-extensions" +version = "11.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/17/2db4b414de89659144488e0d9c6c0bf0c8395841dc12d81d0532cc6ef310/pymdown_extensions-11.0.2.tar.gz", hash = "sha256:9506fcbe66fa355a775b768084334238dd6805020ac4b92bea0c0dda6f8f223d", size = 855419, upload-time = "2026-08-22T19:28:47.236Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/43/9f45ec4d14e596efc32c925a78104934790438b0c0628b70d741016734ad/pymdown_extensions-11.0.2-py3-none-any.whl", hash = "sha256:259910762019732caa1dfd76f3faa62c59f191d46573e80bcb1d13c0f675bbe5", size = 269929, upload-time = "2026-08-22T19:28:45.389Z" }, +] + [[package]] name = "pyparsing" version = "3.3.2" @@ -1389,7 +1750,7 @@ wheels = [ [[package]] name = "pytest" -version = "9.0.3" +version = "9.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -1398,9 +1759,9 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] [[package]] @@ -1475,6 +1836,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, +] + [[package]] name = "qdldl" version = "0.1.9.post1" @@ -1517,6 +1890,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/63/48/34fa827457aa0e373fb50dab4490757350076f9afcf7eb749a71926023af/qdldl-0.1.9.post1-cp314-cp314t-win_arm64.whl", hash = "sha256:fcc2184cac1e502ed624efe7f00c1c215b95cfd324a8cacf7f0053c00fa524f5", size = 107844, upload-time = "2026-02-19T16:48:22.899Z" }, ] +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + [[package]] name = "ruff" version = "0.16.4" @@ -1583,53 +1971,73 @@ wheels = [ [[package]] name = "scipy" -version = "1.18.0" +version = "1.18.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a7/25/c2700dfaf6442b4effaa91af24ebce5dc9d31bb4a69706313aae70d72cd0/scipy-1.18.0.tar.gz", hash = "sha256:67b2ad2ad54c72ca6d04975a9b2df8c3638c34ddd5b28738e94fc2b57929d378", size = 30774447, upload-time = "2026-06-19T15:01:43.456Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/19/ca10ead60b0acc80b2b833c2c4a4f2ff753d0f58b811f70d911c7e94a25c/scipy-1.18.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:7bd21faaf5a1a3b2eff922d02db5f191b99a6518db9078a8fb23169f6d22259a", size = 31056519, upload-time = "2026-06-19T14:59:45.203Z" }, - { url = "https://files.pythonhosted.org/packages/96/72/1e6442a00cd2924d361aa1b642ab6373ec35c6fabf311a760be9f76e0f13/scipy-1.18.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:265915e79107de9f946b855e50d7470d5893ec3f54b342e1aa6201cbdcd8bb6b", size = 28681889, upload-time = "2026-06-19T14:59:48.103Z" }, - { url = "https://files.pythonhosted.org/packages/9b/2d/11dd93d21e147a73ba22bd75c0b9208d3a2e0ec76d53170ce7d9029b1015/scipy-1.18.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9ab7b758be6940954a713ee466e2043e9f6e2ed965c1fce5c91039f4be3d90a9", size = 20423580, upload-time = "2026-06-19T14:59:50.665Z" }, - { url = "https://files.pythonhosted.org/packages/9c/01/93552f75e0d2a7dd115a45e59209c51e8d514daff02fc887d2623be06fe1/scipy-1.18.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:97b6cddaaee0a779ef6b5ca83c9604b27cc16b2b8fc22c142652df8793319fb8", size = 23054441, upload-time = "2026-06-19T14:59:53.564Z" }, - { url = "https://files.pythonhosted.org/packages/3c/23/21f5e703643d66f21faa6b4c73195bfcad70c55efcb4f1ab327cd7c4101a/scipy-1.18.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:52a96e21517c7292375c0e27dd796a811f03fcea5fd4d108fdfea8145dcf17ab", size = 33968720, upload-time = "2026-06-19T14:59:56.415Z" }, - { url = "https://files.pythonhosted.org/packages/dd/aa/1b939f6c67ed68635bb538e6752d3dacc02f66535182e939a89581a44e9c/scipy-1.18.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f55797419e16e7f30cf88ffb3113ce0467f00cfe3f70d5c281730b21769bfc2", size = 35287115, upload-time = "2026-06-19T14:59:59.411Z" }, - { url = "https://files.pythonhosted.org/packages/b6/ff/eec46be7e9234208f801062b53e1983085eddebd693f6c9bfb03b459830d/scipy-1.18.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ad033410e2e0672ffdc1042110cef20e1c46f8fd0616cee1d44d8d58fad8fc11", size = 35577989, upload-time = "2026-06-19T15:00:02.235Z" }, - { url = "https://files.pythonhosted.org/packages/84/ca/210d4759c7210bb7d269437421959b39a33434e2776b60c5cb8a763bb30a/scipy-1.18.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4a55985d54c769c872e64b7f4c8a81cc30ef700cc04296abbbf3705439c126de", size = 37421717, upload-time = "2026-06-19T15:00:05.102Z" }, - { url = "https://files.pythonhosted.org/packages/2b/54/9a9edb45345bd6744da5ddfb6628e5d5185920494c6a67ec45b6381004cb/scipy-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:71ccc8faa2dd16ac310233203474a8b5cb67f10dedd54a3116d34943f4b19132", size = 36597428, upload-time = "2026-06-19T15:00:08.112Z" }, - { url = "https://files.pythonhosted.org/packages/99/0e/33f32a2a58987e26aec0f7df252cbbad1e90ae77bdbc76f40dd4ed0cf0ea/scipy-1.18.0-cp312-cp312-win_arm64.whl", hash = "sha256:d88363fd9d8fbd3511bd273f1a49efb2a540773ddf92a91d57498ce7dd7f3e76", size = 24351481, upload-time = "2026-06-19T15:00:11.103Z" }, - { url = "https://files.pythonhosted.org/packages/05/52/9c0136c2de7ae0779b7b366447766cec6d9f0702c56bb8ffeb04c8fd3af4/scipy-1.18.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:09143f676d157d9f546d663504ef9c1becb819824f1afc018814176411942446", size = 31036107, upload-time = "2026-06-19T15:00:14.03Z" }, - { url = "https://files.pythonhosted.org/packages/02/73/0291a64843270f4efb86cdcf2ee0f2048631b65ec6b405398b2b4dbf11bf/scipy-1.18.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5efe260f69417b97ddae455bfb5a95e8359f7f66ad7fa9522a60feb66f169520", size = 28663303, upload-time = "2026-06-19T15:00:16.819Z" }, - { url = "https://files.pythonhosted.org/packages/d3/0f/10ffa0b697a572f4e0d48b92a88895d366422f019f723e7e14a84c050dac/scipy-1.18.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:68363b7eaacd8b5dd426df56d782cc156468ac79a127a1b87ca597d6e2e82197", size = 20404960, upload-time = "2026-06-19T15:00:19.635Z" }, - { url = "https://files.pythonhosted.org/packages/7e/d2/e896cea21ba8edd6c81d4c55b1ffcc717e79698dcbebf9641b4cfb4c6622/scipy-1.18.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:c5557d8be5da8e41353fcd4d21491fdbab83b062fc579e94dc09a7c8ab4f669b", size = 23034074, upload-time = "2026-06-19T15:00:22.107Z" }, - { url = "https://files.pythonhosted.org/packages/ea/b2/e83ea34279a52c03374477c74006256ec78df65fc877baa4617d6de1d202/scipy-1.18.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d13bca67c096d89fb95ced0d8921807300fce0275643aef9533cc63a0773468", size = 33942038, upload-time = "2026-06-19T15:00:24.964Z" }, - { url = "https://files.pythonhosted.org/packages/f6/af/e8fe5fb136f51e2b01678b92cb4106d10d8cd68ec147ead2e7cb0ac75398/scipy-1.18.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a46f9273dbd0eb1cefba61c9b8648b4dfe3cbc14a080176f9a73e44b8336dc7f", size = 35266390, upload-time = "2026-06-19T15:00:28.059Z" }, - { url = "https://files.pythonhosted.org/packages/3a/49/2c5cbb907b56695fc67517811d1db234dfd83381a84814ec220aded2794d/scipy-1.18.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5aba46108853ddfc77906b6557aac839d2b52e900c1d72a1180adaaab58d265f", size = 35551324, upload-time = "2026-06-19T15:00:31.014Z" }, - { url = "https://files.pythonhosted.org/packages/bb/73/eda39f7a2d306ff0ffc574afd13c0bbb6d10a603d9a413998ee269487a80/scipy-1.18.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b6f758e35f12757b5d95c00bc6de2438e229c2664b7a92e96f205959d9f2dfa4", size = 37404785, upload-time = "2026-06-19T15:00:34.072Z" }, - { url = "https://files.pythonhosted.org/packages/b7/d2/ae881ee28d014f38e0ccbfd974a06a919ba9af34f1f74bf42b5301891d63/scipy-1.18.0-cp313-cp313-win_amd64.whl", hash = "sha256:1afac4a847207c7ff8efd321734a50b06d0280b3b2a2c0fc2f413101747ad7c7", size = 36554943, upload-time = "2026-06-19T15:00:36.903Z" }, - { url = "https://files.pythonhosted.org/packages/70/3a/21154e2d54eb3639c6bf4dbae2e531c68356bfe95990daa30df33b30d556/scipy-1.18.0-cp313-cp313-win_arm64.whl", hash = "sha256:c5dbddf60e58c2312316d097271a8e73d40eaf2eabfa4d95ed7d3695bbf2ce7b", size = 24350911, upload-time = "2026-06-19T15:00:40.062Z" }, - { url = "https://files.pythonhosted.org/packages/78/b5/915a19b3de2f7430062b509653563db1633ddbb6f021b06731521115d4e2/scipy-1.18.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4c256ee70c0d1a8a2ace807e199ccd4e3f57037433842abb3fb36bc17eaa9578", size = 31036253, upload-time = "2026-06-19T15:00:43.216Z" }, - { url = "https://files.pythonhosted.org/packages/d7/88/b72def7262e150d16be13fca37a96481138d624e700340bc3362a7588929/scipy-1.18.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:2ef3abc54a4ffc53765374b0d5728532dfdd2585ed23f6b11c206a1f0b1b9af8", size = 28673758, upload-time = "2026-06-19T15:00:46.663Z" }, - { url = "https://files.pythonhosted.org/packages/91/02/2e636a61a525632c373cf6a9c24442a3ffb79e364d38e98b32042964ac32/scipy-1.18.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f2a6af57bd9e4a75d70e4117e78a1bbee84f79ae3fbb6d0111005d6ebcc4cb8d", size = 20415514, upload-time = "2026-06-19T15:00:49.399Z" }, - { url = "https://files.pythonhosted.org/packages/c9/b6/2135974442f6aba159d9d39d774a1c8cb19947016725d69fecc685df45bf/scipy-1.18.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:3f1ac564d3bf6c03d861d2cd87a1bea0da2887136f7fb1bf519c05a8971452d6", size = 23034398, upload-time = "2026-06-19T15:00:51.941Z" }, - { url = "https://files.pythonhosted.org/packages/f6/e6/ba89ec5abf6ee9257c0d1ec985573f3ae32742c24bc03e016388a40b1b15/scipy-1.18.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40395a5fcd1abee49a5c7aaa98c29db393eedc835138560a588c47ec16156690", size = 33998032, upload-time = "2026-06-19T15:00:54.838Z" }, - { url = "https://files.pythonhosted.org/packages/7f/c4/bc41eb19b0fd0db868f4132920879019318d80cc522ad8f2bca4611af808/scipy-1.18.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ca01e8ae69f1b18e9a58d91afead31be3cef0dd905a10249dac559ee15460a0", size = 35283333, upload-time = "2026-06-19T15:00:58.152Z" }, - { url = "https://files.pythonhosted.org/packages/53/a4/cbdeef6eb3830a8462a9d4ada814de5fc984345cc9ecf17cbec51a036f1e/scipy-1.18.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7a7f3b01647384dbc3a711e8c6778e0aabbe93959249fef5c7393396bcac0867", size = 35610216, upload-time = "2026-06-19T15:01:01.155Z" }, - { url = "https://files.pythonhosted.org/packages/80/4d/b2b82502b65f661d1b789c1665dcdf315d5f12194e06fc0b37946294ebae/scipy-1.18.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6aa94e78ec192a30063a5e72e561c28af769dc311190b24fe91774eff1969709", size = 37418960, upload-time = "2026-06-19T15:01:04.155Z" }, - { url = "https://files.pythonhosted.org/packages/93/3e/902d836831474b0ab5a37d16404f7bc5fafd9efba632890e271ba952635f/scipy-1.18.0-cp314-cp314-win_amd64.whl", hash = "sha256:2d8bbdc6c817f5b4006a54d799d4f5bab6f910193cbb9a1ff310833d4d270f61", size = 37288845, upload-time = "2026-06-19T15:01:07.822Z" }, - { url = "https://files.pythonhosted.org/packages/b6/43/8d73b337a3bdb14daa0314f0434210747c02d79d729ce1777574a817dcf6/scipy-1.18.0-cp314-cp314-win_arm64.whl", hash = "sha256:18e9575f1569b2c54174e6159d32942e03731177f63dce7975f0a0c88d102f5b", size = 24988971, upload-time = "2026-06-19T15:01:11.076Z" }, - { url = "https://files.pythonhosted.org/packages/b4/b4/f11918b0508a2787031a0499a03fbe3546f3bb5ca05d01038c45b278c09a/scipy-1.18.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f351e0dd702687d12a402b867a1b4146a256923e1c38317cbc472f6372b94707", size = 31399325, upload-time = "2026-06-19T15:01:13.723Z" }, - { url = "https://files.pythonhosted.org/packages/7b/d1/1f287b57c0ff0ee5185dff3946d92c8017d39b0e431f0ae79a3ff1859512/scipy-1.18.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7c7a51b33ce387193c97f228320cf8e87361daa1bba750638677729598b3e677", size = 29092110, upload-time = "2026-06-19T15:01:16.908Z" }, - { url = "https://files.pythonhosted.org/packages/ff/1a/7b74eb6c392fdcb27d414c0e7558a6d0231eb3b6d73571f479bb81ea8794/scipy-1.18.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:84031d7b052a54fae2f8632e0ec802073d385476eb9a63079bce6e23ef9283d4", size = 20833811, upload-time = "2026-06-19T15:01:20.488Z" }, - { url = "https://files.pythonhosted.org/packages/7c/ad/f3941716320a7b9cb4d68734a903b45fe16eff5fb7da7e16f2e619304979/scipy-1.18.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:56abf29a7c067dde59be8b9a22d606a4ea1b2f2a4b756d9d903c62818f5dacce", size = 23396644, upload-time = "2026-06-19T15:01:23.364Z" }, - { url = "https://files.pythonhosted.org/packages/22/22/1446b62ffe07f9719b7d9b1b6a4e05a772833ae8f441fe4c22c34c9b250f/scipy-1.18.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ad44305cfa24b1ba5803cbbebf033590ccbac1aa5d612d727b785325ab408b0", size = 34079318, upload-time = "2026-06-19T15:01:26.002Z" }, - { url = "https://files.pythonhosted.org/packages/56/3b/b87da667098bb470fa30c7011b0ba351ee976dd395c78798c66e941665a3/scipy-1.18.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:945c1761b93f38d7f99ae81ae80c63e621471608c7eeead563f6df025585cd58", size = 35324320, upload-time = "2026-06-19T15:01:28.881Z" }, - { url = "https://files.pythonhosted.org/packages/f8/a1/c7932f91909759b0267f75fdea34e91309f96b895757534b76a90b6b4344/scipy-1.18.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1a4441f15d620578772a49e5ab48c0ee1f7a0220e387110283062729136b2553", size = 35699541, upload-time = "2026-06-19T15:01:31.968Z" }, - { url = "https://files.pythonhosted.org/packages/f7/86/5185061a1fcc41d18c5dc2463969b3a3964b31d9ac67b2fb05d4c7ff7670/scipy-1.18.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9aac6192fac56bf2ca534389d24623f07b39ff83317d58287285e7fbd622ff76", size = 37472480, upload-time = "2026-06-19T15:01:35.136Z" }, - { url = "https://files.pythonhosted.org/packages/31/8e/f04c68e39919a010d34f2ee1367fd705b0a25a02f609d755f0bfbc0a15fc/scipy-1.18.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e40baea28ae7f5475c779741e2d90b1247c78531207b49c7030e698ff81cee3f", size = 37365390, upload-time = "2026-06-19T15:01:38.091Z" }, - { url = "https://files.pythonhosted.org/packages/d5/19/969dc072906c84dd0a3b05dcf57ea750936087d7873549e408b35cfc3f97/scipy-1.18.0-cp314-cp314t-win_arm64.whl", hash = "sha256:368e0a705903c466aa5f08eefb39e6b1b6b2d659e7352a31fd9e2438365be0f8", size = 25279661, upload-time = "2026-06-19T15:01:40.817Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/7e/74/66de6258867beb2ef08f35f9f2ac017a52cacd5081714d239ff1a442d458/scipy-1.18.1.tar.gz", hash = "sha256:52c4b7422442aba924d03ad4019852b08a92e64ea187b933135687bfe2747307", size = 30781235, upload-time = "2026-08-21T23:28:50.599Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/f7/240c110c08693826b4513a52f5717d62ec7c7af72f2920821247c03b17b3/scipy-1.18.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:457fd7a2a8edeb044ab6ffbc0aa03ff6cd18491356e5e0c834d76ce621b916d1", size = 31111061, upload-time = "2026-08-21T23:23:44.522Z" }, + { url = "https://files.pythonhosted.org/packages/05/4a/78c6285577c375e7cf27277ea8ee6961224327f1e1a0c44af5f17f23635c/scipy-1.18.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:e708533e8b2ae2497d65346538a7dcc92814410b25b81432eac66de0f2af8265", size = 28733332, upload-time = "2026-08-21T23:23:50.015Z" }, + { url = "https://files.pythonhosted.org/packages/a5/f6/a5b82f8abbe14d134691b8b903696f701d25a081353a29dc655c364d9e62/scipy-1.18.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:7bbf207c4453ce1ad2e00b17313852b33310b83090c2311bdaf97f93c0380d12", size = 20475078, upload-time = "2026-08-21T23:23:54.138Z" }, + { url = "https://files.pythonhosted.org/packages/23/22/0858a0bbd6b3e825ceb8cd9baf9eaf3b2f2b1d77727eb6be40500bcdc92f/scipy-1.18.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:78c0665edead396b1abb4897c41a5c1d9bf090c8a637a4c20a61678e0a264e66", size = 23108904, upload-time = "2026-08-21T23:23:57.824Z" }, + { url = "https://files.pythonhosted.org/packages/75/9a/2e71719f31eaefe0e3a1706c4a1ded94e664bfd95ffca2b219a671faee01/scipy-1.18.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c085faa2cfa879c5141df483f836f4d691045a078224a670fa570fa01612d89", size = 34025113, upload-time = "2026-08-21T23:24:02.209Z" }, + { url = "https://files.pythonhosted.org/packages/df/64/ff35eb9e54894cf471ff4716abd3c81eb0a0626869217ce3e6ba4ccf17d7/scipy-1.18.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f55fa87b6c612ecd6b058f167c53231b1d14e412efe361d3d6e38b3631c73218", size = 35344199, upload-time = "2026-08-21T23:24:07.844Z" }, + { url = "https://files.pythonhosted.org/packages/d3/af/c5538be1792f7034c12c7db6ee67cace58253c7b87b122d68253eaf5de89/scipy-1.18.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c35d74ce0e193ff740c2f2be2ac913ddc232fe6c1ff40b26cfecb9c670c63314", size = 35639587, upload-time = "2026-08-21T23:24:13.05Z" }, + { url = "https://files.pythonhosted.org/packages/91/4c/075e4f66471bac101141ac739e9e135549be1bae584571bd03a530c056e1/scipy-1.18.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d2924a03db38dc2e848bca2fe9f077dafb891480b91a00a0963a8cf86dfc31c1", size = 37480330, upload-time = "2026-08-21T23:24:19.608Z" }, + { url = "https://files.pythonhosted.org/packages/39/e7/979fd14e75008623df31ba70d6bb144700f68feadcea042021c06a05bf82/scipy-1.18.1-cp312-cp312-win_amd64.whl", hash = "sha256:5e4d44984abc0020154ea81b247adeddcc3ac5527b975ff798bd1ba0adc513c2", size = 36658278, upload-time = "2026-08-21T23:24:25.463Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0b/e1525354ff9d7d5feb6d1b31af6d14072e5c91e9607b421fa1ec889660b3/scipy-1.18.1-cp312-cp312-win_arm64.whl", hash = "sha256:d65d448389b8436493abcf629cc94ad0cf32aecaf06e1acca1de53cc795f2f12", size = 24400588, upload-time = "2026-08-21T23:24:30.579Z" }, + { url = "https://files.pythonhosted.org/packages/b6/55/4540ee0f9c42a9ad7109d0d1a8cc70de54c3572b01c6693a2b1c70e90ceb/scipy-1.18.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:3ab3523da44749156e1f68b464dc56af11ae4cbc5c739a49d05f32b982eca9f3", size = 31089958, upload-time = "2026-08-21T23:24:35.8Z" }, + { url = "https://files.pythonhosted.org/packages/2a/f5/769f36d14922b8071a43e95d24d18b6bdafad10d7f5cf647867e1ac052bc/scipy-1.18.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e6fb6a55cc0ba97b59a1f288fb86dc6fce8bdfc0fffcbfd015e3a954bf2a2d93", size = 28715106, upload-time = "2026-08-21T23:24:40.775Z" }, + { url = "https://files.pythonhosted.org/packages/9a/d7/21d890274f75ea37a8209d5519e72da3da90302e3b9fb8397a0918386a62/scipy-1.18.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ea324d9dd34c38bfb9bec8ca4d1b407db97dbb74029f566b8e322b1b6fe56fe6", size = 20456846, upload-time = "2026-08-21T23:24:45.066Z" }, + { url = "https://files.pythonhosted.org/packages/ec/01/798430ecea2e78ec7c02663d5f71c007bb6abeca931080debd40d7fa55ea/scipy-1.18.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:75b00eb8fb802090aa903f4ea1c7f5a584779f967361e68b7e98e531cc2d7174", size = 23087986, upload-time = "2026-08-21T23:24:49.539Z" }, + { url = "https://files.pythonhosted.org/packages/e6/5f/4634e9d35c68496e4e34cb6946eafab044458e6cedab42b40b6588e475b6/scipy-1.18.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d416b16cccfd70fbf62400e84d0bb2f4e6af519a45557f1692c749b37f14b315", size = 33998146, upload-time = "2026-08-21T23:24:54.714Z" }, + { url = "https://files.pythonhosted.org/packages/41/48/6450ed9243315322bbc19ac57b9b70d66a20bf1d38d124c96bc4bf6af9ea/scipy-1.18.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fdaf5ea890a6183d0565f51a61799d67081bd5b1cf03c5f4b3fd3732108625c9", size = 35312578, upload-time = "2026-08-21T23:25:00.44Z" }, + { url = "https://files.pythonhosted.org/packages/00/bd/bf5a4be6a3525676499f6dff307991739ff6fdcad1481b1aeb6745339f58/scipy-1.18.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c825cef2f49e46753726a7181a8e199804a912b29519ada542c6ebc654951899", size = 35612621, upload-time = "2026-08-21T23:25:06.144Z" }, + { url = "https://files.pythonhosted.org/packages/bd/4e/3c45c33e00a77996c4b1cb707929f833ba7b1d522ee29f882512c330676d/scipy-1.18.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e3b417bf8c2c7c16e8f58ad91db17783ec911ac16e7b50eb6eab6e809b4f5b07", size = 37457323, upload-time = "2026-08-21T23:25:12.483Z" }, + { url = "https://files.pythonhosted.org/packages/93/0e/e0348fbc0dbab65c114cf78957e7dfeb49f8e8b556b4d930cc12ff195e18/scipy-1.18.1-cp313-cp313-win_amd64.whl", hash = "sha256:559ed65f60c1af5a03f3912605a1b5114f522c7c32fb23c3376ae8f03219fe28", size = 36622841, upload-time = "2026-08-21T23:25:18.722Z" }, + { url = "https://files.pythonhosted.org/packages/50/a8/6a77f5f267c555108f0a864b6db714363dab567a8266422a79a385f9232b/scipy-1.18.1-cp313-cp313-win_arm64.whl", hash = "sha256:cd479fc04dd9401e3b4f49e76518768ef99c4f517a98c284eb091fd725719adf", size = 24399315, upload-time = "2026-08-21T23:25:23.458Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/d8eb4e280ddb56a4ab2c6f02ee49b56b23f6e977cf0802fd6d68dbef14f5/scipy-1.18.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:83de5453a7799afc9048b4616bd085cef126e36412f0ea2f6370c36a2a3a51e7", size = 31090936, upload-time = "2026-08-21T23:25:28.686Z" }, + { url = "https://files.pythonhosted.org/packages/2a/49/59ea385dc3a62ff498ddf3cfff7c2b41b0f9f9d3c4122b3f1dcb6d6327fe/scipy-1.18.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:9554bcc6d715ee87a633a3cc8e7703c6628b100dd29cb8a2efc4c0533c7ff729", size = 28725221, upload-time = "2026-08-21T23:25:33.244Z" }, + { url = "https://files.pythonhosted.org/packages/70/e8/6b0c288c50942d78193696c9f15f9a0874f5178aa0ddf40f83d9924b3e8d/scipy-1.18.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:011413b7426b75012840e35649e00fe0a2c3bae89fed433876e3a99251572efc", size = 20466839, upload-time = "2026-08-21T23:25:37.516Z" }, + { url = "https://files.pythonhosted.org/packages/4b/e0/54fd3793c729e3b936782f181b59cbb1205bf250ab605a16cb1ba61cdd5e/scipy-1.18.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:88f0e784020649f88ea48c9f5ddfa403bf9205820667c0914740b392035afb82", size = 23089121, upload-time = "2026-08-21T23:25:42.019Z" }, + { url = "https://files.pythonhosted.org/packages/0b/56/030af62bea3cf878e0028515dff78c123b01633606a879b63f42d2db99cc/scipy-1.18.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d3ab0e8c69a17dd3559eab8cbb88f258e285c94d572c2719033f90f83290c89", size = 34053851, upload-time = "2026-08-21T23:25:47.998Z" }, + { url = "https://files.pythonhosted.org/packages/6b/89/2a844506d49651e9aa1af6ef95b6bd8031cb1d5a4375edec6155037e04cf/scipy-1.18.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac0333bdf38309aa3dcbe7e3fa7ea29e7a2c37c6ea306a757b700ded8e4596ad", size = 35329183, upload-time = "2026-08-21T23:25:53.522Z" }, + { url = "https://files.pythonhosted.org/packages/eb/56/c7370c3640e92ac9613cbf26cb3f729f9b12ddf1727b55b94b53b24d6f48/scipy-1.18.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:911de823097db8b63f034299d12662db93344e6ffa0b881cbb57748974b70168", size = 35672551, upload-time = "2026-08-21T23:25:59.387Z" }, + { url = "https://files.pythonhosted.org/packages/24/16/ec8536f351421f8bf60a1120930638f83790f4710b8230446aca3d6159d4/scipy-1.18.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:95298364e251be3e60249facbeeca03631d3bb7584f85879516ec55ac717b81f", size = 37469416, upload-time = "2026-08-21T23:26:05.432Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/d73da0d28f16c45bb9b0a5691b91610b0275c5ef0eb5e43c87cf2dc1bf31/scipy-1.18.1-cp314-cp314-win_amd64.whl", hash = "sha256:78a0d7c918e74a232394117160e7e3db503377572a45bcef8826e4ab8a35feba", size = 37362755, upload-time = "2026-08-21T23:26:11.366Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/e996e4dc74e10e227b1e14db5eaf6608bb6dd33884a64851c38f18dd4249/scipy-1.18.1-cp314-cp314-win_arm64.whl", hash = "sha256:cbf38d043c1aa4ab306e1ada6ab6eddacc3322a20b7af1b30bc93254b366fe09", size = 25036090, upload-time = "2026-08-21T23:26:15.887Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c9/c00213f92309d753b48903e6a451b87eb52ff5b7a16e789d1568bbf221c4/scipy-1.18.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0fcb3c93519f27bb4f0c4b0f7802cdcaca7fcf93267b75edda2e9f4e8a55cbd7", size = 31485550, upload-time = "2026-08-21T23:26:20.776Z" }, + { url = "https://files.pythonhosted.org/packages/74/b2/e3067c487982d4eeab2938928529410370c06fea84a4d3f4925e7d96647d/scipy-1.18.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:ddef79fb382df40104a19bb7151b3b23e57c1778fcf857c71ceecd9bd264513f", size = 29174642, upload-time = "2026-08-21T23:26:25.395Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ab/374c9fe2d1ec014e576c781a4b5d8e1ba340e8f6b4638c16f711d2b194f0/scipy-1.18.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0e82073ecc7acc6436fac4b31674109c7e1d3e596789767eda01258a8c9e8123", size = 20916357, upload-time = "2026-08-21T23:26:30.112Z" }, + { url = "https://files.pythonhosted.org/packages/90/38/223915c88a17317cafbf8ca2a42b11c265a9fb1e804aa665544132b5fe8a/scipy-1.18.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:8bcf3c1ba5d6456e2effd30fcbd3459b044d683fcdac79a2e6830f0bdf7de487", size = 23482611, upload-time = "2026-08-21T23:26:34.846Z" }, + { url = "https://files.pythonhosted.org/packages/c4/d1/db0948da8ca57a80b36520ef0a768b967d99f3af65f4b6f1bf6362ad4dd4/scipy-1.18.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cfbf154f2ba187f2ed6cce2639efff7d105f1140573642c0161615b6d91d6a87", size = 34143202, upload-time = "2026-08-21T23:26:40.4Z" }, + { url = "https://files.pythonhosted.org/packages/87/53/39d046cc7574ed6acacb6bd5723e220107ece80bff12faaf3efc4ddeede4/scipy-1.18.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a1d33a7836f7ddc1993427966a0823468ec41bcbdb1a9f9942d1d7e57f803ba3", size = 35380876, upload-time = "2026-08-21T23:26:46.1Z" }, + { url = "https://files.pythonhosted.org/packages/f9/da/32e0e799d875a85ca57d9bde6c78148afcc0e38276df683d95854eadc8c3/scipy-1.18.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f4b8bc363b6d65ee2152bec57568e3c52639bb34c46057b09857a307ed5e21d", size = 35770885, upload-time = "2026-08-21T23:26:51.533Z" }, + { url = "https://files.pythonhosted.org/packages/88/2e/f97a666d362fee68b18f41c9c30ed502ca5c98b549749bfcb52a8b74d1eb/scipy-1.18.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:11c423f1049c5755ad4409af52a9ada1cff96fe9b50795d4af3619f292901239", size = 37525424, upload-time = "2026-08-21T23:26:56.751Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d5/a9e765a84654ebba8479a1fd1b059ced1af72b168a3b2a3a46540ea38d20/scipy-1.18.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c24acac1e18912761c4700239bbc1fd32f615af690f1584d49b35859be51324d", size = 37416961, upload-time = "2026-08-21T23:27:01.546Z" }, + { url = "https://files.pythonhosted.org/packages/ee/16/e79e0d1c63ef698879d85439d37e9fb434e3b804e506a6991038d086ebd9/scipy-1.18.1-cp314-cp314t-win_arm64.whl", hash = "sha256:9f2897bf7737392ad0d5213ea7b6add72a4edf5679b3153106aeb88b6507b3b9", size = 25331848, upload-time = "2026-08-21T23:27:05.884Z" }, + { url = "https://files.pythonhosted.org/packages/be/4f/1bd37c883b67163e2ca1f60977a399500e6879c15defecac62831c8d078d/scipy-1.18.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:eb0dfcf4e28a99c12c999744a2ff67c9b06200e20401c7c88186e33552a46331", size = 31091484, upload-time = "2026-08-21T23:27:11.051Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c5/ba929d7feb9b2332f96827c12e0e924b61973b59b4dea383b603372c65ce/scipy-1.18.1-cp315-cp315-macosx_12_0_arm64.whl", hash = "sha256:30f464bee641fa8e282577c7dce027308403213c6ca8270bba73285c91024bc5", size = 28725057, upload-time = "2026-08-21T23:27:15.9Z" }, + { url = "https://files.pythonhosted.org/packages/a4/19/68f1c50f609d955d230e66d25d02bd3e1e167ec540232135354fb9a4b9e3/scipy-1.18.1-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:1bca3b943fc2567ea49cd02c99abde49da4d5178ec46f624bd8255cda8755beb", size = 20466734, upload-time = "2026-08-21T23:27:20.044Z" }, + { url = "https://files.pythonhosted.org/packages/ef/6d/319fa29b73d1802fa80b32a6eaf3f5be456ef81526da2716a9493bcb5501/scipy-1.18.1-cp315-cp315-macosx_14_0_x86_64.whl", hash = "sha256:c9d18a33309122074ea483dd92dd444189166b8b2ec429fe9ed5ac73c7a0aa23", size = 23089664, upload-time = "2026-08-21T23:27:24.345Z" }, + { url = "https://files.pythonhosted.org/packages/b7/db/30992f9b51a63de671daf3888ffd18378b6cb9ec9f2c972264238ffa7fd6/scipy-1.18.1-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82f201b4c878551d48558337aab270d3c6cca5507b8737c8d8a608d234cccde0", size = 34054035, upload-time = "2026-08-21T23:27:29.409Z" }, + { url = "https://files.pythonhosted.org/packages/91/d4/bf3e735dc0b9d5a8ff45079d2540e17d3aff7a2f0048dd8f552ffd031d2b/scipy-1.18.1-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ac49ea97594532dd44b7136094d35f5440fa06e6d9c6384a74c01764df388c5", size = 35333883, upload-time = "2026-08-21T23:27:34.293Z" }, + { url = "https://files.pythonhosted.org/packages/19/93/12d78ce9f871fe945fca588d32644e6e63f553c2a35c564d73f3b22a3313/scipy-1.18.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:ceb30a00ce7c92d459819443d29ca486d882b83fb6738bdcbb2a1cce94ac5daa", size = 35673124, upload-time = "2026-08-21T23:27:39.059Z" }, + { url = "https://files.pythonhosted.org/packages/70/cd/886219313a1012a48e6ae0ec4f302c837151beb92e1ff0d709ef8fdfc488/scipy-1.18.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f29633129f9fa7e88a3f0fca835de2d030bfc9643f7799e1a0c46cee24d38fc7", size = 37470753, upload-time = "2026-08-21T23:27:44.435Z" }, + { url = "https://files.pythonhosted.org/packages/17/6c/a776888ce618bee54fbde26172f0f46ac1da70d27b63861797fe78e1904b/scipy-1.18.1-cp315-cp315-win_amd64.whl", hash = "sha256:92c14f5bdbfb6216315ce33e78080474082de8b3830122ba97809bfbe65f75c0", size = 37361483, upload-time = "2026-08-21T23:27:49.334Z" }, + { url = "https://files.pythonhosted.org/packages/ab/09/97b651691322ebee97999b017ffc18a15a0b815103844c97e8da9d469731/scipy-1.18.1-cp315-cp315-win_arm64.whl", hash = "sha256:e402cf31eb68f453dbb2d36fc6d722b33f24a55d68b2ae1d92fa6305ca71c298", size = 25035883, upload-time = "2026-08-21T23:27:53.596Z" }, + { url = "https://files.pythonhosted.org/packages/ed/0f/9ec20467bbabd0d44e2a77d0fd3d124f884b4d67df92af82c91d2d6a486f/scipy-1.18.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:2a0b02f9fc46f8520330c23d45e6560db7e3a0d927232139427637f98943e11d", size = 31474926, upload-time = "2026-08-21T23:27:57.993Z" }, + { url = "https://files.pythonhosted.org/packages/8a/58/dcb79161e56efbedc50079fcd2f5fe427a0ebb53022eb476aa73c015ad8f/scipy-1.18.1-cp315-cp315t-macosx_12_0_arm64.whl", hash = "sha256:1d73131e358976663dd969e1fb4ed1404b815cd977eaaedc3b3a133ba2d81c35", size = 29164940, upload-time = "2026-08-21T23:28:03.062Z" }, + { url = "https://files.pythonhosted.org/packages/71/d3/1eeea80c817fcb8ef7bd4a05a58824977a0e57a375cfc3d7ea7c911c01ad/scipy-1.18.1-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:bff0b729edd992766136b34e39cc76bc2fad905aa58897ee72a9cd000a6d8443", size = 20906742, upload-time = "2026-08-21T23:28:07.642Z" }, + { url = "https://files.pythonhosted.org/packages/54/46/e59350428b6099301a20128108c995e2eb175a43f383af9a346e38824f9b/scipy-1.18.1-cp315-cp315t-macosx_14_0_x86_64.whl", hash = "sha256:10ac20c69d880f77f375db44c22e3e6a644f9fefa291d4cd2fb9790a89fc99fd", size = 23472183, upload-time = "2026-08-21T23:28:12.109Z" }, + { url = "https://files.pythonhosted.org/packages/89/31/cc91623fa98f0621766a0f0aaaadb2c66de74a7ea7e3837164f6e4354260/scipy-1.18.1-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33a834464fdabc0f26a45508df31b3cc5d028e04dbf6c5ed398541418e0a12fe", size = 34130796, upload-time = "2026-08-21T23:28:17.906Z" }, + { url = "https://files.pythonhosted.org/packages/fc/3e/8572ef536957ddb8aa81bb4090d9e25f257e3b4e05d97deb54319deb8a3a/scipy-1.18.1-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49023963c193dacee096301452f223ee24d86ec5807f8df93c0f7221d119e305", size = 35374253, upload-time = "2026-08-21T23:28:23.732Z" }, + { url = "https://files.pythonhosted.org/packages/b5/c6/59fdeffb4f1435299f93d9dc8140b43ad2916e6cfc944be6c3041fcec86d/scipy-1.18.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:d84a09d0dad90ba6525d8ac1c2334b33e64bf3ccfe9e841f02feb867a22681e4", size = 35758543, upload-time = "2026-08-21T23:28:29.431Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d9/135be205d9de8783193aff9cc3bf483a03a38e4b29432c954e8cb66ac14e/scipy-1.18.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:179ce34a8d0fe273d8883ba59e17e052247d08973dfcb743ca52bb1cce2d60b0", size = 37521946, upload-time = "2026-08-21T23:28:35.245Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a2/5b7d5270621ab7cfa3f7766067bf95dc360b5efb6394694e8143b4156e2b/scipy-1.18.1-cp315-cp315t-win_amd64.whl", hash = "sha256:5632e3ae3d09197c446310cd5187de63e28448ce22f0f67b2b93d97503c0c230", size = 37408295, upload-time = "2026-08-21T23:28:40.724Z" }, + { url = "https://files.pythonhosted.org/packages/63/ad/741c19fcb66755ff953daf9243af8480e4bf3d7fbe57583c178c7d2b6b51/scipy-1.18.1-cp315-cp315t-win_arm64.whl", hash = "sha256:eda632a7981f69730d6281f451db9c1c370993a2c0d7ddb43e2a809a2862b83a", size = 25319710, upload-time = "2026-08-21T23:28:45.713Z" }, ] [[package]] @@ -1796,27 +2204,27 @@ wheels = [ [[package]] name = "ty" -version = "0.0.73" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e5/90/c4e1bb4cead3b644c3e258a27f9b05c7dc5eb0ec96a4f5282194edae9e0d/ty-0.0.73.tar.gz", hash = "sha256:823d4ce0d237bfc7eb6bcee70842f2c0706113813a16951077840743712f4b74", size = 6712739, upload-time = "2026-08-19T03:12:43.381Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/0f/f5e1801e55cc631f2db193276675b30561b963a2403da832bffb5d100267/ty-0.0.73-py3-none-linux_armv6l.whl", hash = "sha256:90a946082bf9bc446b5e72973d9f4ff1222a240b2ca4c9e6eed61eb913e30810", size = 12715452, upload-time = "2026-08-19T03:12:06.673Z" }, - { url = "https://files.pythonhosted.org/packages/54/32/515dd05074c213b433524ab97eb003b0132ae7e358e0d75633ba7a314ed8/ty-0.0.73-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b7d6b5c6a6db7ea95fbbc16af514ef44a27a29a2fe1dc798900790364d170209", size = 12301870, upload-time = "2026-08-19T03:12:08.924Z" }, - { url = "https://files.pythonhosted.org/packages/50/4d/085b4889f0d4bbe4af8b96242d4a1cb209fff95967cfa239ea141983719b/ty-0.0.73-py3-none-macosx_11_0_arm64.whl", hash = "sha256:dd6f657f463e01372d8688f235be164750c8db722c97da27fa4903aa8d40b203", size = 12111741, upload-time = "2026-08-19T03:12:11.067Z" }, - { url = "https://files.pythonhosted.org/packages/95/f6/d6ec277cadfecf03ad4c18551b67c4c6eb7807a0560d801db14be99d7a89/ty-0.0.73-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc2de468e33fd44c9ff1c43473a7316f4289480f5cba8995a67b6d22aee39ca9", size = 12196124, upload-time = "2026-08-19T03:12:13.14Z" }, - { url = "https://files.pythonhosted.org/packages/75/b7/ce78d8707563af9cae9bbd25328bfbc4931035085bd20089adf0c418f70e/ty-0.0.73-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2942fa0ef795a66034cdc8d75a72f453442f3b58ff2f69b4da05b7b954765b55", size = 12488557, upload-time = "2026-08-19T03:12:15.252Z" }, - { url = "https://files.pythonhosted.org/packages/d8/e8/329b9851b23502758c5c98e8cc875ea2a1b4c9674b4ca3a86da56a5063d3/ty-0.0.73-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e0f1ef14f642e18ac4e7a616a2796dcf7a5d82e28cd17f9796494acc7c4aabb", size = 13215606, upload-time = "2026-08-19T03:12:17.225Z" }, - { url = "https://files.pythonhosted.org/packages/36/38/67fedfd2cb77516ef0066b1642f487dba0eb3006493cf3475b15f5b8b228/ty-0.0.73-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16981e15fdceedb37d0aff76c5ac25914595dfee2675af95335550064251ad22", size = 13665497, upload-time = "2026-08-19T03:12:19.286Z" }, - { url = "https://files.pythonhosted.org/packages/8e/b3/154f4dd48ec5eebc186ab4b822c6e62f982fc5ddfd262d6e3903c2acba44/ty-0.0.73-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:644b2bec8a2e2e4957a942ae81d6cff5571c489bb5a8675e4d3886de537a694d", size = 13351231, upload-time = "2026-08-19T03:12:21.353Z" }, - { url = "https://files.pythonhosted.org/packages/35/5f/d462496903fbe453fb76363f8478be929c8e6ff21e6928c57dcd7e5fa21f/ty-0.0.73-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:338d565be3186f50ff8e9d10483685549c2d23f0754485d5ede3b54f4319188a", size = 12782586, upload-time = "2026-08-19T03:12:23.667Z" }, - { url = "https://files.pythonhosted.org/packages/87/52/ec6d24b74abe3ec324204c1c71e6d0c6c76a17ffc15fd51d603b0a302abe/ty-0.0.73-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:11c7b6d839309d2c102cb3a4c03d817176bbfab5b2fccc95a75ec5c9597421c9", size = 13247134, upload-time = "2026-08-19T03:12:25.956Z" }, - { url = "https://files.pythonhosted.org/packages/26/20/cc74650fec56a54786c6d7c89e09576fcad3092be34cf21715d39a406a9b/ty-0.0.73-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:488572db7ff97fb50ea36a76250f2d617c9727d143da6c7bf0623276eb0fc507", size = 12309344, upload-time = "2026-08-19T03:12:28.122Z" }, - { url = "https://files.pythonhosted.org/packages/89/bd/4b0a9087f4315d7fbadf77a3ce44c816cc9ffabed1ced06cc5be81fbc414/ty-0.0.73-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1b958ebceefbbf594e59eb8d3d55bbd033ce634026fcba3e4bc3179e78e45bb7", size = 12502319, upload-time = "2026-08-19T03:12:30.128Z" }, - { url = "https://files.pythonhosted.org/packages/11/80/0a925074911fe111912ea29d9eed309bcc183f43d2fb3eef07db056a0beb/ty-0.0.73-py3-none-musllinux_1_2_i686.whl", hash = "sha256:91a32993b3c34e42c3f323ad6c0399cb596bd1c27e9b7f20db7cd64c1067b68e", size = 12753688, upload-time = "2026-08-19T03:12:32.433Z" }, - { url = "https://files.pythonhosted.org/packages/24/6b/aeccaf89efbc2e112bd415340a22e2669ec998aa397242503e747b712ca4/ty-0.0.73-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bab8a19fbf51f479bddb2a12c5fabfe52f918a5590362321ed5d89b44eb62c15", size = 13069050, upload-time = "2026-08-19T03:12:35.398Z" }, - { url = "https://files.pythonhosted.org/packages/d7/3e/eae485fd86c1585943fd4e1746b0757b2da01e2c43136ebe8c686fe1c7f1/ty-0.0.73-py3-none-win32.whl", hash = "sha256:03347a612f0fa020b19bfd8dbd521db6ecc75d377a3e4d4f6e6c2e62871da4cc", size = 12053187, upload-time = "2026-08-19T03:12:37.565Z" }, - { url = "https://files.pythonhosted.org/packages/a7/01/9b8b983786e3ce34924e372e8b76b92b508273ab65c589fc7e88cc03ee17/ty-0.0.73-py3-none-win_amd64.whl", hash = "sha256:cedd05122ded0b5dcc55431a370e974b747f99c41c290a3d2ab8c1867f197519", size = 12693838, upload-time = "2026-08-19T03:12:39.483Z" }, - { url = "https://files.pythonhosted.org/packages/ea/88/25333bbfea6a5dc064371d2002d3d4807db90b84d5448f9106b2712b0fbc/ty-0.0.73-py3-none-win_arm64.whl", hash = "sha256:e47068f8369dea5d641a26a2ad0a947a320b02ff87099b07e95de0323245a4dc", size = 12443573, upload-time = "2026-08-19T03:12:41.449Z" }, +version = "0.0.74" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/0f/c767853e88567a2ec7e996dd95e3105b1bc62c95d103689311ef0f4a603c/ty-0.0.74.tar.gz", hash = "sha256:da14344fc8625fc9ff359bafb856ad575636ea86d9bb6a629b146bff27b380e6", size = 6786318, upload-time = "2026-08-22T15:05:54.054Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/95/6ded58bc97885c6d88fa1f9cd815031489200738f961cbf0466663213f80/ty-0.0.74-py3-none-linux_armv6l.whl", hash = "sha256:8969ef4e508debf00cf58f9ea85a539f799b1732c59cdfcecd037630b9755b30", size = 12790043, upload-time = "2026-08-22T15:05:05.015Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8a/5e323603b6ab8731144421877ee8a0f8ac5a5511e67857127caa09f6730e/ty-0.0.74-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:51fb6cf5b98e1e1140825b2430943f78d744876a735231656eafbb4c3f7eca3c", size = 12371748, upload-time = "2026-08-22T15:05:08.609Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/ee72e08cb705281e8d8c42917dd577aa598a8a098008495fda5176ee3f6e/ty-0.0.74-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8ebe60b1f0a948c793d6c77fc9e9ddda599e4f023c04ab16e8e03bcb428c3fa0", size = 12282403, upload-time = "2026-08-22T15:05:11.448Z" }, + { url = "https://files.pythonhosted.org/packages/da/b3/fd935b694ff68bc278af50f7ad04770b36ce6306399baef7e1847b553a9d/ty-0.0.74-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa97f407a695c890a53615966a663c7d2167e2cabe88db7ca1a24d62635cdfc8", size = 12345164, upload-time = "2026-08-22T15:05:14.19Z" }, + { url = "https://files.pythonhosted.org/packages/54/5c/5b5825268e029ebb164c909780103dbbae367f069801410068bf1cef29b3/ty-0.0.74-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:673ddb733d4a0db31385ba1ed9ff1f6bd9dc5565413ce57b1ca5ac4c7803da5d", size = 12556646, upload-time = "2026-08-22T15:05:16.994Z" }, + { url = "https://files.pythonhosted.org/packages/56/e7/515914e571d62ce0101744fed3f881936eeb1b30dc37beb72b4f7ca1e289/ty-0.0.74-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1028e7c6b4f6145e9704552f43a5fffdcd51b42263ffdcd9c9677762bc395a4a", size = 13311653, upload-time = "2026-08-22T15:05:20.254Z" }, + { url = "https://files.pythonhosted.org/packages/b0/07/d1452babb6f9266c2122cabc095180b70ed306fb770b2996753814d2237d/ty-0.0.74-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79841a8890493021fb308772474983316eb91f7b56cb227a6a05a06b262a36f0", size = 13768284, upload-time = "2026-08-22T15:05:23.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/60/8d4a2fc7842a47210a1cb0a16a187d9de39ad5d509a00fb74c1c073afcde/ty-0.0.74-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94859d321f3c6a6c8f7bfc3f40e8319cda7e6e012e613440f3dfd145d5010e2e", size = 13422306, upload-time = "2026-08-22T15:05:26.248Z" }, + { url = "https://files.pythonhosted.org/packages/de/76/ebbc269a8c4efcc4d44624993bd188145f20d60ebda9680b15aaec42cc50/ty-0.0.74-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:970a8b2c09ff3be04c8a1c6767332d861be4fce85efe7bb205e4ade7c8655274", size = 12970637, upload-time = "2026-08-22T15:05:29.15Z" }, + { url = "https://files.pythonhosted.org/packages/9e/dd/b99f7236acbf856780ca1779a48143d2d9f2c24d7f531a0ce15a022b8a87/ty-0.0.74-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:795f763b3ded85574c2c2846a6fb8acf2aa76e9e83d761143e92b1f0c7ffa2cd", size = 13344891, upload-time = "2026-08-22T15:05:32.033Z" }, + { url = "https://files.pythonhosted.org/packages/0b/d7/9ff7449a4c7e6428f2c6f298e74cf24b70668f29d45c249507a723ff3782/ty-0.0.74-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:dc086db5367d912c31c0cc872deb7387290e779a4b9b54fcb944673a7cd52c7b", size = 12395272, upload-time = "2026-08-22T15:05:34.702Z" }, + { url = "https://files.pythonhosted.org/packages/b1/dd/b23a5b6b35d37df89dc8dc5daa09efd9245a668b50c4c81c25de21567dc1/ty-0.0.74-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0314d7b391cf684e47c2fa093d2ce4c597cfc9b01d9a315fe204aed6359b271b", size = 12573079, upload-time = "2026-08-22T15:05:37.683Z" }, + { url = "https://files.pythonhosted.org/packages/23/c5/ccba16239d6129533c8b3603458d0f4dd2ba69478e47059073968e74261d/ty-0.0.74-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c4a45dd2e991e8bdae82ba78c8cd051b253f60bc71a6536598fa3ef580b4fc9b", size = 12832506, upload-time = "2026-08-22T15:05:40.505Z" }, + { url = "https://files.pythonhosted.org/packages/6d/1c/2390912634dff4f341f97b397f2aee341ff062be0a66cda37d59375454f2/ty-0.0.74-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:210e2eac6b018fb934e2b8dac3956a0ba076a3fb1fa6f135058c825e5b759b81", size = 13154752, upload-time = "2026-08-22T15:05:43.355Z" }, + { url = "https://files.pythonhosted.org/packages/c4/33/a8c12188227e6f74f91853a7374e01ed81d6ad21c16c8b70e92dbebfe46a/ty-0.0.74-py3-none-win32.whl", hash = "sha256:db0bb6a8f098ef9bd1be861f73b4f7c0320d40d4c05c7ae0a8677d4e7aa4f6e5", size = 12130002, upload-time = "2026-08-22T15:05:46.058Z" }, + { url = "https://files.pythonhosted.org/packages/21/5c/064f28ccb9c234cfce5a2f7aa69a256663d5ae5bb0290b3a9706cc4d1e4c/ty-0.0.74-py3-none-win_amd64.whl", hash = "sha256:bebff181515255b3c78bd2e7693ae66fab6064ad4feea2065c68bc01022aa678", size = 12771435, upload-time = "2026-08-22T15:05:48.811Z" }, + { url = "https://files.pythonhosted.org/packages/fe/06/d6becdaca0315346c26b6df97cb0eafa81de4f870945d6989e88704374ed/ty-0.0.74-py3-none-win_arm64.whl", hash = "sha256:1a3469eaaf8c85b1c0a15bede25d36daea4b09fce1d913e965b24e24b3f1d6c6", size = 12558299, upload-time = "2026-08-22T15:05:51.543Z" }, ] [[package]] @@ -1836,3 +2244,36 @@ sdist = { url = "https://files.pythonhosted.org/packages/92/ff/5a28bdfd8c3ebec42 wheels = [ { url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" }, ] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +] From 5defd6cbdd03ef37b1d85371dc6c8fafd76de6a9 Mon Sep 17 00:00:00 2001 From: Aaron Meyer Date: Sun, 23 Aug 2026 20:25:26 -0700 Subject: [PATCH 2/2] Remove unused panflute and mygene dependencies Neither package is referenced anywhere in the codebase; dropping them also removes their transitive dependencies (anyio, httpx, httpcore, h11, biothings-client) from the lockfile. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01CQsZJTWRf2sCC9hVH5Zv2Y --- pyproject.toml | 2 - uv.lock | 100 ------------------------------------------------- 2 files changed, 102 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 14405d61..0a12bbfa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,13 +12,11 @@ dependencies = [ "seaborn>=0.13.2", "svgutils>=0.3.4", "pandas>=3", - "panflute>=2.3.1", "biopython>=1.85", "scikit-learn>=1.5", "bioinfokit>=2.1.4", "statsmodels>=0.14.4", "fancyimpute>=0.7.0", - "mygene>=3.2.2", "logomaker>=0.8.6", ] diff --git a/uv.lock b/uv.lock index b4163249..3f917894 100644 --- a/uv.lock +++ b/uv.lock @@ -24,19 +24,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b8/2c/897bdd17b05724c894a5b831c6b2e9853adcc2d07a70d6246c0cd5cd3912/adjusttext-1.4.0-py3-none-any.whl", hash = "sha256:6febd6484c0d45c39a22f44b2c1f4a8cd01ef58fada565cab4b629c771df79b5", size = 13262, upload-time = "2026-06-08T16:48:31.765Z" }, ] -[[package]] -name = "anyio" -version = "4.14.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, -] - [[package]] name = "babel" version = "2.18.0" @@ -119,18 +106,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2b/4a/b2dd85241aa8f5fd6829a14dfc7f856364ea0b2ef4278b3193ebcf03db69/biopython-1.88-cp315-cp315t-win_amd64.whl", hash = "sha256:abeda1d57624b82476be498259dbc4531a1957bb1d77c80fd986adaa44f1b406", size = 2751865, upload-time = "2026-08-12T19:30:04.566Z" }, ] -[[package]] -name = "biothings-client" -version = "0.5.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "httpx" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ee/21/73de534c2e1903d58613be872368c8319239d3fa723edd4402f089a81abd/biothings_client-0.5.1.tar.gz", hash = "sha256:4ee3a64e278dc0483aae9c85faef88296534a955543d5291a9d528c5164a8506", size = 61589, upload-time = "2026-07-23T22:14:30.624Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/df/f8fe522363990cc3b588d4dafd7601545ca5c3a7b452afc1a79f7802ba2f/biothings_client-0.5.1-py3-none-any.whl", hash = "sha256:8ca9e23887016eabda10054c3fcd1a2482271afac2e58e8399c5a6f841f1735d", size = 52076, upload-time = "2026-07-23T22:14:29.417Z" }, -] - [[package]] name = "certifi" version = "2026.7.22" @@ -652,9 +627,7 @@ dependencies = [ { name = "fancyimpute" }, { name = "logomaker" }, { name = "matplotlib" }, - { name = "mygene" }, { name = "pandas" }, - { name = "panflute" }, { name = "scikit-learn" }, { name = "scipy" }, { name = "seaborn" }, @@ -682,9 +655,7 @@ requires-dist = [ { name = "fancyimpute", specifier = ">=0.7.0" }, { name = "logomaker", specifier = ">=0.8.6" }, { name = "matplotlib", specifier = ">=3.10" }, - { name = "mygene", specifier = ">=3.2.2" }, { name = "pandas", specifier = ">=3" }, - { name = "panflute", specifier = ">=2.3.1" }, { name = "scikit-learn", specifier = ">=1.5" }, { name = "scipy", specifier = ">=1.16" }, { name = "seaborn", specifier = ">=0.13.2" }, @@ -781,15 +752,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f6/b6/f65ac785d4ac90dcf7c831ac6256f5dd4a19780f4e1575b2c0d6eeebe319/griffelib-2.2.0-py3-none-any.whl", hash = "sha256:d71c3bc2bbed9f958488634fe788b843a9f705d6d2838ca32cd6c25eeb64dfc4", size = 166779, upload-time = "2026-08-16T14:04:54.365Z" }, ] -[[package]] -name = "h11" -version = "0.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, -] - [[package]] name = "highspy" version = "1.15.1" @@ -831,34 +793,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bf/97/e85d751aaba8231e86915077532fd584711d30aa9eb85c26331e2bd87596/highspy-1.15.1-cp314-cp314-win_amd64.whl", hash = "sha256:864258c59aeaea9d3bd7ccdd10c03258e2be764e2cf1e21f829fd1f8d8c15d57", size = 2813851, upload-time = "2026-07-02T12:03:01.836Z" }, ] -[[package]] -name = "httpcore" -version = "1.0.9" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, -] - -[[package]] -name = "httpx" -version = "0.28.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "certifi" }, - { name = "httpcore" }, - { name = "idna" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, -] - [[package]] name = "idna" version = "3.19" @@ -1383,18 +1317,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b2/6d/77546d8c26f038fce314a507106954f76270f6c182488bcf9ac9721175df/mkdocstrings_python-2.0.7-py3-none-any.whl", hash = "sha256:1fce5fbfe4ffa6e8136a35351cdc97c3bf55219c7efbd3f92a82260f93235d60", size = 105387, upload-time = "2026-08-17T16:56:16.813Z" }, ] -[[package]] -name = "mygene" -version = "3.2.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "biothings-client" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0a/ec/a256003f84196aa3fdd65a7c6f5adfc0688398fb66442eba75b39c9b7627/mygene-3.2.2.tar.gz", hash = "sha256:e729cabbc28cf5afb221bca1ab637883b375cb1a3e2f067587ec79f71affdaea", size = 5399, upload-time = "2021-04-05T21:24:30.934Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/b7/132b1673c0ec00881d49d56c09624942fa0ebd2fc21d73d80647efa082e9/mygene-3.2.2-py2.py3-none-any.whl", hash = "sha256:18d85d1b28ecee2be31d844607fb0c5f7d7c58573278432df819ee2a5e88fe46", size = 5357, upload-time = "2021-04-05T21:24:29.07Z" }, -] - [[package]] name = "narwhals" version = "2.25.0" @@ -1585,19 +1507,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/cf/6a51b2c38980e04c279fd2fa908a1b0982064e860444acfca4ec2e2c8359/pandas-3.0.5-cp314-cp314t-win_arm64.whl", hash = "sha256:3c5015fd1730fbf883647e88068176c839c102cea883ba1769a6f4593bfc1f8c", size = 9509776, upload-time = "2026-07-22T22:19:26.694Z" }, ] -[[package]] -name = "panflute" -version = "2.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b7/91/b659923bea127e51bc1b279c3afb3724cae116893ddba7ae498a97496693/panflute-2.3.1.tar.gz", hash = "sha256:5f1bd02a34ef3982ee025ec5b58fb3a6eedfc31d994b8ae39d8dc9915a2d8f1f", size = 44829, upload-time = "2024-03-20T05:47:41.374Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/dd/49f5d0aa985c14218da8759ec4a93390b683c92fd9081d5b96383d48db6e/panflute-2.3.1-py3-none-any.whl", hash = "sha256:e44afd875b7b17ffebbbe58282849df06d9f1b20a45a2f933cd51bdcf4e89130", size = 36714, upload-time = "2024-03-20T05:47:38.375Z" }, -] - [[package]] name = "pathspec" version = "1.1.1" @@ -2227,15 +2136,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fe/06/d6becdaca0315346c26b6df97cb0eafa81de4f870945d6989e88704374ed/ty-0.0.74-py3-none-win_arm64.whl", hash = "sha256:1a3469eaaf8c85b1c0a15bede25d36daea4b09fce1d913e965b24e24b3f1d6c6", size = 12558299, upload-time = "2026-08-22T15:05:51.543Z" }, ] -[[package]] -name = "typing-extensions" -version = "4.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, -] - [[package]] name = "tzdata" version = "2026.3"