From b94c7300bcd29a0a7476a96e2da504fd8b1ba48d Mon Sep 17 00:00:00 2001 From: Wei Xiao <11197323+wxiao0421@users.noreply.github.com> Date: Mon, 3 Aug 2026 16:29:01 -0700 Subject: [PATCH 1/7] Reuse observation embeddings across a sweep, and guard result columns Three related changes for 2.0.0. run_grid_search re-encoded the observation texts on every scoring pass, even though an observation's embedding depends on the encoder and never on the index it is scored against. subsample() shares the parent's sentence model and encoding kwargs, so one set of embeddings is valid for a whole sweep. calculate_rare_class_affinity now accepts sample_embeddings, simulation gains encode_observations(), and run_grid_search hoists the encoding out of its loops. On a 2x2x3 sweep over 320 observations that is 2.99s to 0.52s, a 5.7x saving, with byte-identical rows. Kept behind cache_observation_embeddings so callers short of memory can opt out, and indices that cannot pre-encode fall back rather than failing, which keeps the harness usable with the duck-typed doubles it advertises support for. Result rows are plain dicts so they drop into a DataFrame, but that also means a second write to a key silently destroys the first - which is exactly how an index size once replaced the positive-group count. The grid-search columns are now named constants applied through _add_columns, which raises instead of overwriting. run_grid_search's arguments were ordered by the accident of when each was added, leaving the two index axes separated from the other sweep axes by unrelated plumbing. They are all keyword-only, so regrouping them by role breaks nothing; they now read in the same order as the cost-ordered loop the docstring describes. On SentinelLocalIndex the opposite was true: everything was positional-or-keyword, so a new argument could only be appended. sample_embeddings belongs beside text_samples, so the parameters after it become keyword-only, as do those of from_texts and load. Every call site in the repo already used keywords. Also converts __init__.py from tabs to spaces, the only such file in the repo and the source of all 29 W191 warnings. Co-authored-by: Cursor --- pyproject.toml | 2 +- src/sentinel/__init__.py | 66 +++++---- src/sentinel/sentinel_local_index.py | 46 ++++-- src/sentinel/simulation.py | 192 +++++++++++++++++++++---- tests/test_from_texts.py | 36 +++++ tests/test_simulation.py | 203 +++++++++++++++++++++++++++ 6 files changed, 477 insertions(+), 68 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 88c5a65..c00ccae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "sentinel" -version = "1.0.0" +version = "2.0.0" description = "A Python library for local, contrastive semantic scoring of text to identify affinity towards predefined critical harm patterns." authors = ["Younes Abouelnagah "] readme = "README.md" diff --git a/src/sentinel/__init__.py b/src/sentinel/__init__.py index 4b937ba..46964a8 100644 --- a/src/sentinel/__init__.py +++ b/src/sentinel/__init__.py @@ -20,38 +20,46 @@ from sentinel.sentinel_local_index import SentinelLocalIndex from sentinel.score_formulae import ( - calculate_contrastive_score, - skewness, - mean_of_positives, - top_k_mean, - percentile_score, - softmax_weighted_mean, - max_score, + calculate_contrastive_score, + skewness, + mean_of_positives, + top_k_mean, + percentile_score, + softmax_weighted_mean, + max_score, ) from sentinel.simulation import ( - DEFAULT_AGGREGATORS, - LabeledGroup, - GroupObservationScores, - score_groups, - evaluate_groups, - compare_aggregators, - run_grid_search, + DEFAULT_AGGREGATORS, + LabeledGroup, + GroupObservationScores, + encode_observations, + score_groups, + evaluate_groups, + compare_aggregators, + run_grid_search, ) +# Kept in step with the version in pyproject.toml, which is the source of truth for +# packaging. Duplicated here because importing package metadata at runtime fails when +# the library is used straight from a source checkout rather than an installed wheel. +__version__ = "2.0.0" + __all__ = [ - "SentinelLocalIndex", - "calculate_contrastive_score", - "skewness", - "mean_of_positives", - "top_k_mean", - "percentile_score", - "softmax_weighted_mean", - "max_score", - "DEFAULT_AGGREGATORS", - "LabeledGroup", - "GroupObservationScores", - "score_groups", - "evaluate_groups", - "compare_aggregators", - "run_grid_search", + "__version__", + "SentinelLocalIndex", + "calculate_contrastive_score", + "skewness", + "mean_of_positives", + "top_k_mean", + "percentile_score", + "softmax_weighted_mean", + "max_score", + "DEFAULT_AGGREGATORS", + "LabeledGroup", + "GroupObservationScores", + "encode_observations", + "score_groups", + "evaluate_groups", + "compare_aggregators", + "run_grid_search", ] diff --git a/src/sentinel/sentinel_local_index.py b/src/sentinel/sentinel_local_index.py index e101308..978474d 100644 --- a/src/sentinel/sentinel_local_index.py +++ b/src/sentinel/sentinel_local_index.py @@ -369,6 +369,7 @@ def from_texts( cls, positive_texts: List[str], negative_texts: List[str], + *, model_name: str = "all-MiniLM-L6-v2", neg_to_pos_ratio: Optional[float] = None, batch_size: int = 256, @@ -478,6 +479,7 @@ def from_texts( def load( cls, path: str, + *, aws_access_key_id: Optional[str] = None, aws_secret_access_key: Optional[str] = None, negative_to_positive_ratio: Optional[float] = 5.0, @@ -729,6 +731,8 @@ def subsample( def calculate_rare_class_affinity( self, text_samples: List[str], + sample_embeddings: Optional[np.ndarray] = None, + *, top_k: int = 5, similarity_formula: Callable[[List[float], List[float]], float] = calculate_contrastive_score, # Function to aggregate individual scores into an overall affinity score @@ -755,6 +759,12 @@ def calculate_rare_class_affinity( Args: text_samples: List of text strings to evaluate for rare class affinity. + sample_embeddings: Optional pre-computed embeddings for ``text_samples``, in + the same order. Supplying them skips the encoding step, which is the only + expensive part of this method. Embeddings depend on the encoder, not on + the index contents, so one set can be reused across many indices built + from the same model - see + :func:`sentinel.simulation.encode_observations`. top_k: Number of closest neighbors to consider when calculating the score. similarity_formula: Function to calculate individual similarity scores. aggregation_function: Function to aggregate individual scores into an overall score. @@ -769,19 +779,31 @@ def calculate_rare_class_affinity( Returns: RareClassAffinityResult containing both the overall affinity score and individual observation scores for each text sample. + + Raises: + ValueError: If ``sample_embeddings`` is supplied with a different number of + rows than ``text_samples``, which would silently score each observation + against the wrong embedding. """ - # Merge the default encoding kwargs with any additional ones provided - effective_encoding_kwargs = self.encoding_kwargs.copy() - effective_encoding_kwargs["show_progress_bar"] = show_progress_bar - effective_encoding_kwargs.update(encoding_additional_kwargs) - - # Encode the input samples to get their embeddings - # We currently don't support multi-process encoding in this method, because it is meant for online scoring. - # We can add it if needed, probably by just allowing the caller to pass sample embeddings instead of text. - sample_embeddings = self.sentence_model.encode( - text_samples, - **effective_encoding_kwargs, - ) + if sample_embeddings is None: + # Merge the default encoding kwargs with any additional ones provided + effective_encoding_kwargs = self.encoding_kwargs.copy() + effective_encoding_kwargs["show_progress_bar"] = show_progress_bar + effective_encoding_kwargs.update(encoding_additional_kwargs) + + # Encode the input samples to get their embeddings. + # We currently don't support multi-process encoding in this method, because + # it is meant for online scoring. + sample_embeddings = self.sentence_model.encode( + text_samples, + **effective_encoding_kwargs, + ) + elif len(sample_embeddings) != len(text_samples): + raise ValueError( + f"sample_embeddings has {len(sample_embeddings)} rows but text_samples " + f"has {len(text_samples)} entries. Scoring them together would pair each " + f"observation with the wrong embedding, so refusing to continue." + ) # If we need to prevent exact matches (e.g., when scoring examples that are in the index), # request an additional neighbor so we can skip the exact match later diff --git a/src/sentinel/simulation.py b/src/sentinel/simulation.py index 57150be..f77e426 100644 --- a/src/sentinel/simulation.py +++ b/src/sentinel/simulation.py @@ -58,7 +58,7 @@ import logging from dataclasses import dataclass -from typing import TYPE_CHECKING, Callable, Dict, List, Mapping, Optional, Sequence +from typing import TYPE_CHECKING, Any, Callable, Dict, List, Mapping, Optional, Sequence import numpy as np @@ -127,11 +127,122 @@ class GroupObservationScores: observation_scores: np.ndarray +# Columns run_grid_search attaches on top of whatever evaluate_groups returns. Every +# index-describing column is prefixed, because evaluate_groups already returns an +# "n_positive" meaning the number of positive *groups* in the evaluation set; an +# unprefixed index size once overwrote it and silently turned evaluation metadata into +# an index size. _add_columns turns any repeat of that mistake into an error. +COLUMN_TOP_K = "top_k" +COLUMN_INDEX_N_POSITIVE = "index_n_positive" +COLUMN_INDEX_NEG_TO_POS_RATIO = "index_neg_to_pos_ratio" +COLUMN_INDEX_N_POSITIVE_ACTUAL = "index_n_positive_actual" +COLUMN_INDEX_N_NEGATIVE_ACTUAL = "index_n_negative_actual" + +GRID_SEARCH_COLUMNS = ( + COLUMN_TOP_K, + COLUMN_INDEX_N_POSITIVE, + COLUMN_INDEX_NEG_TO_POS_RATIO, + COLUMN_INDEX_N_POSITIVE_ACTUAL, + COLUMN_INDEX_N_NEGATIVE_ACTUAL, +) + + +def _add_columns(row: Dict[str, Any], **columns: Any) -> Dict[str, Any]: + """Add columns to a result row, refusing to overwrite one that already exists. + + Rows are plain dicts so they drop straight into ``pandas.DataFrame``, but that also + means a second write to the same key destroys the first with no error. That is how + an index size once replaced the positive-group count. Raising here turns a silent + data loss into a failing test. + + Args: + row: The row to extend, modified in place. + **columns: Column name to value. + + Returns: + The same row, for convenient chaining. + + Raises: + ValueError: If any column is already present in the row. + """ + clashes = sorted(name for name in columns if name in row) + if clashes: + raise ValueError( + f"Refusing to overwrite existing result column(s): {', '.join(clashes)}. " + f"Two different measurements are competing for one name; rename the new " + f"one rather than letting it replace the old value." + ) + row.update(columns) + return row + + +def encode_observations( + index: "SentinelLocalIndex", + groups: Sequence[LabeledGroup], + *, + show_progress_bar: bool = False, + encoding_additional_kwargs: Optional[Mapping[str, object]] = None, +) -> Dict[str, np.ndarray]: + """Embed every group's observations once, so many scoring passes can share them. + + Encoding is the only expensive step in a simulation, and an observation's embedding + depends on the encoder, never on the index it is scored against. A sweep that varies + index size, ratio or ``top_k`` therefore re-computes identical numbers on every pass + unless the embeddings are hoisted out, which is what this does. + + The result stays valid across every index produced by + :meth:`~sentinel.sentinel_local_index.SentinelLocalIndex.subsample`, because a + subsampled copy shares the parent's sentence model and encoding kwargs. + + Args: + index: A loaded :class:`~sentinel.sentinel_local_index.SentinelLocalIndex`. + groups: The labeled groups whose observations should be embedded. + show_progress_bar: Whether to show the encoder progress bar. + encoding_additional_kwargs: Extra keyword arguments forwarded to the encoder. + + Returns: + Group name to the embeddings of that group's observations, in observation order. + Groups with no observations are omitted. Empty if ``index`` cannot encode on its + own, in which case callers simply fall back to encoding per pass. + """ + if encoding_additional_kwargs is None: + encoding_additional_kwargs = {} + + sentence_model = getattr(index, "sentence_model", None) + if sentence_model is None or not hasattr(index, "encoding_kwargs"): + # This harness deliberately accepts any index-like object, including test + # doubles that only implement calculate_rare_class_affinity. Those cannot be + # pre-encoded, so skip the optimisation rather than refusing to run. + LOG.debug( + "Index exposes no sentence model, so observations cannot be pre-encoded; " + "scoring will encode per pass." + ) + return {} + + encoding_kwargs = dict(index.encoding_kwargs) + encoding_kwargs["show_progress_bar"] = show_progress_bar + encoding_kwargs.update(encoding_additional_kwargs) + + embeddings: Dict[str, np.ndarray] = {} + for group in groups: + observations = list(group.observations) + if not observations: + continue + embeddings[group.name] = sentence_model.encode( + observations, + **encoding_kwargs, + ) + + LOG.info("Encoded observations for %d groups", len(embeddings)) + return embeddings + + def score_groups( index: "SentinelLocalIndex", groups: Sequence[LabeledGroup], *, top_k: int = 5, + observation_embeddings: Optional[Mapping[str, np.ndarray]] = None, show_progress_bar: bool = False, encoding_additional_kwargs: Optional[Mapping[str, object]] = None, ) -> List[GroupObservationScores]: @@ -150,6 +261,10 @@ def score_groups( groups: The labeled groups to score. top_k: Number of nearest neighbors used per observation. Changing this changes the per-observation scores, so it requires re-scoring. + observation_embeddings: Optional embeddings from + :func:`encode_observations`, keyed by group name. Supplying them skips the + encoding step, which is what makes repeated scoring passes cheap. A group + missing from the mapping is encoded normally. show_progress_bar: Whether to show the encoder progress bar. encoding_additional_kwargs: Extra keyword arguments forwarded to the encoder. @@ -177,6 +292,15 @@ def score_groups( # We only need the per-observation scores here, so we disable the # explainability extras for speed and pass a trivial aggregation # function (its output is ignored). + # Only forwarded when there is something to forward, so an index-like object + # that does not know about sample_embeddings keeps working unchanged. + cached = ( + None + if observation_embeddings is None + else observation_embeddings.get(group.name) + ) + cached_kwargs = {} if cached is None else {"sample_embeddings": cached} + result = index.calculate_rare_class_affinity( observations, top_k=top_k, @@ -186,6 +310,7 @@ def score_groups( explain=False, include_neighbors=False, encoding_additional_kwargs=encoding_additional_kwargs, + **cached_kwargs, ) observation_scores = np.asarray( @@ -537,15 +662,19 @@ def run_grid_search( index: "SentinelLocalIndex", groups: Sequence[LabeledGroup], *, + # What to sweep, outermost axis first, mirroring the loop nesting below. + n_positive_values: Optional[Sequence[int]] = None, + neg_to_pos_ratios: Optional[Sequence[float]] = None, top_k_values: Sequence[int] = (5,), min_score_values: Sequence[float] = (0.1,), aggregators: Optional[Mapping[str, Callable[[np.ndarray], float]]] = None, + # How each configuration is judged. top_n: Optional[int] = None, decision_threshold: Optional[float] = None, - show_progress_bar: bool = False, - n_positive_values: Optional[Sequence[int]] = None, - neg_to_pos_ratios: Optional[Sequence[float]] = None, + # Execution. index_seed: Optional[int] = None, + cache_observation_embeddings: bool = True, + show_progress_bar: bool = False, ) -> List[Dict[str, float]]: """Sweep hyperparameters and summarize metrics, returning a flat result table. @@ -563,30 +692,32 @@ def run_grid_search( The returned rows are easy to turn into a ``pandas.DataFrame`` for plotting. - Cost warning: the number of scoring passes is - ``len(n_positive_values) x len(neg_to_pos_ratios) x len(top_k_values)``. A 7x7 - size/ratio grid with three ``top_k`` values is 147 full passes. Note that the - observation texts are re-encoded on every pass even though their embeddings do - not depend on the index at all, so most of that cost is recomputing identical - numbers. Letting callers pass pre-computed sample embeddings would collapse it - to roughly one encoding pass; that is a separate change. + Cost: the sweep runs + ``len(n_positive_values) x len(neg_to_pos_ratios) x len(top_k_values)`` scoring + passes, so a 7x7 size/ratio grid with three ``top_k`` values is 147 of them. The + observations are encoded once up front and reused across all of them, because an + observation's embedding depends on the encoder rather than on the index being + varied. Each pass is then a nearest-neighbour search rather than a re-encode. Args: index: A loaded :class:`~sentinel.sentinel_local_index.SentinelLocalIndex`. groups: The labeled groups to evaluate. + n_positive_values: Index sizes to try, as counts of positive examples. + ``None`` (the default) means one pass using the index exactly as given. + neg_to_pos_ratios: Negative-to-positive ratios to try. ``None`` (the + default) means one pass using the index exactly as given. top_k_values: ``top_k`` values to try (each triggers a re-scoring). min_score_values: Per-observation thresholds to try (evaluated cheaply). aggregators: Name -> function map. Defaults to :data:`DEFAULT_AGGREGATORS`. top_n: Size of the top-N slice for recall@N / precision@N. decision_threshold: Cutoff for the classification family (``None`` = best-F1). - show_progress_bar: Whether to show the encoder progress bar. - n_positive_values: Index sizes to try, as counts of positive examples. - ``None`` (the default) means one pass using the index exactly as given. - neg_to_pos_ratios: Negative-to-positive ratios to try. ``None`` (the - default) means one pass using the index exactly as given. index_seed: Seed for the subsampling, so each index configuration is reproducible across runs. + cache_observation_embeddings: Whether to encode the observations once and reuse + them for every pass. Leave it on unless memory is tight: the cache holds one + embedding per observation, so a very large evaluation set can be sizeable. + show_progress_bar: Whether to show the encoder progress bar. Returns: A list of metric dicts, one per ``(n_positive, neg_to_pos_ratio, top_k, @@ -617,6 +748,14 @@ def run_grid_search( ) total_configurations = len(positive_options) * len(ratio_options) + # Hoisted out of every loop below: subsampling changes which rows the index holds, + # never the encoder, so one set of observation embeddings serves the whole sweep. + cached_embeddings: Optional[Dict[str, np.ndarray]] = None + if cache_observation_embeddings: + cached_embeddings = encode_observations( + index, groups, show_progress_bar=show_progress_bar + ) + rows: List[Dict[str, float]] = [] configuration = 0 for n_positive in positive_options: @@ -659,6 +798,7 @@ def run_grid_search( working_index, groups, top_k=top_k, + observation_embeddings=cached_embeddings, show_progress_bar=show_progress_bar, ) for min_score in min_score_values: @@ -671,16 +811,16 @@ def run_grid_search( top_n=top_n, decision_threshold=decision_threshold, ) - row["top_k"] = int(top_k) - # Every index-describing column is prefixed, because - # evaluate_groups already returns an "n_positive" meaning the - # number of positive *groups* in the evaluation set. Reusing - # that name here overwrote it, silently replacing evaluation - # metadata with an index size. - row["index_n_positive"] = n_positive - row["index_neg_to_pos_ratio"] = ratio - row["index_n_positive_actual"] = n_positive_actual - row["index_n_negative_actual"] = n_negative_actual + _add_columns( + row, + **{ + COLUMN_TOP_K: int(top_k), + COLUMN_INDEX_N_POSITIVE: n_positive, + COLUMN_INDEX_NEG_TO_POS_RATIO: ratio, + COLUMN_INDEX_N_POSITIVE_ACTUAL: n_positive_actual, + COLUMN_INDEX_N_NEGATIVE_ACTUAL: n_negative_actual, + }, + ) rows.append(row) return rows diff --git a/tests/test_from_texts.py b/tests/test_from_texts.py index 0353920..179234e 100644 --- a/tests/test_from_texts.py +++ b/tests/test_from_texts.py @@ -197,6 +197,42 @@ def test_without_a_ratio_every_negative_is_encoded(self, monkeypatch): assert spy.encoded[1] == negatives assert index.negative_corpus == negatives + @pytest.mark.integration + def test_precomputed_embeddings_match_encoding_inline(self): + """Passing embeddings must score identically to letting the index encode. + + This is what lets a sweep encode once and reuse the result: the embeddings + depend on the encoder, not on the index they are scored against. + """ + index = SentinelLocalIndex.from_texts( + positive_texts=self.POSITIVE, + negative_texts=self.NEGATIVE, + model_name="sentence-transformers/all-MiniLM-L6-v2", + ) + texts = ["harmful unsafe behavior", "normal regular activity"] + + inline = index.calculate_rare_class_affinity(texts) + precomputed = index.calculate_rare_class_affinity( + texts, + index.sentence_model.encode(texts, **index.encoding_kwargs), + ) + + assert precomputed.observation_scores == inline.observation_scores + + @pytest.mark.integration + def test_mismatched_embeddings_raise(self): + """Too few embeddings would pair observations with the wrong vectors.""" + index = SentinelLocalIndex.from_texts( + positive_texts=self.POSITIVE, + negative_texts=self.NEGATIVE, + model_name="sentence-transformers/all-MiniLM-L6-v2", + ) + texts = ["one", "two", "three"] + too_few = index.sentence_model.encode(texts[:2], **index.encoding_kwargs) + + with pytest.raises(ValueError, match="sample_embeddings has 2 rows"): + index.calculate_rare_class_affinity(texts, too_few) + @pytest.mark.parametrize( "kwargs,message", [ diff --git a/tests/test_simulation.py b/tests/test_simulation.py index 3256c3c..7ebb707 100644 --- a/tests/test_simulation.py +++ b/tests/test_simulation.py @@ -30,7 +30,9 @@ DEFAULT_AGGREGATORS, GroupObservationScores, LabeledGroup, + _add_columns, compare_aggregators, + encode_observations, evaluate_groups, run_grid_search, score_groups, @@ -297,6 +299,207 @@ def _two_groups(): ] +class _SpyEncoder: + """Sentence model stand-in that counts how many texts it was asked to encode.""" + + def __init__(self): + self.encoded_batches = [] + + def encode(self, texts, **kwargs): + self.encoded_batches.append(list(texts)) + # The observation text doubles as its score elsewhere in these tests, so keep + # the embedding trivially derived from it to stay debuggable. + return np.array([[float(t)] * 4 for t in texts], dtype=float) + + +class _EncodingStubIndex(_StubSubsamplableIndex): + """Stub that can pre-encode, so the caching path can be exercised without a model.""" + + def __init__(self, n_positive=100, n_negative=500, encoder=None): + super().__init__(n_positive, n_negative) + self.sentence_model = encoder if encoder is not None else _SpyEncoder() + self.encoding_kwargs = {"normalize_embeddings": True} + + def calculate_rare_class_affinity(self, text_samples, sample_embeddings=None, **kwargs): + # Record whether this pass was handed cached embeddings, so a test can assert + # the cache is actually reaching the scorer rather than being dropped. + self.calls.append({**kwargs, "used_cache": sample_embeddings is not None}) + if sample_embeddings is None: + self.sentence_model.encode(text_samples) + observation_scores = {text: float(text) for text in text_samples} + return SimpleNamespace(observation_scores=observation_scores) + + def subsample(self, n_positive=None, neg_to_pos_ratio=None, seed=None): + smaller = super().subsample( + n_positive=n_positive, neg_to_pos_ratio=neg_to_pos_ratio, seed=seed + ) + # A real subsample() shares the parent's model, which is exactly why one set of + # observation embeddings stays valid across a whole sweep. + rebuilt = _EncodingStubIndex( + smaller.positive_embeddings.shape[0], + smaller.negative_embeddings.shape[0], + encoder=self.sentence_model, + ) + rebuilt.calls = self.calls + rebuilt.subsample_calls = self.subsample_calls + return rebuilt + + +def _rows_equal(left, right): + """Compare result rows, treating NaN as equal to NaN. + + Some metrics are legitimately NaN on small fixtures (Cohen's d needs spread within + a class), and NaN never equals itself, so a plain ``==`` would report a difference + where none exists. + """ + if len(left) != len(right): + return False + for row_a, row_b in zip(left, right): + if row_a.keys() != row_b.keys(): + return False + for key in row_a: + a, b = row_a[key], row_b[key] + both_nan = ( + isinstance(a, float) + and isinstance(b, float) + and np.isnan(a) + and np.isnan(b) + ) + if not both_nan and a != b: + return False + return True + + +class TestObservationEmbeddingCache: + """Encoding observations once and reusing them across scoring passes.""" + + def test_encoder_runs_once_regardless_of_sweep_size(self): + """The whole point: encoding cost stops scaling with the number of passes. + + An observation's embedding depends on the encoder, never on the index it is + scored against, so a sweep that re-encodes per pass is recomputing identical + numbers. + """ + index = _EncodingStubIndex() + run_grid_search( + index, + _two_groups(), + n_positive_values=[10, 20], + neg_to_pos_ratios=[1.0, 2.0], + top_k_values=[3, 5], + min_score_values=[0.0], + ) + + # 2 sizes x 2 ratios x 2 top_k = 8 scoring passes over 2 groups. + assert len(index.calls) == 8 * len(_two_groups()) + assert all(call["used_cache"] for call in index.calls) + # But only one encode per group, up front. + assert len(index.sentence_model.encoded_batches) == len(_two_groups()) + + def test_disabling_the_cache_encodes_every_pass(self): + """The opt-out still works, for callers who cannot spare the memory.""" + index = _EncodingStubIndex() + run_grid_search( + index, + _two_groups(), + top_k_values=[3, 5], + min_score_values=[0.0], + cache_observation_embeddings=False, + ) + + assert not any(call["used_cache"] for call in index.calls) + assert len(index.sentence_model.encoded_batches) == 2 * len(_two_groups()) + + def test_cached_and_uncached_results_are_identical(self): + """Caching is an optimisation, so it must not change a single number.""" + groups = [ + LabeledGroup(name="pos_a", label=1, observations=["0.9", "0.8"]), + LabeledGroup(name="pos_b", label=1, observations=["0.85", "0.7"]), + LabeledGroup(name="neg_a", label=0, observations=["0.2", "0.1"]), + LabeledGroup(name="neg_b", label=0, observations=["0.15", "0.05"]), + ] + kwargs = dict( + n_positive_values=[10, 20], + neg_to_pos_ratios=[1.0], + top_k_values=[3, 5], + min_score_values=[0.0, 0.1], + index_seed=42, + ) + cached = run_grid_search( + _EncodingStubIndex(), groups, cache_observation_embeddings=True, **kwargs + ) + uncached = run_grid_search( + _EncodingStubIndex(), groups, cache_observation_embeddings=False, **kwargs + ) + + # Guards against the comparison passing vacuously if everything were NaN. + assert cached and all(not np.isnan(row["roc_auc"]) for row in cached) + assert _rows_equal(cached, uncached) + + def test_index_without_an_encoder_still_runs(self): + """Index-like objects that cannot pre-encode fall back instead of failing. + + This harness deliberately accepts test doubles that only implement + calculate_rare_class_affinity, so the optimisation has to be skippable. + """ + index = _StubSubsamplableIndex() # no sentence_model at all + rows = run_grid_search( + index, _two_groups(), top_k_values=[3], min_score_values=[0.0] + ) + + assert encode_observations(index, _two_groups()) == {} + assert len(rows) == len(DEFAULT_AGGREGATORS) + + def test_score_groups_accepts_precomputed_embeddings(self): + """The embeddings can also be reused directly, without a grid search.""" + index = _EncodingStubIndex() + groups = _two_groups() + embeddings = encode_observations(index, groups) + + assert set(embeddings) == {"pos", "neg"} + index.sentence_model.encoded_batches.clear() + + scored = score_groups(index, groups, top_k=3, observation_embeddings=embeddings) + + assert [s.name for s in scored] == ["pos", "neg"] + assert index.sentence_model.encoded_batches == [] + + +class TestResultColumnCollisions: + """A second write to one column used to destroy the first, silently.""" + + def test_adding_an_existing_column_raises(self): + row = {"n_positive": 2} + with pytest.raises(ValueError, match="n_positive"): + _add_columns(row, n_positive=10) + # The original value survives the refusal. + assert row["n_positive"] == 2 + + def test_adding_new_columns_succeeds(self): + row = {"roc_auc": 0.9} + _add_columns(row, top_k=5, index_n_positive=10) + assert row == {"roc_auc": 0.9, "top_k": 5, "index_n_positive": 10} + + def test_rows_still_expand_into_one_column_each(self): + """Rows stay plain dicts so pd.DataFrame(rows) keeps working as documented.""" + pd = pytest.importorskip("pandas") + rows = run_grid_search( + _EncodingStubIndex(), + _two_groups(), + top_k_values=[3], + min_score_values=[0.0], + n_positive_values=[10], + ) + + frame = pd.DataFrame(rows) + assert len(frame) == len(rows) + for column in ("n_positive", "index_n_positive", "index_n_positive_actual"): + assert column in frame.columns + # The evaluation metadata and the index size remain distinct columns. + assert frame["n_positive"].tolist() == [1] * len(rows) + assert frame["index_n_positive"].tolist() == [10] * len(rows) + + def test_grid_search_without_index_axes_never_subsamples(): """The default path must not touch the index at all. From 7e7202a37b4af0134aff9d5b14799d4b36213c52 Mon Sep 17 00:00:00 2001 From: Wei Xiao <11197323+wxiao0421@users.noreply.github.com> Date: Mon, 3 Aug 2026 16:31:56 -0700 Subject: [PATCH 2/7] Document the 2.0 features and how to migrate Adds V2_FEATURES.md covering each addition since 1.0 with what it is, why it exists and how to use it, plus migration notes for the two deliberate breaks: keyword-only arguments after the first, and the index_-prefixed grid-search columns. In the README, replaces the "What's New" section with a 2.0 summary table pointing at the relevant sections, adds a section on subsample(), and documents the index sweep axes and the observation embedding cache with its measured saving. Also separates the two things called "metrics" in the tuning section, since the aggregator is swept while the evaluation metrics are all reported. Co-authored-by: Cursor --- README.md | 73 ++++++++++++++++- V2_FEATURES.md | 219 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 289 insertions(+), 3 deletions(-) create mode 100644 V2_FEATURES.md diff --git a/README.md b/README.md index 7126766..3d72548 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # Sentinel +**Version 2.0** — see [V2_FEATURES.md](V2_FEATURES.md) for what changed and how to migrate. ## Overview @@ -11,9 +12,26 @@ Roblox Sentinel, part of the Roblox Safety Toolkit, is a Python library designed By prioritizing recall over precision, Sentinel serves as a high-recall candidate generator for more thorough investigation. This approach is particularly effective for applications where rare patterns are critical to identify. Rather than treating each message in isolation, Sentinel analyzes patterns across messages to identify concerning behavior. -## What’s New: Aggregation options and Explainability +## What's new in 2.0 -Sentinel now includes multiple aggregation strategies and built‑in explainability to help you tune for your use case and understand why a score was assigned. +Version 1.0 gave you an index and a score. Getting a *good* score still meant +hand-building the index, guessing at hyperparameters, and re-encoding your corpus every +time you wanted to try something different. 2.0 closes that loop: + +| | | +|---|---| +| [`from_texts()`](#creating-a-new-index) | Build an index in one call, instead of an eight-step recipe where two steps fail silently if skipped | +| [`subsample()`](#resizing-an-index) | Resize an index without re-encoding — a 3x3 grid in 9 ms rather than 12.5 s | +| [`sentinel.simulation`](#simulation-based-tuning) | Measure which aggregator and hyperparameters suit your data | +| [Index sweeps](#simulation-based-tuning) | Grid search over index size and negative ratio, encoding the observations only once | +| [Persisted corpus](#storage-options) | Explanations name the matched sentence after a reload, not a row number | +| [Seeded loading](#quick-start) | A saved index behaves identically on every load | + +Full detail and migration notes: [V2_FEATURES.md](V2_FEATURES.md). + +### Aggregation options and explainability + +Sentinel includes multiple aggregation strategies and built‑in explainability to help you tune for your use case and understand why a score was assigned. - Aggregators (in `sentinel.score_formulae`): - `skewness(scores, min_size_of_scores=10)`: default, pattern‑oriented and robust to message count @@ -260,6 +278,23 @@ saved_config = index.save( ) ``` +## Resizing an index + +Encoding a sentence produces the same numbers regardless of which index it ends up in, +so a small index is just a large one with rows removed. `subsample()` copies the rows you +want rather than re-running the model, which is what makes sweeping index size +affordable — on the shipped example index, building a 3x3 grid of configurations takes +about 9 ms against roughly 12.5 s to re-encode the same 16,100 rows. + +```python +smaller = index.subsample(n_positive=1000, neg_to_pos_ratio=5.0, seed=42) +``` + +It returns a new index and never modifies the original, which matters when you loop over +sizes: if each call shrank the receiver, the second run would start from the first run's +leftovers and every later result would be quietly wrong. Corpus texts follow their own +embedding rows, so explanations stay truthful. + ## Testing for optimal Thresholds and data ratio's Usage of the 'examples\Example_Threshold_Script.py' script will allow for quick threshold checks for a variety of ratios, by default these are 10:1, 5:1 and 1:1 ratios. This has predefined example chat logs, and should, show optimal settings for the dataset being used based on an average score and average detection count. @@ -317,12 +352,44 @@ pd.DataFrame(compare_aggregators(scored)) pd.DataFrame(run_grid_search(index, groups, top_k_values=[3, 5, 10], min_score_values=[0.0, 0.1, 0.25])) ``` -Each result row reports three families of evaluation metrics, so you can tune for whatever matters to your use case: +Two different things are called "metrics" here, and keeping them apart helps: + +- The **summarize metric** (the aggregator) is *how* a group's many per-observation scores become one number. This is what you sweep. +- The **evaluation metric** is *how good* that separation turned out to be. You do not pick one — every row reports all three families below, so you can tune for whatever matters to your use case. - Ranking: `roc_auc`, `recall_at_n`, `precision_at_n`, `rank_ratio` (do known positives rank at the top?) - Threshold / classification: `precision`, `recall`, `f1`, `false_positive_rate` at a chosen (or automatically best‑F1) cutoff - Separation / distribution: `mean_separation`, `cohens_d`, `ks_statistic` (threshold‑free) +### Sweeping the index itself + +The grid search can also vary the index, not just `top_k` and the threshold. Index size is +usually the highest-leverage knob, and `subsample()` makes it cheap to explore: + +```python +pd.DataFrame(run_grid_search( + index, groups, + n_positive_values=[1000, 5000, 10000], # index size + neg_to_pos_ratios=[0.2, 1.0, 5.0], # index balance + top_k_values=[3, 5, 10], + index_seed=42, # reproducible index configurations +)) +``` + +The arguments are ordered to match the loop nesting, which is itself ordered by cost: +resizing the index is cheap, re-scoring is not, and thresholds and aggregators are free on +the cached scores. + +**Observations are encoded once for the whole sweep.** An observation's embedding depends +on the encoder, never on the index it is scored against, so re-encoding on every pass was +recomputing identical numbers. On a 2x2x3 sweep over 320 observations this took a measured +run from **2.99 s to 0.52 s, a 5.7x saving**, with byte-identical results. Pass +`cache_observation_embeddings=False` if memory is tight. + +Rows report both the requested and actual index sizes, because a request is clipped when +the index holds fewer examples than you asked for — without `index_n_positive_actual` two +rows can look like a genuine size sweep while describing nearly the same index. + See [examples/sentinel_against_hate.ipynb](examples/sentinel_against_hate.ipynb) for a worked example comparing aggregators on hate‑speech data. ## How It Works diff --git a/V2_FEATURES.md b/V2_FEATURES.md new file mode 100644 index 0000000..eaad908 --- /dev/null +++ b/V2_FEATURES.md @@ -0,0 +1,219 @@ +# What's new in Sentinel 2.0 + +Sentinel 1.0 gave you an index and a score. Getting a *good* score still meant +hand-building the index, guessing at hyperparameters, and re-encoding your corpus every +time you wanted to try something different. + +Version 2.0 is about closing that loop: build an index in one call, resize it without +re-encoding, sweep the settings that matter, and see which text actually drove a score. + +- [Build an index in one call](#build-an-index-in-one-call) +- [Resize an index without re-encoding](#resize-an-index-without-re-encoding) +- [Tune with the simulation harness](#tune-with-the-simulation-harness) +- [Sweep index size and ratio](#sweep-index-size-and-ratio) +- [Explanations survive a reload](#explanations-survive-a-reload) +- [Reproducible loading](#reproducible-loading) +- [Migrating from 1.0](#migrating-from-10) + +## Build an index in one call + +`SentinelLocalIndex.from_texts()` replaces an eight-step recipe, two steps of which fail +*silently* when you skip them: omit `normalize_embeddings=True` and the similarity maths +quietly returns wrong numbers, and omit the corpus and you lose explanations. Neither +raises. Neither warns. + +```python +from sentinel import SentinelLocalIndex + +index = SentinelLocalIndex.from_texts( + positive_texts=["...examples of the rare class..."], + negative_texts=["...examples of ordinary content..."], + neg_to_pos_ratio=5.0, + seed=42, +) +``` + +Surplus negatives are dropped *before* encoding rather than after. Encoding is per-text +and is the only expensive step, so at a 1:1 ratio against 1,000 positives, passing +100,000 negatives no longer means paying to encode 99,000 rows that never reach the index. + +## Resize an index without re-encoding + +Encoding a sentence produces the same numbers regardless of which index it ends up in, so +a small index is just a large one with rows removed. `subsample()` copies the rows you +want instead of re-running the model: + +```python +smaller = index.subsample(n_positive=1000, neg_to_pos_ratio=5.0, seed=42) +``` + +It returns a new index and never mutates the original, which matters when you loop over +sizes: if each call shrank the receiver, run two would start from run one's leftovers and +every later result would be quietly wrong. + +Each side draws from its own seeded generator. Sharing one would couple them, so a +configuration that kept every positive (and therefore drew nothing) would select +different negatives from one that subsampled - two cells meant to differ along one axis +would differ along two. + +## Tune with the simulation harness + +`sentinel.simulation` answers "which settings work best on *my* data?". It is numpy-only, +with no Ray, S3 or experiment trackers, and it separates two things that are easy to +confuse: + +- The **summarize metric** (the aggregator) is how a group's many per-observation scores + become one number. This is what you sweep. +- The **evaluation metric** is how good the resulting separation is. You do not choose + one; every row reports three families, so you can tune for whatever matters. + +Why the summarize metric matters so much: picture a hateful podcast episode with 97 dull +segments and 3 nasty ones, against a normal episode of 100 unremarkable segments. + +| Episode | Segment scores | Average | Maximum | +|---|---|---|---| +| Hateful | 97 x 0.05, 3 x 0.90 | 0.0755 | 0.90 | +| Normal | 100 x 0.06 | 0.06 | 0.08 | + +Averaging drowns the signal; the maximum finds it. But the maximum is fragile - one odd +segment flags an innocent episode - which is why the default is `skewness`, which looks +for a *pattern* of spikes and does not care how long the episode is. + +```python +from sentinel.simulation import LabeledGroup, score_groups, compare_aggregators +import pandas as pd + +groups = [ + LabeledGroup(name="source_a", label=1, observations=[...]), + LabeledGroup(name="source_b", label=0, observations=[...]), +] + +scored = score_groups(index, groups, top_k=5) # expensive, once +pd.DataFrame(compare_aggregators(scored)) # cheap, all six aggregators +``` + +Every row carries all three families: + +- Ranking: `roc_auc`, `recall_at_n`, `precision_at_n`, `rank_ratio` +- Threshold: `precision`, `recall`, `f1`, `false_positive_rate` +- Separation: `mean_separation`, `cohens_d`, `ks_statistic` + +## Sweep index size and ratio + +`run_grid_search` can now sweep the index itself, not just `top_k` and the threshold: + +```python +pd.DataFrame(run_grid_search( + index, groups, + n_positive_values=[1000, 5000, 10000], + neg_to_pos_ratios=[0.2, 1.0, 5.0], + top_k_values=[3, 5, 10], + index_seed=42, +)) +``` + +The arguments are ordered to match the loop nesting, which is itself ordered by cost: + +``` +for n_positive in n_positive_values: # cheap: subsample() + for ratio in neg_to_pos_ratios: # cheap: subsample() + for top_k in top_k_values: # re-scores + for min_score in min_score_values: # cheap + for aggregator in aggregators: # cheap +``` + +**Observations are encoded once for the whole sweep.** An observation's embedding depends +on the encoder, never on the index it is scored against, and a subsampled index shares its +parent's model - so re-encoding per pass was recomputing identical numbers. On a 2x2x3 +sweep over 320 observations this took a real run from **2.99s to 0.52s, a 5.7x saving**, +producing byte-identical rows. The saving grows with the grid, because the one encoding +pass is amortised over more scoring passes. + +Set `cache_observation_embeddings=False` if memory is tight; the cache holds one embedding +per observation. + +Rows report both what you asked for and what you got, because a request is clipped when +the index is smaller than requested: + +``` +index_n_positive=10 -> index_n_positive_actual=10, index_n_negative_actual=20 +index_n_positive=999 -> index_n_positive_actual=15, index_n_negative_actual=30 +``` + +Without the actual counts, those two rows could look like a genuine size sweep when they +describe nearly the same index. + +## Explanations survive a reload + +In 1.0, `save()` wrote only embeddings and config. After any reload the corpus was gone, +so explanations reported the row number of a match instead of the matched sentence. + +2.0 writes a `corpus.json` beside the embeddings. It is written **unconditionally**, with +nulls when there is no corpus, so it can never describe rows from an earlier save to the +same path. Saving without a corpus therefore clears any corpus already there - which is +deliberate, because keeping it is only correct if the new embeddings are the same rows in +the same order, and nothing enforces that. + +Alignment is defended at three points: a mismatched corpus is refused at save time, +discarded with a warning at load time, and carried through row-for-row whenever rows are +dropped. Degrading to row numbers is recoverable; naming the wrong sentence is not. + +Indices saved before this file existed simply lack it and continue to load normally. + +## Reproducible loading + +`load()` downsamples negatives to the requested ratio, and that choice is random. In 1.0 +it was unseeded, so a saved index behaved like a slightly different model on every load - +on the shipped example index, the same text scored 0.028636 on one load and 0.011523 on +the next. + +```python +index = SentinelLocalIndex.load(path="...", seed=42) +``` + +Seeding uses a private generator, so your own randomness is untouched. + +## Migrating from 1.0 + +Most code needs no change. The breaking changes are deliberate and narrow. + +**Arguments after the first are now keyword-only** on `calculate_rare_class_affinity`, +`from_texts` and `load`. This is what lets a new argument sit in a logical place instead +of being appended to an eleven-parameter list forever. + +```python +# 1.0 - still works, but only because the first argument is positional +index.calculate_rare_class_affinity(texts) + +# 1.0 positional style - no longer valid +index.calculate_rare_class_affinity(texts, 10) + +# 2.0 +index.calculate_rare_class_affinity(texts, top_k=10) +``` + +Every call site in this repository already used keywords, and the documentation always +taught that style, so in practice this is unlikely to affect you. + +**Grid-search index columns are prefixed.** `evaluate_groups` returns an `n_positive` +meaning the number of positive *groups* in your evaluation set. The index size briefly +shared that name and silently overwrote it. If you read grid-search output, rename: + +| Old | New | +|---|---| +| `n_positive` (index size) | `index_n_positive` | +| `neg_to_pos_ratio` | `index_neg_to_pos_ratio` | +| `n_positive_actual` | `index_n_positive_actual` | +| `n_negative_actual` | `index_n_negative_actual` | + +`n_positive` now unambiguously means the positive-group count, as `evaluate_groups` +always documented. Writing a column twice raises instead of overwriting. + +**Rows are still plain dicts**, so `pd.DataFrame(rows)` keeps working. + +## Known limitations + +- `top_k` still forces a re-score. Caching the neighbour search would make it as cheap as + the index axes, but that is a deeper change. +- `RareClassAffinityResult.observation_scores` is keyed by observation text, so duplicate + observations within one group collapse into a single entry. From 3ae53976e0e124cd2ad91c6c2defd6b70b0d9ebc Mon Sep 17 00:00:00 2001 From: Wei Xiao <11197323+wxiao0421@users.noreply.github.com> Date: Mon, 3 Aug 2026 16:43:00 -0700 Subject: [PATCH 3/7] Demonstrate the 2.0 features in the hate-speech notebook Appends a self-contained appendix showing from_texts(), the persisted corpus and seeded loading, subsample(), the grid-search index axes, and the observation embedding cache, with executed outputs. Self-contained deliberately: it builds a small index from a dozen example sentences rather than depending on the data-loading cells above, so it runs in seconds on its own. The shipped example index is used only for the subsample demonstration, where a realistic size is the point - it predates corpus support, so its explanations would show row numbers rather than text. Appended rather than woven in, so the diff is 789 insertions and no deletions and the existing 32 cells are byte-identical. The final aggregator comparison reports the whole table rather than picking a winner: all six separate this example perfectly, which says the example is easy, not that the aggregators are equivalent. Sorting by Cohen's d there produced a number like 8.5e15, because the within-class variance is zero on a fixture this clean. The real comparison is the earlier section, on real data. Co-authored-by: Cursor --- examples/sentinel_against_hate.ipynb | 789 +++++++++++++++++++++++++++ 1 file changed, 789 insertions(+) diff --git a/examples/sentinel_against_hate.ipynb b/examples/sentinel_against_hate.ipynb index 5936edc..0f9a8bf 100644 --- a/examples/sentinel_against_hate.ipynb +++ b/examples/sentinel_against_hate.ipynb @@ -2817,6 +2817,795 @@ "\n", "This approach demonstrates why robust hate speech detection requires attention not just to average content characteristics, but to distribution patterns that might reveal occasional but significant harmful content within otherwise unremarkable material." ] + }, + { + "cell_type": "markdown", + "id": "fabe7360", + "metadata": {}, + "source": [ + "# Appendix: What's new in Sentinel 2.0\n", + "\n", + "Everything above was written against Sentinel 1.0, where building and tuning an index\n", + "meant a good deal of manual work. This appendix demonstrates what 2.0 adds.\n", + "\n", + "It is deliberately **self-contained** - it builds its own small index from a handful of\n", + "example sentences and does not depend on the data-loading cells above, so you can run it\n", + "on its own in a few seconds. The example set is tiny so the notebook stays fast; the\n", + "advantages grow with real corpora, where encoding dominates everything else.\n", + "\n", + "Full write-up and migration notes: [V2_FEATURES.md](../V2_FEATURES.md)." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "c0365d4b", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-03T23:41:23.212352Z", + "iopub.status.busy": "2026-08-03T23:41:23.212256Z", + "iopub.status.idle": "2026-08-03T23:41:28.729741Z", + "shell.execute_reply": "2026-08-03T23:41:28.728086Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sentinel version: 2.0.0\n" + ] + } + ], + "source": [ + "import tempfile\n", + "import time\n", + "from pathlib import Path\n", + "\n", + "import pandas as pd\n", + "\n", + "import sentinel\n", + "from sentinel import SentinelLocalIndex\n", + "from sentinel.simulation import (\n", + " LabeledGroup,\n", + " compare_aggregators,\n", + " encode_observations,\n", + " run_grid_search,\n", + " score_groups,\n", + ")\n", + "\n", + "print(f\"Sentinel version: {sentinel.__version__}\")" + ] + }, + { + "cell_type": "markdown", + "id": "02650405", + "metadata": {}, + "source": [ + "## 1. `from_texts()` - build an index in one call\n", + "\n", + "The **Build The Indexes** section above does this by hand: load the model, keep the\n", + "scaling function, encode both sides, remember `normalize_embeddings=True`, keep the\n", + "corpus, then construct the index.\n", + "\n", + "Two of those steps fail *silently* when skipped. Without normalisation the similarity\n", + "maths quietly returns wrong numbers, and without the corpus you lose explanations.\n", + "Neither raises, and neither warns - you simply get worse results and never find out.\n", + "`from_texts()` does the whole recipe, inside the library, where it is tested." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "65642761", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-03T23:41:28.732522Z", + "iopub.status.busy": "2026-08-03T23:41:28.732426Z", + "iopub.status.idle": "2026-08-03T23:41:29.368117Z", + "shell.execute_reply": "2026-08-03T23:41:29.367055Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "positives: (6, 384)\n", + "negatives: (6, 384)\n", + "corpus kept automatically: True\n", + "normalisation applied: True\n" + ] + } + ], + "source": [ + "# A few examples of the rare class we want to catch, and of ordinary conversation.\n", + "HATEFUL = [\n", + " \"they are vermin and need to be removed from our country\",\n", + " \"those people are subhuman and do not deserve any rights\",\n", + " \"we must cleanse the nation of their filthy kind\",\n", + " \"they are a disease infecting our society and must be purged\",\n", + " \"their race has always been inferior and violent by nature\",\n", + " \"we should drive every last one of them out by force\",\n", + " \"the world would be better off without their bloodline\",\n", + " \"they breed like rats and are ruining everything we built\",\n", + "]\n", + "ORDINARY = [\n", + " \"the weather has been lovely this week\",\n", + " \"i really enjoyed that documentary last night\",\n", + " \"we should meet for coffee sometime soon\",\n", + " \"the train was delayed again this morning\",\n", + " \"she gave a thoughtful talk about urban planning\",\n", + " \"my neighbour is teaching me to bake bread\",\n", + " \"the new library opens on the corner next month\",\n", + " \"he spent the weekend repairing his bicycle\",\n", + " \"they are renovating the community centre this spring\",\n", + " \"the match ended in a draw after extra time\",\n", + " \"i finally finished the book you lent me\",\n", + " \"the bakery on the high street changed hands\",\n", + " \"we planted tomatoes along the back fence\",\n", + " \"the bus route is being extended next year\",\n", + " \"there is a food market every second saturday\",\n", + " \"my sister is moving house in the autumn\",\n", + "]\n", + "\n", + "# Half of each list goes into the index; the rest is held out for evaluation below, so we\n", + "# never score an example against itself.\n", + "index_positives, held_out_hateful = HATEFUL[:6], HATEFUL[6:]\n", + "index_negatives, held_out_ordinary = ORDINARY[:6], ORDINARY[6:]\n", + "\n", + "demo_index = SentinelLocalIndex.from_texts(\n", + " positive_texts=index_positives,\n", + " negative_texts=index_negatives,\n", + " model_name=\"sentence-transformers/all-MiniLM-L6-v2\",\n", + ")\n", + "\n", + "print(f\"positives: {tuple(demo_index.positive_embeddings.shape)}\")\n", + "print(f\"negatives: {tuple(demo_index.negative_embeddings.shape)}\")\n", + "print(f\"corpus kept automatically: {demo_index.positive_corpus is not None}\")\n", + "print(f\"normalisation applied: {demo_index.encoding_kwargs['normalize_embeddings']}\")" + ] + }, + { + "cell_type": "markdown", + "id": "8a656e18", + "metadata": {}, + "source": [ + "## 2. The corpus survives a save/load round trip\n", + "\n", + "In 1.0, `save()` wrote only the embeddings and the config. After any reload the corpus\n", + "was gone, so explanations reported the *row number* of a match rather than the sentence\n", + "that caused it - useful to a machine, useless to a reviewer.\n", + "\n", + "2.0 writes a `corpus.json` beside the embeddings, so an explanation can name the text." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "8f191b0a", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-03T23:41:29.370531Z", + "iopub.status.busy": "2026-08-03T23:41:29.370441Z", + "iopub.status.idle": "2026-08-03T23:41:29.979441Z", + "shell.execute_reply": "2026-08-03T23:41:29.978410Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "files written: ['corpus.json', 'embeddings.safetensors', 'sentinel_local_index_config.json']\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Why that text scored as it did - the nearest examples, named rather than numbered:\n", + " + 0.540 they are a disease infecting our society and must be purged\n", + " + 0.424 those people are subhuman and do not deserve any rights\n", + " + 0.397 we must cleanse the nation of their filthy kind\n" + ] + } + ], + "source": [ + "with tempfile.TemporaryDirectory() as tmp:\n", + " demo_index.save(\n", + " path=tmp, encoder_model_name_or_path=\"sentence-transformers/all-MiniLM-L6-v2\"\n", + " )\n", + " print(\"files written:\", sorted(p.name for p in Path(tmp).iterdir()))\n", + "\n", + " # A seed makes the load reproducible, which is also new in 2.0: without it the\n", + " # negative downsampling differed on every load and the same text scored differently.\n", + " reloaded = SentinelLocalIndex.load(\n", + " path=tmp, negative_to_positive_ratio=None, seed=42\n", + " )\n", + "\n", + "result = reloaded.calculate_rare_class_affinity(\n", + " [\"those people are a plague on this country\"]\n", + ")\n", + "explanation = next(iter(result.explanations.values()))\n", + "\n", + "print(\"\\nWhy that text scored as it did - the nearest examples, named rather than numbered:\")\n", + "for neighbor in explanation[\"neighbors\"][:3]:\n", + " print(f\" {neighbor['sign']} {neighbor['scaled_score']:.3f} {neighbor['neighbor']}\")" + ] + }, + { + "cell_type": "markdown", + "id": "8031fd92", + "metadata": {}, + "source": [ + "## 3. `subsample()` - resize an index without re-encoding\n", + "\n", + "Encoding a sentence produces the same numbers regardless of which index it ends up in,\n", + "so a small index is just a large one with rows removed. Before 2.0, trying a smaller\n", + "index meant encoding the whole corpus from scratch.\n", + "\n", + "The example index shipped with this repository is a realistic size, so it shows the\n", + "point better than our six-sentence one." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "d750fb71", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-03T23:41:29.981739Z", + "iopub.status.busy": "2026-08-03T23:41:29.981656Z", + "iopub.status.idle": "2026-08-03T23:41:30.451752Z", + "shell.execute_reply": "2026-08-03T23:41:30.450496Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "original: 1,516 positives / 15,160 negatives\n", + "resized: 500 positives / 2,500 negatives\n", + "took 2.7 ms - no encoding involved\n", + "\n", + "original still intact: 1,516 positives\n" + ] + } + ], + "source": [ + "shipped = SentinelLocalIndex.load(\n", + " path=\"hate_speech_model\", negative_to_positive_ratio=None, seed=42\n", + ")\n", + "\n", + "start = time.perf_counter()\n", + "smaller = shipped.subsample(n_positive=500, neg_to_pos_ratio=5.0, seed=42)\n", + "elapsed_ms = (time.perf_counter() - start) * 1000\n", + "\n", + "print(f\"original: {shipped.positive_embeddings.shape[0]:,} positives / \"\n", + " f\"{shipped.negative_embeddings.shape[0]:,} negatives\")\n", + "print(f\"resized: {smaller.positive_embeddings.shape[0]:,} positives / \"\n", + " f\"{smaller.negative_embeddings.shape[0]:,} negatives\")\n", + "print(f\"took {elapsed_ms:.1f} ms - no encoding involved\")\n", + "\n", + "# The original is untouched, which is what makes it safe to loop over sizes. If each\n", + "# call shrank the receiver, run two would start from run one's leftovers.\n", + "print(f\"\\noriginal still intact: {shipped.positive_embeddings.shape[0]:,} positives\")" + ] + }, + { + "cell_type": "markdown", + "id": "44070b97", + "metadata": {}, + "source": [ + "## 4. Grid search over the index itself\n", + "\n", + "`run_grid_search` can now sweep index size and negative ratio, not just `top_k` and the\n", + "per-observation threshold. Index size is usually the highest-leverage knob, and\n", + "`subsample()` is what makes exploring it affordable.\n", + "\n", + "We evaluate on the sentences held out above, arranged as four \"sources\". The two\n", + "positive sources are mostly ordinary chatter with a couple of hateful lines mixed in -\n", + "the realistic pattern, and the one the appendix on skewness above is about." + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "77fe2419", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-03T23:41:30.454364Z", + "iopub.status.busy": "2026-08-03T23:41:30.454275Z", + "iopub.status.idle": "2026-08-03T23:41:30.552105Z", + "shell.execute_reply": "2026-08-03T23:41:30.551211Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " hateful_a label=1 10 observations\n", + " hateful_b label=1 10 observations\n", + " ordinary_a label=0 10 observations\n", + " ordinary_b label=0 10 observations\n", + "\n", + "48 rows across 4 index configurations\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
aggregatorindex_n_positiveindex_neg_to_pos_ratioindex_n_positive_actualindex_n_negative_actualtop_kroc_auc
0skewness30.53111.0
1mean_of_positives30.53111.0
2top_k_mean30.53111.0
3percentile_score30.53111.0
4softmax_weighted_mean30.53111.0
5max_score30.53111.0
\n", + "
" + ], + "text/plain": [ + " aggregator index_n_positive index_neg_to_pos_ratio \\\n", + "0 skewness 3 0.5 \n", + "1 mean_of_positives 3 0.5 \n", + "2 top_k_mean 3 0.5 \n", + "3 percentile_score 3 0.5 \n", + "4 softmax_weighted_mean 3 0.5 \n", + "5 max_score 3 0.5 \n", + "\n", + " index_n_positive_actual index_n_negative_actual top_k roc_auc \n", + "0 3 1 1 1.0 \n", + "1 3 1 1 1.0 \n", + "2 3 1 1 1.0 \n", + "3 3 1 1 1.0 \n", + "4 3 1 1 1.0 \n", + "5 3 1 1 1.0 " + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "groups = [\n", + " LabeledGroup(\n", + " name=\"hateful_a\", label=1,\n", + " observations=held_out_ordinary[:9] + held_out_hateful[:1],\n", + " ),\n", + " LabeledGroup(\n", + " name=\"hateful_b\", label=1,\n", + " observations=held_out_ordinary[1:] + held_out_hateful[1:],\n", + " ),\n", + " LabeledGroup(name=\"ordinary_a\", label=0, observations=held_out_ordinary[:10]),\n", + " LabeledGroup(name=\"ordinary_b\", label=0, observations=held_out_ordinary),\n", + "]\n", + "for group in groups:\n", + " print(f\" {group.name:11} label={group.label} {len(group.observations)} observations\")\n", + "\n", + "grid = pd.DataFrame(\n", + " run_grid_search(\n", + " demo_index,\n", + " groups,\n", + " n_positive_values=[3, 6], # new in 2.0: index size\n", + " neg_to_pos_ratios=[0.5, 1.0], # new in 2.0: index balance\n", + " top_k_values=[1, 3],\n", + " min_score_values=[0.0],\n", + " index_seed=42, # reproducible index configurations\n", + " )\n", + ")\n", + "\n", + "configurations = grid[[\"index_n_positive\", \"index_neg_to_pos_ratio\"]].drop_duplicates()\n", + "print(f\"\\n{len(grid)} rows across {len(configurations)} index configurations\")\n", + "grid[[\n", + " \"aggregator\", \"index_n_positive\", \"index_neg_to_pos_ratio\",\n", + " \"index_n_positive_actual\", \"index_n_negative_actual\", \"top_k\", \"roc_auc\",\n", + "]].head(6)" + ] + }, + { + "cell_type": "markdown", + "id": "b7782170", + "metadata": {}, + "source": [ + "### Two details in that table worth pausing on\n", + "\n", + "**Requested versus actual.** A request is *clipped* when the index holds fewer examples\n", + "than you asked for, so `index_n_positive` (what you asked for) and\n", + "`index_n_positive_actual` (what you got) can differ. Without the actual counts, two rows\n", + "can look like a genuine size sweep while describing nearly the same index.\n", + "\n", + "**Why every index column is prefixed.** `evaluate_groups` already returns an\n", + "`n_positive`, meaning *the number of positive groups in your evaluation set*. That is an\n", + "entirely different number from the index size, and the two briefly shared one name - so\n", + "the index size silently overwrote the group count. Writing a column twice now raises\n", + "rather than overwriting." + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "54205914", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-03T23:41:30.554086Z", + "iopub.status.busy": "2026-08-03T23:41:30.554010Z", + "iopub.status.idle": "2026-08-03T23:41:30.561259Z", + "shell.execute_reply": "2026-08-03T23:41:30.559999Z" + } + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
n_positiveindex_n_positiveindex_n_positive_actual
0233
24266
\n", + "
" + ], + "text/plain": [ + " n_positive index_n_positive index_n_positive_actual\n", + "0 2 3 3\n", + "24 2 6 6" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Same rows, two unrelated meanings, both surviving side by side.\n", + "# n_positive -> how many of our four sources are labelled positive (2)\n", + "# index_n_positive -> how many positive examples the index was built with (3 or 6)\n", + "grid[[\"n_positive\", \"index_n_positive\", \"index_n_positive_actual\"]].drop_duplicates()" + ] + }, + { + "cell_type": "markdown", + "id": "afed8218", + "metadata": {}, + "source": [ + "## 5. Observations are encoded once per sweep\n", + "\n", + "An observation's embedding depends on the **encoder**, never on the index it is scored\n", + "against - and a subsampled index shares its parent's model. So re-encoding the same text\n", + "on every pass was recomputing identical numbers.\n", + "\n", + "2.0 hoists the encoding out of the loops. The results are unchanged; only the time is." + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "8dd6275f", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-03T23:41:30.563436Z", + "iopub.status.busy": "2026-08-03T23:41:30.563363Z", + "iopub.status.idle": "2026-08-03T23:41:31.266170Z", + "shell.execute_reply": "2026-08-03T23:41:31.264902Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "re-encoding on every pass: 0.60s\n", + "encoding once: 0.09s\n", + "saving: 6.8x\n", + "identical results: True\n" + ] + } + ], + "source": [ + "sweep = dict(\n", + " n_positive_values=[3, 6],\n", + " neg_to_pos_ratios=[0.5, 1.0],\n", + " top_k_values=[1, 2, 3],\n", + " min_score_values=[0.0],\n", + " index_seed=42,\n", + ")\n", + "\n", + "start = time.perf_counter()\n", + "uncached = run_grid_search(demo_index, groups, cache_observation_embeddings=False, **sweep)\n", + "uncached_seconds = time.perf_counter() - start\n", + "\n", + "start = time.perf_counter()\n", + "cached = run_grid_search(demo_index, groups, cache_observation_embeddings=True, **sweep)\n", + "cached_seconds = time.perf_counter() - start\n", + "\n", + "print(f\"re-encoding on every pass: {uncached_seconds:.2f}s\")\n", + "print(f\"encoding once: {cached_seconds:.2f}s\")\n", + "print(f\"saving: {uncached_seconds / cached_seconds:.1f}x\")\n", + "print(f\"identical results: {pd.DataFrame(uncached).equals(pd.DataFrame(cached))}\")" + ] + }, + { + "cell_type": "markdown", + "id": "8fbb1cb2", + "metadata": {}, + "source": [ + "The saving grows with the grid, because the single encoding pass is amortised over more\n", + "scoring passes. Pass `cache_observation_embeddings=False` if memory is tight - the cache\n", + "holds one embedding per observation.\n", + "\n", + "You can also hold the embeddings yourself, which is what you want when scoring the same\n", + "observations against several unrelated indices." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "3e9f68f1", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-03T23:41:31.268638Z", + "iopub.status.busy": "2026-08-03T23:41:31.268551Z", + "iopub.status.idle": "2026-08-03T23:41:31.333630Z", + "shell.execute_reply": "2026-08-03T23:41:31.332603Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "cached embeddings for 4 groups\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
indexfullhalf-size
aggregator
max_score1.01.0
mean_of_positives1.01.0
percentile_score1.01.0
skewness1.01.0
softmax_weighted_mean1.01.0
top_k_mean1.01.0
\n", + "
" + ], + "text/plain": [ + "index full half-size\n", + "aggregator \n", + "max_score 1.0 1.0\n", + "mean_of_positives 1.0 1.0\n", + "percentile_score 1.0 1.0\n", + "skewness 1.0 1.0\n", + "softmax_weighted_mean 1.0 1.0\n", + "top_k_mean 1.0 1.0" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "embeddings = encode_observations(demo_index, groups)\n", + "print(f\"cached embeddings for {len(embeddings)} groups\")\n", + "\n", + "# The same embeddings, scored against two different indices built from one encoder.\n", + "comparisons = []\n", + "for name, candidate in (\n", + " (\"full\", demo_index),\n", + " (\"half-size\", demo_index.subsample(n_positive=3, seed=42)),\n", + "):\n", + " scored = score_groups(candidate, groups, top_k=3, observation_embeddings=embeddings)\n", + " for row in compare_aggregators(scored):\n", + " comparisons.append({\"index\": name, **row})\n", + "\n", + "pd.DataFrame(comparisons).pivot(\n", + " index=\"aggregator\", columns=\"index\", values=\"roc_auc\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "2726ea38", + "metadata": {}, + "source": [ + "Every aggregator separates this example perfectly, which tells you the example is easy\n", + "rather than that the aggregators are equivalent - with four sources and a vocabulary this\n", + "clean, there is nothing to distinguish them. The comparison only becomes interesting on\n", + "real data, which is what the **Tuning aggregation strategies** section above does.\n", + "\n", + "What this cell does show is the mechanism: one set of embeddings, computed once, scored\n", + "against two different indices without re-encoding anything.\n", + "\n", + "## Summary\n", + "\n", + "| Feature | What it replaces |\n", + "|---|---|\n", + "| `from_texts()` | An eight-step manual build, two steps of which fail silently |\n", + "| `subsample()` | Re-encoding the whole corpus to try a smaller index |\n", + "| Index sweep axes | Only being able to sweep `top_k` and the threshold |\n", + "| Cached observation embeddings | Re-encoding identical text on every pass |\n", + "| Persisted corpus | Explanations degrading to row numbers after a reload |\n", + "| Seeded `load()` | A saved index scoring differently on every load |\n", + "\n", + "One caveat on the numbers above: this appendix uses a deliberately tiny example set so it\n", + "runs in seconds. Treat the timings as illustrations of the shape of the improvement\n", + "rather than as benchmarks - on a real corpus the encoding cost dominates far more, so the\n", + "savings are correspondingly larger." + ] } ], "metadata": { From d0e16e02844216b7842fd8a76b4c567ddc260062 Mon Sep 17 00:00:00 2001 From: Wei Xiao <11197323+wxiao0421@users.noreply.github.com> Date: Mon, 3 Aug 2026 16:58:01 -0700 Subject: [PATCH 4/7] Cover the remaining V2 additions in the notebook appendix An audit of the public API added since 0e924ae against the appendix found four things demonstrated nowhere: evaluate_groups, DEFAULT_AGGREGATORS, load_corpus, and the sample_embeddings parameter itself, which the appendix only reached indirectly through encode_observations. Adds a section introducing the harness by cost - score once, then evaluate with one aggregator, then all of them, then sweep - which is where evaluate_groups and DEFAULT_AGGREGATORS naturally belong, and which also shows the three metric families on a single row. The group fixture moves there, since that section is now the first to use it. Extends the corpus section with load_corpus, reading the texts back without the embeddings. It returns (None, None) for the shipped example index, which is a concrete demonstration of the pre-2.0 format rather than an assertion about it. Extends the caching section with the underlying parameter, including the refusal when the embeddings do not line up with the text. Co-authored-by: Cursor --- examples/sentinel_against_hate.ipynb | 352 ++++++++++++++++++++------- 1 file changed, 268 insertions(+), 84 deletions(-) diff --git a/examples/sentinel_against_hate.ipynb b/examples/sentinel_against_hate.ipynb index 0f9a8bf..398ef09 100644 --- a/examples/sentinel_against_hate.ipynb +++ b/examples/sentinel_against_hate.ipynb @@ -2820,7 +2820,7 @@ }, { "cell_type": "markdown", - "id": "fabe7360", + "id": "d2659d67", "metadata": {}, "source": [ "# Appendix: What's new in Sentinel 2.0\n", @@ -2839,13 +2839,13 @@ { "cell_type": "code", "execution_count": 22, - "id": "c0365d4b", + "id": "52d0094c", "metadata": { "execution": { - "iopub.execute_input": "2026-08-03T23:41:23.212352Z", - "iopub.status.busy": "2026-08-03T23:41:23.212256Z", - "iopub.status.idle": "2026-08-03T23:41:28.729741Z", - "shell.execute_reply": "2026-08-03T23:41:28.728086Z" + "iopub.execute_input": "2026-08-03T23:56:17.820400Z", + "iopub.status.busy": "2026-08-03T23:56:17.820313Z", + "iopub.status.idle": "2026-08-03T23:56:23.361049Z", + "shell.execute_reply": "2026-08-03T23:56:23.359713Z" } }, "outputs": [ @@ -2870,6 +2870,7 @@ " LabeledGroup,\n", " compare_aggregators,\n", " encode_observations,\n", + " evaluate_groups,\n", " run_grid_search,\n", " score_groups,\n", ")\n", @@ -2879,7 +2880,7 @@ }, { "cell_type": "markdown", - "id": "02650405", + "id": "65f4f3a1", "metadata": {}, "source": [ "## 1. `from_texts()` - build an index in one call\n", @@ -2897,13 +2898,13 @@ { "cell_type": "code", "execution_count": 23, - "id": "65642761", + "id": "978aefee", "metadata": { "execution": { - "iopub.execute_input": "2026-08-03T23:41:28.732522Z", - "iopub.status.busy": "2026-08-03T23:41:28.732426Z", - "iopub.status.idle": "2026-08-03T23:41:29.368117Z", - "shell.execute_reply": "2026-08-03T23:41:29.367055Z" + "iopub.execute_input": "2026-08-03T23:56:23.364588Z", + "iopub.status.busy": "2026-08-03T23:56:23.364218Z", + "iopub.status.idle": "2026-08-03T23:56:23.974273Z", + "shell.execute_reply": "2026-08-03T23:56:23.973053Z" } }, "outputs": [ @@ -2968,7 +2969,7 @@ }, { "cell_type": "markdown", - "id": "8a656e18", + "id": "6ea59f1a", "metadata": {}, "source": [ "## 2. The corpus survives a save/load round trip\n", @@ -2983,13 +2984,13 @@ { "cell_type": "code", "execution_count": 24, - "id": "8f191b0a", + "id": "b4222b16", "metadata": { "execution": { - "iopub.execute_input": "2026-08-03T23:41:29.370531Z", - "iopub.status.busy": "2026-08-03T23:41:29.370441Z", - "iopub.status.idle": "2026-08-03T23:41:29.979441Z", - "shell.execute_reply": "2026-08-03T23:41:29.978410Z" + "iopub.execute_input": "2026-08-03T23:56:23.976751Z", + "iopub.status.busy": "2026-08-03T23:56:23.976640Z", + "iopub.status.idle": "2026-08-03T23:56:24.494507Z", + "shell.execute_reply": "2026-08-03T23:56:24.493255Z" } }, "outputs": [ @@ -3037,7 +3038,58 @@ }, { "cell_type": "markdown", - "id": "8031fd92", + "id": "629213a8", + "metadata": {}, + "source": [ + "`load_corpus` reads those texts back on their own, without loading the embeddings -\n", + "handy for checking what a saved index actually contains before you trust it. It returns\n", + "`(None, None)` for an index saved before 2.0, which is how the shipped example index\n", + "behaves and why its explanations would show row numbers." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "41c1dfe0", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-03T23:56:24.497182Z", + "iopub.status.busy": "2026-08-03T23:56:24.497042Z", + "iopub.status.idle": "2026-08-03T23:56:24.505049Z", + "shell.execute_reply": "2026-08-03T23:56:24.504047Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "positives stored: 6 negatives stored: 6\n", + "first positive: they are vermin and need to be removed from our country\n", + "\n", + "shipped example index: (None, None)\n" + ] + } + ], + "source": [ + "from sentinel.io import load_corpus\n", + "\n", + "with tempfile.TemporaryDirectory() as tmp:\n", + " demo_index.save(\n", + " path=tmp, encoder_model_name_or_path=\"sentence-transformers/all-MiniLM-L6-v2\"\n", + " )\n", + " saved_positive, saved_negative = load_corpus(path=tmp)\n", + "\n", + "print(f\"positives stored: {len(saved_positive)} negatives stored: {len(saved_negative)}\")\n", + "print(f\"first positive: {saved_positive[0]}\")\n", + "\n", + "# The index shipped with this repository predates corpus support.\n", + "print(f\"\\nshipped example index: {load_corpus(path='hate_speech_model')}\")" + ] + }, + { + "cell_type": "markdown", + "id": "5eec83da", "metadata": {}, "source": [ "## 3. `subsample()` - resize an index without re-encoding\n", @@ -3052,14 +3104,14 @@ }, { "cell_type": "code", - "execution_count": 25, - "id": "d750fb71", + "execution_count": 26, + "id": "0a5d9eed", "metadata": { "execution": { - "iopub.execute_input": "2026-08-03T23:41:29.981739Z", - "iopub.status.busy": "2026-08-03T23:41:29.981656Z", - "iopub.status.idle": "2026-08-03T23:41:30.451752Z", - "shell.execute_reply": "2026-08-03T23:41:30.450496Z" + "iopub.execute_input": "2026-08-03T23:56:24.507918Z", + "iopub.status.busy": "2026-08-03T23:56:24.507789Z", + "iopub.status.idle": "2026-08-03T23:56:25.101191Z", + "shell.execute_reply": "2026-08-03T23:56:25.099711Z" } }, "outputs": [ @@ -3069,7 +3121,7 @@ "text": [ "original: 1,516 positives / 15,160 negatives\n", "resized: 500 positives / 2,500 negatives\n", - "took 2.7 ms - no encoding involved\n", + "took 2.2 ms - no encoding involved\n", "\n", "original still intact: 1,516 positives\n" ] @@ -3097,30 +3149,128 @@ }, { "cell_type": "markdown", - "id": "44070b97", + "id": "1ffe03e2", + "metadata": {}, + "source": [ + "## 4. The tuning harness, cheapest entry point first\n", + "\n", + "`sentinel.simulation` is layered by cost. Scoring runs the model and is the only\n", + "expensive step, so you do it once and then evaluate as many ways as you like for free:\n", + "\n", + "| Function | What it does | Cost |\n", + "|---|---|---|\n", + "| `score_groups` | Scores every observation in every group | runs the model |\n", + "| `evaluate_groups` | Collapses each group to one number with **one** aggregator, then measures the separation | free |\n", + "| `compare_aggregators` | Runs `evaluate_groups` once per aggregator on the same scores | free |\n", + "| `run_grid_search` | Wraps all of the above and sweeps hyperparameters | re-scores per `top_k` |\n", + "\n", + "Two different things get called \"metrics\" here. The **summarize metric** (the\n", + "aggregator) is *how* a group's many scores become one number, and it is what you sweep.\n", + "The **evaluation metric** is *how good* that separation is, and you do not choose one -\n", + "every row reports all three families." + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "5f7f90b1", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-03T23:56:25.103361Z", + "iopub.status.busy": "2026-08-03T23:56:25.103234Z", + "iopub.status.idle": "2026-08-03T23:56:25.161903Z", + "shell.execute_reply": "2026-08-03T23:56:25.160411Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " hateful_a label=1 10 observations\n", + " hateful_b label=1 10 observations\n", + " ordinary_a label=0 10 observations\n", + " ordinary_b label=0 10 observations\n", + "\n", + "built-in summarize metrics: skewness, mean_of_positives, top_k_mean, percentile_score, softmax_weighted_mean, max_score\n", + "\n", + "scored 4 groups; hateful_a has 10 observation scores\n", + "\n", + "skewness alone -> roc_auc=1.00 f1=1.00\n", + " ranking: roc_auc=1.00 recall_at_n=1.00\n", + " threshold: precision=1.00 recall=1.00\n", + " separation: mean_separation=0.333 ks=1.00\n", + "\n", + "n_groups=4 n_positive=2 (positive sources)\n" + ] + } + ], + "source": [ + "from sentinel.simulation import DEFAULT_AGGREGATORS\n", + "\n", + "# Four \"sources\" built from the sentences held out above. The two positive sources are\n", + "# mostly ordinary chatter with a couple of hateful lines mixed in - the realistic\n", + "# pattern, and the one the appendix on skewness above is about.\n", + "groups = [\n", + " LabeledGroup(\n", + " name=\"hateful_a\", label=1,\n", + " observations=held_out_ordinary[:9] + held_out_hateful[:1],\n", + " ),\n", + " LabeledGroup(\n", + " name=\"hateful_b\", label=1,\n", + " observations=held_out_ordinary[1:] + held_out_hateful[1:],\n", + " ),\n", + " LabeledGroup(name=\"ordinary_a\", label=0, observations=held_out_ordinary[:10]),\n", + " LabeledGroup(name=\"ordinary_b\", label=0, observations=held_out_ordinary),\n", + "]\n", + "for group in groups:\n", + " print(f\" {group.name:11} label={group.label} {len(group.observations)} observations\")\n", + "\n", + "print(\"\\nbuilt-in summarize metrics:\", \", \".join(DEFAULT_AGGREGATORS))\n", + "\n", + "# Score once...\n", + "scored_once = score_groups(demo_index, groups, top_k=3)\n", + "print(f\"\\nscored {len(scored_once)} groups; \"\n", + " f\"{scored_once[0].name} has {len(scored_once[0].observation_scores)} observation scores\")\n", + "\n", + "# ...then evaluate with a single aggregator, for free.\n", + "row = evaluate_groups(\n", + " scored_once, DEFAULT_AGGREGATORS[\"skewness\"], aggregator_name=\"skewness\"\n", + ")\n", + "print(f\"\\nskewness alone -> roc_auc={row['roc_auc']:.2f} f1={row['f1']:.2f}\")\n", + "print(f\" ranking: roc_auc={row['roc_auc']:.2f} recall_at_n={row['recall_at_n']:.2f}\")\n", + "print(f\" threshold: precision={row['precision']:.2f} recall={row['recall']:.2f}\")\n", + "print(f\" separation: mean_separation={row['mean_separation']:.3f} ks={row['ks_statistic']:.2f}\")\n", + "\n", + "# n_positive here counts positive GROUPS, not index rows - the distinction that the\n", + "# index_ prefix exists to protect.\n", + "print(f\"\\nn_groups={row['n_groups']} n_positive={row['n_positive']} (positive sources)\")" + ] + }, + { + "cell_type": "markdown", + "id": "b9abde0c", "metadata": {}, "source": [ - "## 4. Grid search over the index itself\n", + "## 5. Grid search over the index itself\n", "\n", "`run_grid_search` can now sweep index size and negative ratio, not just `top_k` and the\n", "per-observation threshold. Index size is usually the highest-leverage knob, and\n", "`subsample()` is what makes exploring it affordable.\n", "\n", - "We evaluate on the sentences held out above, arranged as four \"sources\". The two\n", - "positive sources are mostly ordinary chatter with a couple of hateful lines mixed in -\n", - "the realistic pattern, and the one the appendix on skewness above is about." + "Same four sources as above, so the only thing changing is the index." ] }, { "cell_type": "code", - "execution_count": 26, - "id": "77fe2419", + "execution_count": 28, + "id": "578b8945", "metadata": { "execution": { - "iopub.execute_input": "2026-08-03T23:41:30.454364Z", - "iopub.status.busy": "2026-08-03T23:41:30.454275Z", - "iopub.status.idle": "2026-08-03T23:41:30.552105Z", - "shell.execute_reply": "2026-08-03T23:41:30.551211Z" + "iopub.execute_input": "2026-08-03T23:56:25.164197Z", + "iopub.status.busy": "2026-08-03T23:56:25.164107Z", + "iopub.status.idle": "2026-08-03T23:56:25.270108Z", + "shell.execute_reply": "2026-08-03T23:56:25.268331Z" } }, "outputs": [ @@ -3128,10 +3278,6 @@ "name": "stdout", "output_type": "stream", "text": [ - " hateful_a label=1 10 observations\n", - " hateful_b label=1 10 observations\n", - " ordinary_a label=0 10 observations\n", - " ordinary_b label=0 10 observations\n", "\n", "48 rows across 4 index configurations\n" ] @@ -3249,27 +3395,12 @@ "5 3 1 1 1.0 " ] }, - "execution_count": 26, + "execution_count": 28, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "groups = [\n", - " LabeledGroup(\n", - " name=\"hateful_a\", label=1,\n", - " observations=held_out_ordinary[:9] + held_out_hateful[:1],\n", - " ),\n", - " LabeledGroup(\n", - " name=\"hateful_b\", label=1,\n", - " observations=held_out_ordinary[1:] + held_out_hateful[1:],\n", - " ),\n", - " LabeledGroup(name=\"ordinary_a\", label=0, observations=held_out_ordinary[:10]),\n", - " LabeledGroup(name=\"ordinary_b\", label=0, observations=held_out_ordinary),\n", - "]\n", - "for group in groups:\n", - " print(f\" {group.name:11} label={group.label} {len(group.observations)} observations\")\n", - "\n", "grid = pd.DataFrame(\n", " run_grid_search(\n", " demo_index,\n", @@ -3292,7 +3423,7 @@ }, { "cell_type": "markdown", - "id": "b7782170", + "id": "ee6247da", "metadata": {}, "source": [ "### Two details in that table worth pausing on\n", @@ -3311,14 +3442,14 @@ }, { "cell_type": "code", - "execution_count": 27, - "id": "54205914", + "execution_count": 29, + "id": "fadb7c12", "metadata": { "execution": { - "iopub.execute_input": "2026-08-03T23:41:30.554086Z", - "iopub.status.busy": "2026-08-03T23:41:30.554010Z", - "iopub.status.idle": "2026-08-03T23:41:30.561259Z", - "shell.execute_reply": "2026-08-03T23:41:30.559999Z" + "iopub.execute_input": "2026-08-03T23:56:25.272935Z", + "iopub.status.busy": "2026-08-03T23:56:25.272842Z", + "iopub.status.idle": "2026-08-03T23:56:25.281131Z", + "shell.execute_reply": "2026-08-03T23:56:25.279853Z" } }, "outputs": [ @@ -3371,7 +3502,7 @@ "24 2 6 6" ] }, - "execution_count": 27, + "execution_count": 29, "metadata": {}, "output_type": "execute_result" } @@ -3385,10 +3516,10 @@ }, { "cell_type": "markdown", - "id": "afed8218", + "id": "f159d274", "metadata": {}, "source": [ - "## 5. Observations are encoded once per sweep\n", + "## 6. Observations are encoded once per sweep\n", "\n", "An observation's embedding depends on the **encoder**, never on the index it is scored\n", "against - and a subsampled index shares its parent's model. So re-encoding the same text\n", @@ -3399,14 +3530,14 @@ }, { "cell_type": "code", - "execution_count": 28, - "id": "8dd6275f", + "execution_count": 30, + "id": "5be653dc", "metadata": { "execution": { - "iopub.execute_input": "2026-08-03T23:41:30.563436Z", - "iopub.status.busy": "2026-08-03T23:41:30.563363Z", - "iopub.status.idle": "2026-08-03T23:41:31.266170Z", - "shell.execute_reply": "2026-08-03T23:41:31.264902Z" + "iopub.execute_input": "2026-08-03T23:56:25.284893Z", + "iopub.status.busy": "2026-08-03T23:56:25.284565Z", + "iopub.status.idle": "2026-08-03T23:56:26.002174Z", + "shell.execute_reply": "2026-08-03T23:56:26.000855Z" } }, "outputs": [ @@ -3414,9 +3545,9 @@ "name": "stdout", "output_type": "stream", "text": [ - "re-encoding on every pass: 0.60s\n", + "re-encoding on every pass: 0.62s\n", "encoding once: 0.09s\n", - "saving: 6.8x\n", + "saving: 6.6x\n", "identical results: True\n" ] } @@ -3446,7 +3577,7 @@ }, { "cell_type": "markdown", - "id": "8fbb1cb2", + "id": "6abcb44e", "metadata": {}, "source": [ "The saving grows with the grid, because the single encoding pass is amortised over more\n", @@ -3459,14 +3590,14 @@ }, { "cell_type": "code", - "execution_count": 29, - "id": "3e9f68f1", + "execution_count": 31, + "id": "21f7cb1a", "metadata": { "execution": { - "iopub.execute_input": "2026-08-03T23:41:31.268638Z", - "iopub.status.busy": "2026-08-03T23:41:31.268551Z", - "iopub.status.idle": "2026-08-03T23:41:31.333630Z", - "shell.execute_reply": "2026-08-03T23:41:31.332603Z" + "iopub.execute_input": "2026-08-03T23:56:26.004890Z", + "iopub.status.busy": "2026-08-03T23:56:26.004795Z", + "iopub.status.idle": "2026-08-03T23:56:26.079726Z", + "shell.execute_reply": "2026-08-03T23:56:26.076989Z" } }, "outputs": [ @@ -3553,7 +3684,7 @@ "top_k_mean 1.0 1.0" ] }, - "execution_count": 29, + "execution_count": 31, "metadata": {}, "output_type": "execute_result" } @@ -3579,7 +3710,60 @@ }, { "cell_type": "markdown", - "id": "2726ea38", + "id": "5f3f3643", + "metadata": {}, + "source": [ + "At the lowest level this is just a new argument on `calculate_rare_class_affinity`.\n", + "Everything above is built on it, and you can reach for it directly when scoring the same\n", + "text repeatedly. Sentinel checks that the embeddings line up with the text, because\n", + "silently pairing an observation with the wrong vector would be far worse than an error." + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "id": "a196e6d6", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-03T23:56:26.082839Z", + "iopub.status.busy": "2026-08-03T23:56:26.082736Z", + "iopub.status.idle": "2026-08-03T23:56:26.106353Z", + "shell.execute_reply": "2026-08-03T23:56:26.105172Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "same scores either way: True\n", + " 0.202 those people are a plague on this country\n", + " 0.000 the bus route changed again\n", + "\n", + "refused: sample_embeddings has 1 rows but text_samples has 2 entries. Scoring them together would pair each observation with the wrong embedding, so refusing to continue.\n" + ] + } + ], + "source": [ + "texts = [\"those people are a plague on this country\", \"the bus route changed again\"]\n", + "vectors = demo_index.sentence_model.encode(texts, **demo_index.encoding_kwargs)\n", + "\n", + "inline = demo_index.calculate_rare_class_affinity(texts)\n", + "reused = demo_index.calculate_rare_class_affinity(texts, vectors)\n", + "print(f\"same scores either way: {inline.observation_scores == reused.observation_scores}\")\n", + "for observation, score in reused.observation_scores.items():\n", + " print(f\" {score:.3f} {observation}\")\n", + "\n", + "# A mismatch is refused rather than quietly scoring against the wrong vectors.\n", + "try:\n", + " demo_index.calculate_rare_class_affinity(texts, vectors[:1])\n", + "except ValueError as error:\n", + " print(f\"\\nrefused: {error}\")" + ] + }, + { + "cell_type": "markdown", + "id": "84d95551", "metadata": {}, "source": [ "Every aggregator separates this example perfectly, which tells you the example is easy\n", From 43ba0176bccdd33e4d0377861a7c9a99536bd3b1 Mon Sep 17 00:00:00 2001 From: Wei Xiao <11197323+wxiao0421@users.noreply.github.com> Date: Mon, 3 Aug 2026 17:33:47 -0700 Subject: [PATCH 5/7] Add a real-data configuration sweep and its conclusions to the appendix The "Tuning aggregation strategies" section is itself a 2.0 feature - the simulation harness arrived in #31, inside this release - so the appendix opened on a false premise by calling everything above it 1.0. Corrected, and it now points at that section as the worked example on real data. Adds section 7, which sweeps the axes that section could not: three index sizes x three negative ratios x three top_k x two thresholds over the real 30-vs-30 podcast set, 22,885 segments, 324 rows. It runs on the groups built earlier in the notebook rather than the appendix's toy fixture, and is marked as depending on them. That sweep is only practical because of the embedding cache. Encoding those segments takes 118s and a scoring pass afterwards takes 1.4s, so the 27 configurations are 2.6 minutes rather than 54. Three findings, all from the executed output. A larger index is reliably better, with no plateau at the full 1,516 positives. Fewer neighbours beat more, top_k=3 winning on both mean and max. And the aggregator ranking inverts depending on how you read it: top_k_mean has the best average ROC-AUC while skewness has by far the best achievable, 0.996 against 0.890, so skewness is the most configuration-sensitive of the six. Picking by average would have chosen top_k_mean and given up about a tenth of a point. The EditNotebook pass used for the intro correction stripped required fields from 41 stream outputs across 19 of the original cells, which invalidated the notebook. The original 32 cells are restored byte-for-byte from before this release's work, and the remaining outputs repaired, so the file validates again. Co-authored-by: Cursor --- examples/sentinel_against_hate.ipynb | 935 ++++++++++++++++++++------- 1 file changed, 702 insertions(+), 233 deletions(-) diff --git a/examples/sentinel_against_hate.ipynb b/examples/sentinel_against_hate.ipynb index 398ef09..f12a6e6 100644 --- a/examples/sentinel_against_hate.ipynb +++ b/examples/sentinel_against_hate.ipynb @@ -2820,26 +2820,32 @@ }, { "cell_type": "markdown", - "id": "d2659d67", "metadata": {}, "source": [ "# Appendix: What's new in Sentinel 2.0\n", "\n", - "Everything above was written against Sentinel 1.0, where building and tuning an index\n", - "meant a good deal of manual work. This appendix demonstrates what 2.0 adds.\n", + "One part of 2.0 is already demonstrated above, on real data: **Tuning aggregation\n", + "strategies with a simulation** uses the `sentinel.simulation` harness, which 2.0\n", + "introduced. Treat that section as the worked example - it compares aggregators on\n", + "thousands of real podcast segments, which is the comparison that actually tells you\n", + "something.\n", + "\n", + "This appendix covers the rest, and fills in the parts of the harness that section does\n", + "not reach (`evaluate_groups`, `DEFAULT_AGGREGATORS`, and the new index sweep axes).\n", "\n", "It is deliberately **self-contained** - it builds its own small index from a handful of\n", "example sentences and does not depend on the data-loading cells above, so you can run it\n", - "on its own in a few seconds. The example set is tiny so the notebook stays fast; the\n", - "advantages grow with real corpora, where encoding dominates everything else.\n", + "on its own in a few seconds. The flip side is that its fixture is too easy to separate\n", + "aggregators meaningfully; for that, read the section above. The timings here are\n", + "illustrations of shape rather than benchmarks, and the advantages grow with real corpora\n", + "where encoding dominates everything else.\n", "\n", "Full write-up and migration notes: [V2_FEATURES.md](../V2_FEATURES.md)." - ] + ], + "id": "d2659d67" }, { "cell_type": "code", - "execution_count": 22, - "id": "52d0094c", "metadata": { "execution": { "iopub.execute_input": "2026-08-03T23:56:17.820400Z", @@ -2848,15 +2854,6 @@ "shell.execute_reply": "2026-08-03T23:56:23.359713Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Sentinel version: 2.0.0\n" - ] - } - ], "source": [ "import tempfile\n", "import time\n", @@ -2876,11 +2873,21 @@ ")\n", "\n", "print(f\"Sentinel version: {sentinel.__version__}\")" - ] + ], + "execution_count": 22, + "outputs": [ + { + "output_type": "stream", + "text": [ + "Sentinel version: 2.0.0\n" + ], + "name": "stdout" + } + ], + "id": "52d0094c" }, { "cell_type": "markdown", - "id": "65f4f3a1", "metadata": {}, "source": [ "## 1. `from_texts()` - build an index in one call\n", @@ -2893,12 +2900,11 @@ "maths quietly returns wrong numbers, and without the corpus you lose explanations.\n", "Neither raises, and neither warns - you simply get worse results and never find out.\n", "`from_texts()` does the whole recipe, inside the library, where it is tested." - ] + ], + "id": "65f4f3a1" }, { "cell_type": "code", - "execution_count": 23, - "id": "978aefee", "metadata": { "execution": { "iopub.execute_input": "2026-08-03T23:56:23.364588Z", @@ -2907,18 +2913,6 @@ "shell.execute_reply": "2026-08-03T23:56:23.973053Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "positives: (6, 384)\n", - "negatives: (6, 384)\n", - "corpus kept automatically: True\n", - "normalisation applied: True\n" - ] - } - ], "source": [ "# A few examples of the rare class we want to catch, and of ordinary conversation.\n", "HATEFUL = [\n", @@ -2965,11 +2959,24 @@ "print(f\"negatives: {tuple(demo_index.negative_embeddings.shape)}\")\n", "print(f\"corpus kept automatically: {demo_index.positive_corpus is not None}\")\n", "print(f\"normalisation applied: {demo_index.encoding_kwargs['normalize_embeddings']}\")" - ] + ], + "execution_count": 23, + "outputs": [ + { + "output_type": "stream", + "text": [ + "positives: (6, 384)\n", + "negatives: (6, 384)\n", + "corpus kept automatically: True\n", + "normalisation applied: True\n" + ], + "name": "stdout" + } + ], + "id": "978aefee" }, { "cell_type": "markdown", - "id": "6ea59f1a", "metadata": {}, "source": [ "## 2. The corpus survives a save/load round trip\n", @@ -2979,12 +2986,11 @@ "that caused it - useful to a machine, useless to a reviewer.\n", "\n", "2.0 writes a `corpus.json` beside the embeddings, so an explanation can name the text." - ] + ], + "id": "6ea59f1a" }, { "cell_type": "code", - "execution_count": 24, - "id": "b4222b16", "metadata": { "execution": { "iopub.execute_input": "2026-08-03T23:56:23.976751Z", @@ -2993,26 +2999,6 @@ "shell.execute_reply": "2026-08-03T23:56:24.493255Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "files written: ['corpus.json', 'embeddings.safetensors', 'sentinel_local_index_config.json']\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "Why that text scored as it did - the nearest examples, named rather than numbered:\n", - " + 0.540 they are a disease infecting our society and must be purged\n", - " + 0.424 those people are subhuman and do not deserve any rights\n", - " + 0.397 we must cleanse the nation of their filthy kind\n" - ] - } - ], "source": [ "with tempfile.TemporaryDirectory() as tmp:\n", " demo_index.save(\n", @@ -3034,23 +3020,43 @@ "print(\"\\nWhy that text scored as it did - the nearest examples, named rather than numbered:\")\n", "for neighbor in explanation[\"neighbors\"][:3]:\n", " print(f\" {neighbor['sign']} {neighbor['scaled_score']:.3f} {neighbor['neighbor']}\")" - ] + ], + "execution_count": 24, + "outputs": [ + { + "output_type": "stream", + "text": [ + "files written: ['corpus.json', 'embeddings.safetensors', 'sentinel_local_index_config.json']\n" + ], + "name": "stdout" + }, + { + "output_type": "stream", + "text": [ + "\n", + "Why that text scored as it did - the nearest examples, named rather than numbered:\n", + " + 0.540 they are a disease infecting our society and must be purged\n", + " + 0.424 those people are subhuman and do not deserve any rights\n", + " + 0.397 we must cleanse the nation of their filthy kind\n" + ], + "name": "stdout" + } + ], + "id": "b4222b16" }, { "cell_type": "markdown", - "id": "629213a8", "metadata": {}, "source": [ "`load_corpus` reads those texts back on their own, without loading the embeddings -\n", "handy for checking what a saved index actually contains before you trust it. It returns\n", "`(None, None)` for an index saved before 2.0, which is how the shipped example index\n", "behaves and why its explanations would show row numbers." - ] + ], + "id": "629213a8" }, { "cell_type": "code", - "execution_count": 25, - "id": "41c1dfe0", "metadata": { "execution": { "iopub.execute_input": "2026-08-03T23:56:24.497182Z", @@ -3059,18 +3065,6 @@ "shell.execute_reply": "2026-08-03T23:56:24.504047Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "positives stored: 6 negatives stored: 6\n", - "first positive: they are vermin and need to be removed from our country\n", - "\n", - "shipped example index: (None, None)\n" - ] - } - ], "source": [ "from sentinel.io import load_corpus\n", "\n", @@ -3085,11 +3079,24 @@ "\n", "# The index shipped with this repository predates corpus support.\n", "print(f\"\\nshipped example index: {load_corpus(path='hate_speech_model')}\")" - ] + ], + "execution_count": 25, + "outputs": [ + { + "output_type": "stream", + "text": [ + "positives stored: 6 negatives stored: 6\n", + "first positive: they are vermin and need to be removed from our country\n", + "\n", + "shipped example index: (None, None)\n" + ], + "name": "stdout" + } + ], + "id": "41c1dfe0" }, { "cell_type": "markdown", - "id": "5eec83da", "metadata": {}, "source": [ "## 3. `subsample()` - resize an index without re-encoding\n", @@ -3100,12 +3107,11 @@ "\n", "The example index shipped with this repository is a realistic size, so it shows the\n", "point better than our six-sentence one." - ] + ], + "id": "5eec83da" }, { "cell_type": "code", - "execution_count": 26, - "id": "0a5d9eed", "metadata": { "execution": { "iopub.execute_input": "2026-08-03T23:56:24.507918Z", @@ -3114,19 +3120,6 @@ "shell.execute_reply": "2026-08-03T23:56:25.099711Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "original: 1,516 positives / 15,160 negatives\n", - "resized: 500 positives / 2,500 negatives\n", - "took 2.2 ms - no encoding involved\n", - "\n", - "original still intact: 1,516 positives\n" - ] - } - ], "source": [ "shipped = SentinelLocalIndex.load(\n", " path=\"hate_speech_model\", negative_to_positive_ratio=None, seed=42\n", @@ -3145,17 +3138,35 @@ "# The original is untouched, which is what makes it safe to loop over sizes. If each\n", "# call shrank the receiver, run two would start from run one's leftovers.\n", "print(f\"\\noriginal still intact: {shipped.positive_embeddings.shape[0]:,} positives\")" - ] + ], + "execution_count": 26, + "outputs": [ + { + "output_type": "stream", + "text": [ + "original: 1,516 positives / 15,160 negatives\n", + "resized: 500 positives / 2,500 negatives\n", + "took 2.2 ms - no encoding involved\n", + "\n", + "original still intact: 1,516 positives\n" + ], + "name": "stdout" + } + ], + "id": "0a5d9eed" }, { "cell_type": "markdown", - "id": "1ffe03e2", "metadata": {}, "source": [ "## 4. The tuning harness, cheapest entry point first\n", "\n", - "`sentinel.simulation` is layered by cost. Scoring runs the model and is the only\n", - "expensive step, so you do it once and then evaluate as many ways as you like for free:\n", + "`sentinel.simulation` arrived in 2.0 and is what the **Tuning aggregation strategies**\n", + "section above runs on. That section uses the two convenience entry points; this one shows\n", + "the full ladder, including the single-aggregator step underneath them.\n", + "\n", + "The harness is layered by cost. Scoring runs the model and is the only expensive step, so\n", + "you do it once and then evaluate as many ways as you like for free:\n", "\n", "| Function | What it does | Cost |\n", "|---|---|---|\n", @@ -3168,12 +3179,11 @@ "aggregator) is *how* a group's many scores become one number, and it is what you sweep.\n", "The **evaluation metric** is *how good* that separation is, and you do not choose one -\n", "every row reports all three families." - ] + ], + "id": "1ffe03e2" }, { "cell_type": "code", - "execution_count": 27, - "id": "5f7f90b1", "metadata": { "execution": { "iopub.execute_input": "2026-08-03T23:56:25.103361Z", @@ -3182,29 +3192,6 @@ "shell.execute_reply": "2026-08-03T23:56:25.160411Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " hateful_a label=1 10 observations\n", - " hateful_b label=1 10 observations\n", - " ordinary_a label=0 10 observations\n", - " ordinary_b label=0 10 observations\n", - "\n", - "built-in summarize metrics: skewness, mean_of_positives, top_k_mean, percentile_score, softmax_weighted_mean, max_score\n", - "\n", - "scored 4 groups; hateful_a has 10 observation scores\n", - "\n", - "skewness alone -> roc_auc=1.00 f1=1.00\n", - " ranking: roc_auc=1.00 recall_at_n=1.00\n", - " threshold: precision=1.00 recall=1.00\n", - " separation: mean_separation=0.333 ks=1.00\n", - "\n", - "n_groups=4 n_positive=2 (positive sources)\n" - ] - } - ], "source": [ "from sentinel.simulation import DEFAULT_AGGREGATORS\n", "\n", @@ -3245,11 +3232,35 @@ "# n_positive here counts positive GROUPS, not index rows - the distinction that the\n", "# index_ prefix exists to protect.\n", "print(f\"\\nn_groups={row['n_groups']} n_positive={row['n_positive']} (positive sources)\")" - ] + ], + "execution_count": 27, + "outputs": [ + { + "output_type": "stream", + "text": [ + " hateful_a label=1 10 observations\n", + " hateful_b label=1 10 observations\n", + " ordinary_a label=0 10 observations\n", + " ordinary_b label=0 10 observations\n", + "\n", + "built-in summarize metrics: skewness, mean_of_positives, top_k_mean, percentile_score, softmax_weighted_mean, max_score\n", + "\n", + "scored 4 groups; hateful_a has 10 observation scores\n", + "\n", + "skewness alone -> roc_auc=1.00 f1=1.00\n", + " ranking: roc_auc=1.00 recall_at_n=1.00\n", + " threshold: precision=1.00 recall=1.00\n", + " separation: mean_separation=0.333 ks=1.00\n", + "\n", + "n_groups=4 n_positive=2 (positive sources)\n" + ], + "name": "stdout" + } + ], + "id": "5f7f90b1" }, { "cell_type": "markdown", - "id": "b9abde0c", "metadata": {}, "source": [ "## 5. Grid search over the index itself\n", @@ -3259,12 +3270,11 @@ "`subsample()` is what makes exploring it affordable.\n", "\n", "Same four sources as above, so the only thing changing is the index." - ] + ], + "id": "b9abde0c" }, { "cell_type": "code", - "execution_count": 28, - "id": "578b8945", "metadata": { "execution": { "iopub.execute_input": "2026-08-03T23:56:25.164197Z", @@ -3273,16 +3283,38 @@ "shell.execute_reply": "2026-08-03T23:56:25.268331Z" } }, + "source": [ + "grid = pd.DataFrame(\n", + " run_grid_search(\n", + " demo_index,\n", + " groups,\n", + " n_positive_values=[3, 6], # new in 2.0: index size\n", + " neg_to_pos_ratios=[0.5, 1.0], # new in 2.0: index balance\n", + " top_k_values=[1, 3],\n", + " min_score_values=[0.0],\n", + " index_seed=42, # reproducible index configurations\n", + " )\n", + ")\n", + "\n", + "configurations = grid[[\"index_n_positive\", \"index_neg_to_pos_ratio\"]].drop_duplicates()\n", + "print(f\"\\n{len(grid)} rows across {len(configurations)} index configurations\")\n", + "grid[[\n", + " \"aggregator\", \"index_n_positive\", \"index_neg_to_pos_ratio\",\n", + " \"index_n_positive_actual\", \"index_n_negative_actual\", \"top_k\", \"roc_auc\",\n", + "]].head(6)" + ], + "execution_count": 28, "outputs": [ { - "name": "stdout", "output_type": "stream", "text": [ "\n", "48 rows across 4 index configurations\n" - ] + ], + "name": "stdout" }, { + "output_type": "execute_result", "data": { "text/html": [ "
\n", @@ -3395,35 +3427,14 @@ "5 3 1 1 1.0 " ] }, - "execution_count": 28, "metadata": {}, - "output_type": "execute_result" + "execution_count": 28 } ], - "source": [ - "grid = pd.DataFrame(\n", - " run_grid_search(\n", - " demo_index,\n", - " groups,\n", - " n_positive_values=[3, 6], # new in 2.0: index size\n", - " neg_to_pos_ratios=[0.5, 1.0], # new in 2.0: index balance\n", - " top_k_values=[1, 3],\n", - " min_score_values=[0.0],\n", - " index_seed=42, # reproducible index configurations\n", - " )\n", - ")\n", - "\n", - "configurations = grid[[\"index_n_positive\", \"index_neg_to_pos_ratio\"]].drop_duplicates()\n", - "print(f\"\\n{len(grid)} rows across {len(configurations)} index configurations\")\n", - "grid[[\n", - " \"aggregator\", \"index_n_positive\", \"index_neg_to_pos_ratio\",\n", - " \"index_n_positive_actual\", \"index_n_negative_actual\", \"top_k\", \"roc_auc\",\n", - "]].head(6)" - ] + "id": "578b8945" }, { "cell_type": "markdown", - "id": "ee6247da", "metadata": {}, "source": [ "### Two details in that table worth pausing on\n", @@ -3438,12 +3449,11 @@ "entirely different number from the index size, and the two briefly shared one name - so\n", "the index size silently overwrote the group count. Writing a column twice now raises\n", "rather than overwriting." - ] + ], + "id": "ee6247da" }, { "cell_type": "code", - "execution_count": 29, - "id": "fadb7c12", "metadata": { "execution": { "iopub.execute_input": "2026-08-03T23:56:25.272935Z", @@ -3452,10 +3462,18 @@ "shell.execute_reply": "2026-08-03T23:56:25.279853Z" } }, - "outputs": [ - { - "data": { - "text/html": [ + "source": [ + "# Same rows, two unrelated meanings, both surviving side by side.\n", + "# n_positive -> how many of our four sources are labelled positive (2)\n", + "# index_n_positive -> how many positive examples the index was built with (3 or 6)\n", + "grid[[\"n_positive\", \"index_n_positive\", \"index_n_positive_actual\"]].drop_duplicates()" + ], + "execution_count": 29, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/html": [ "
\n", "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
aggregatorindex_n_positiveindex_neg_to_pos_ratioindex_n_negative_actualtop_kmin_score_to_considerroc_aucf1
288skewness151610.01516030.00.9955560.966667
72skewness25010.0250030.00.9877780.949153
192skewness75010.0750050.00.9866670.935484
84skewness25010.0250050.00.9855560.947368
312skewness151610.015160100.00.9855560.937500
180skewness75010.0750030.00.9855560.967742
300skewness151610.01516050.00.9844440.952381
264skewness15165.0758050.00.9822220.949153
\n", + "
" + ], + "text/plain": [ + " aggregator index_n_positive index_neg_to_pos_ratio \\\n", + "288 skewness 1516 10.0 \n", + "72 skewness 250 10.0 \n", + "192 skewness 750 10.0 \n", + "84 skewness 250 10.0 \n", + "312 skewness 1516 10.0 \n", + "180 skewness 750 10.0 \n", + "300 skewness 1516 10.0 \n", + "264 skewness 1516 5.0 \n", + "\n", + " index_n_negative_actual top_k min_score_to_consider roc_auc f1 \n", + "288 15160 3 0.0 0.995556 0.966667 \n", + "72 2500 3 0.0 0.987778 0.949153 \n", + "192 7500 5 0.0 0.986667 0.935484 \n", + "84 2500 5 0.0 0.985556 0.947368 \n", + "312 15160 10 0.0 0.985556 0.937500 \n", + "180 7500 3 0.0 0.985556 0.967742 \n", + "300 15160 5 0.0 0.984444 0.952381 \n", + "264 7580 5 0.0 0.982222 0.949153 " + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "start = time.perf_counter()\n", + "grid = pd.DataFrame(\n", + " run_grid_search(\n", + " index,\n", + " groups,\n", + " n_positive_values=[250, 750, 1516], # 1516 is the whole index\n", + " neg_to_pos_ratios=[1.0, 5.0, 10.0],\n", + " top_k_values=[3, 5, 10],\n", + " min_score_values=[0.0, 0.1],\n", + " index_seed=42,\n", + " )\n", + ")\n", + "print(f\"{len(grid)} rows in {time.perf_counter() - start:.0f}s\\n\")\n", + "\n", + "best = grid.sort_values(\"roc_auc\", ascending=False)\n", + "best[[\n", + " \"aggregator\", \"index_n_positive\", \"index_neg_to_pos_ratio\",\n", + " \"index_n_negative_actual\", \"top_k\", \"min_score_to_consider\", \"roc_auc\", \"f1\",\n", + "]].head(8)" + ] + }, + { + "cell_type": "markdown", + "id": "07c94386", + "metadata": {}, + "source": [ + "### What each axis is worth\n", + "\n", + "Averaging over everything else shows which knobs move the needle. The `max` column\n", + "matters as much as the `mean`: a setting can be poor on average yet contain the single\n", + "best configuration." + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "id": "3207b644", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-04T00:30:21.357046Z", + "iopub.status.busy": "2026-08-04T00:30:21.356965Z", + "iopub.status.idle": "2026-08-04T00:30:21.370510Z", + "shell.execute_reply": "2026-08-04T00:30:21.369418Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "--- index_n_positive ---\n", + " mean max\n", + "index_n_positive \n", + "1516 0.791 0.996\n", + "750 0.759 0.987\n", + "250 0.718 0.988\n", + "\n", + "--- index_neg_to_pos_ratio ---\n", + " mean max\n", + "index_neg_to_pos_ratio \n", + "1.0 0.773 0.939\n", + "5.0 0.767 0.982\n", + "10.0 0.727 0.996\n", + "\n", + "--- top_k ---\n", + " mean max\n", + "top_k \n", + "3 0.797 0.996\n", + "5 0.771 0.987\n", + "10 0.700 0.986\n", + "\n", + "--- aggregator ---\n", + " mean max\n", + "aggregator \n", + "top_k_mean 0.809 0.890\n", + "max_score 0.794 0.871\n", + "skewness 0.765 0.996\n", + "percentile_score 0.737 0.919\n", + "softmax_weighted_mean 0.716 0.931\n", + "mean_of_positives 0.715 0.934\n", + "\n" + ] + } + ], + "source": [ + "for axis in (\"index_n_positive\", \"index_neg_to_pos_ratio\", \"top_k\", \"aggregator\"):\n", + " summary = (\n", + " grid.groupby(axis)[\"roc_auc\"]\n", + " .agg([\"mean\", \"max\"])\n", + " .round(3)\n", + " .sort_values(\"mean\", ascending=False)\n", + " )\n", + " print(f\"--- {axis} ---\")\n", + " print(summary.to_string())\n", + " print()" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "9b8cf896", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-04T00:30:21.372910Z", + "iopub.status.busy": "2026-08-04T00:30:21.372832Z", + "iopub.status.idle": "2026-08-04T00:30:21.771663Z", + "shell.execute_reply": "2026-08-04T00:30:21.770501Z" + } + }, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# How index size and balance interact, for the aggregator that wins overall.\n", + "winner = best.iloc[0][\"aggregator\"]\n", + "sub = grid[(grid[\"aggregator\"] == winner) & (grid[\"min_score_to_consider\"] == 0.0)]\n", + "pivot = sub.pivot_table(\n", + " index=\"index_n_positive\", columns=\"index_neg_to_pos_ratio\", values=\"roc_auc\", aggfunc=\"max\"\n", + ")\n", "\n", + "fig, ax = plt.subplots(figsize=(7, 4))\n", + "im = ax.imshow(pivot.values, cmap=\"viridis\", aspect=\"auto\")\n", + "ax.set_xticks(range(len(pivot.columns)), [str(c) for c in pivot.columns])\n", + "ax.set_yticks(range(len(pivot.index)), [str(i) for i in pivot.index])\n", + "ax.set_xlabel(\"negatives per positive\")\n", + "ax.set_ylabel(\"positive examples in index\")\n", + "ax.set_title(f\"best ROC-AUC for '{winner}' by index shape\")\n", + "for i in range(len(pivot.index)):\n", + " for j in range(len(pivot.columns)):\n", + " ax.text(j, i, f\"{pivot.values[i, j]:.3f}\", ha=\"center\", va=\"center\", color=\"white\")\n", + "fig.colorbar(im, ax=ax, label=\"ROC-AUC\")\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "9f393199", + "metadata": {}, + "source": [ + "### Conclusion\n", + "\n", + "**A bigger index is reliably better.** Mean ROC-AUC climbs with every step up in size -\n", + "250, then 750, then the full 1,516 positives - and the single best configuration uses\n", + "the whole index. There is no plateau within this range, which suggests the index is if\n", + "anything still too small.\n", + "\n", + "**Fewer neighbours beat more.** `top_k=3` wins on both mean and max, and performance\n", + "falls off steadily at 5 and 10. Widening the neighbourhood pulls in weaker matches that\n", + "dilute the signal.\n", + "\n", + "**Balance is the subtle one.** Averaged across everything, a 1:1 ratio looks best and\n", + "10:1 looks worst - yet the single best configuration in the whole sweep uses 10:1. More\n", + "negatives help the aggregator that can exploit them and hurt the ones that cannot, so\n", + "the average hides the effect entirely.\n", + "\n", + "**The same trap catches the aggregator.** Ranked by *average* ROC-AUC, `top_k_mean`\n", + "comes first and `skewness` only third. Ranked by *best achievable*, `skewness` is far\n", + "ahead of everything else. In other words skewness is the most configuration-sensitive\n", + "aggregator: mediocre when badly tuned, and the clear winner when tuned well.\n", + "\n", + "That is the argument for sweeping rather than picking a default. Choosing the aggregator\n", + "by its average would have selected `top_k_mean` and left roughly a tenth of a point of\n", + "ROC-AUC on the table - and it is exactly the kind of interaction you cannot see without\n", + "varying the index alongside everything else, which is what 2.0 makes cheap enough to do." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ "## Summary\n", "\n", "| Feature | What it replaces |\n", @@ -3789,7 +4257,8 @@ "runs in seconds. Treat the timings as illustrations of the shape of the improvement\n", "rather than as benchmarks - on a real corpus the encoding cost dominates far more, so the\n", "savings are correspondingly larger." - ] + ], + "id": "cb0944e7" } ], "metadata": { From 59637a0359b6766f7f1e97669a06e39d2c6c4fae Mon Sep 17 00:00:00 2001 From: Wei Xiao <11197323+wxiao0421@users.noreply.github.com> Date: Mon, 3 Aug 2026 18:37:09 -0700 Subject: [PATCH 6/7] Cover the summarize metrics, explainability and Docker in the 2.0 notes The release range starts at #21, not after it, so three things the notes had omitted or mis-attributed are in scope. 1.0 shipped two summarize metrics, mean_of_positives and skewness. 2.0 has six, adding top_k_mean, percentile_score, softmax_weighted_mean and max_score. An earlier draft of this file claimed the opposite - that all six predated the release - which was wrong, and understated it: the release both widened the choice and supplied the evidence to make it. Also documents the explainability fields on RareClassAffinityResult, and the Dockerfiles, neither of which appeared anywhere in the notes. Replaces the flat list of links with a table of the main changes and a one-line summary of each, so the file opens with what matters rather than an index. Co-authored-by: Cursor --- V2_FEATURES.md | 91 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 79 insertions(+), 12 deletions(-) diff --git a/V2_FEATURES.md b/V2_FEATURES.md index eaad908..cc232fd 100644 --- a/V2_FEATURES.md +++ b/V2_FEATURES.md @@ -5,15 +5,22 @@ hand-building the index, guessing at hyperparameters, and re-encoding your corpu time you wanted to try something different. Version 2.0 is about closing that loop: build an index in one call, resize it without -re-encoding, sweep the settings that matter, and see which text actually drove a score. +re-encoding, measure which settings actually work, and see which text drove a score. -- [Build an index in one call](#build-an-index-in-one-call) -- [Resize an index without re-encoding](#resize-an-index-without-re-encoding) -- [Tune with the simulation harness](#tune-with-the-simulation-harness) -- [Sweep index size and ratio](#sweep-index-size-and-ratio) -- [Explanations survive a reload](#explanations-survive-a-reload) -- [Reproducible loading](#reproducible-loading) -- [Migrating from 1.0](#migrating-from-10) +The main changes: + +| | | +|---|---| +| [One-line index building](#build-an-index-in-one-call) | `from_texts()` replaces an eight-step recipe with two silent failure modes | +| [Resizing without re-encoding](#resize-an-index-without-re-encoding) | `subsample()` makes index size an affordable thing to explore | +| [Six summarize metrics, not two](#more-ways-to-summarize-and-ways-to-measure-which-one-works) | Plus explainability, and the evaluation metrics to choose between them | +| [A tuning harness](#tune-with-the-simulation-harness) | Measure configurations on your own labelled data, numpy-only | +| [Index sweep axes](#sweep-index-size-and-ratio) | Grid search over index size and balance, encoding the observations once | +| [Explanations that survive a reload](#explanations-survive-a-reload) | The corpus is persisted, so explanations name text rather than row numbers | +| [Reproducible loading](#reproducible-loading) | A saved index behaves identically on every load | +| [Run it in a container](#run-it-in-a-container) | Dockerfiles for GPU and CPU-only use | + +Migration notes are at the end: [Migrating from 1.0](#migrating-from-10). ## Build an index in one call @@ -92,11 +99,58 @@ scored = score_groups(index, groups, top_k=5) # expensive, once pd.DataFrame(compare_aggregators(scored)) # cheap, all six aggregators ``` -Every row carries all three families: +## More ways to summarize, and ways to measure which one works + +1.0 gave you two summarize metrics: `mean_of_positives` and `skewness`. That is not much of +a choice, and no way at all to know whether either suited your data. + +2.0 brings the count to six and adds the means to decide between them. + +**Four new summarize metrics** in `sentinel.score_formulae`, alongside the original two: + +| Metric | Shape it looks for | +|---|---| +| `skewness` (default) | A few spikes in an otherwise flat set of scores, regardless of count | +| `mean_of_positives` | Overall level among scores that cleared the floor | +| `top_k_mean` | The strongest handful of signals | +| `percentile_score` | A high percentile, robust to single outliers | +| `softmax_weighted_mean` | Smoothly emphasises higher scores | +| `max_score` | The single strongest signal | + +**Explainability on every result.** `RareClassAffinityResult` now carries +`aggregation_name`, `aggregation_stats` and per-text `explanations` - which neighbours were +closest, their similarities, and the contrastive terms behind the score. You can see *why* a +source scored as it did, not just that it did. + +**Evaluation metrics.** Three families, reported on every result row, because "good" depends +on what you are building: + +| Family | Metrics | Answers | +|---|---|---| +| Ranking | `roc_auc`, `recall_at_n`, `precision_at_n`, `rank_ratio` | Do the known positives come out on top? Right for a review queue. | +| Threshold | `precision`, `recall`, `f1`, `false_positive_rate` | Of the sources I flagged, how many were right? Right for automatic action. | +| Separation | `mean_separation`, `cohens_d`, `ks_statistic` | How far apart are the two populations, independent of any cutoff? Right for monitoring drift. | + +You do not pick a family. All three are computed, so you read whichever matches your use +case - and you can see when they disagree, which they do: an aggregator can rank well while +having a mediocre best F1. + +**A way to compare them side by side.** `DEFAULT_AGGREGATORS` is a name-to-function map of +all six, so `compare_aggregators` can evaluate every one of them on a single set of scores, +and `evaluate_groups` can measure any one of them on its own: + +```python +from sentinel.simulation import DEFAULT_AGGREGATORS, compare_aggregators, evaluate_groups + +# All six, ranked however you like. +pd.DataFrame(compare_aggregators(scored)).sort_values("roc_auc", ascending=False) -- Ranking: `roc_auc`, `recall_at_n`, `precision_at_n`, `rank_ratio` -- Threshold: `precision`, `recall`, `f1`, `false_positive_rate` -- Separation: `mean_separation`, `cohens_d`, `ks_statistic` +# Or one at a time, with all three families reported. +evaluate_groups(scored, DEFAULT_AGGREGATORS["skewness"], aggregator_name="skewness") +``` + +This is what turns six options into a decision you can defend with evidence rather than a +default you inherited. ## Sweep index size and ratio @@ -173,6 +227,19 @@ index = SentinelLocalIndex.load(path="...", seed=42) Seeding uses a private generator, so your own randomness is untouched. +## Run it in a container + +2.0 ships Dockerfiles, so you can try Sentinel without resolving a Python and PyTorch +environment first. There are two: the default is GPU-capable, and `Dockerfile.cpu` is a +smaller CPU-only build for when you are only scoring. + +```bash +docker build -t sentinel . # GPU-capable +docker build -f Dockerfile.cpu -t sentinel:cpu . # smaller, CPU only +``` + +See the README for running the demo and mounting your own scripts. + ## Migrating from 1.0 Most code needs no change. The breaking changes are deliberate and narrow. From cf532e0e7ed2176450ab2e69915f68886a0834c9 Mon Sep 17 00:00:00 2001 From: Wei Xiao <11197323+wxiao0421@users.noreply.github.com> Date: Thu, 6 Aug 2026 10:34:55 -0700 Subject: [PATCH 7/7] Require Python 3.10, and resync the lock 1.0 declared support for Python 3.9 in both the constraint and the classifiers, but CI has only ever run 3.10 to 3.12, so the claim was never verified. It had also stopped being possible to honour: current torch requires 3.10 or newer, and the constraint here allows anything up to 3.0, so a 3.9 install had to silently resolve to an older torch that nobody tests. An unverified promise that quietly hands users a different dependency set is worse than no promise. The source itself was already 3.9-clean - every file in src/ parses under ast.parse(feature_version=(3, 9)) and there are no 3.10-only runtime APIs - so this is about what the metadata claims rather than about fixing breakage. Raising the minimum is a breaking change, which is why it goes in the major release rather than waiting. Relocking drops three backports whose functionality is in the standard library from 3.10: importlib-metadata, importlib-resources and zipp. Also removes 200-odd python_version < "3.10" markers that can no longer fire. No package versions change, and no packages are added. The lock had to be regenerated because it records the python constraint and a content-hash derived from pyproject.toml; leaving it stale would fail the poetry install that CI runs. Regenerated with Poetry 2.4.1 to preserve lock-version 2.1 - relocking with the older Poetry on this machine silently rewrote it to 2.0. Both versions validate the result. Co-authored-by: Cursor --- V2_FEATURES.md | 7 + poetry.lock | 428 +++++++++---------------------------------------- pyproject.toml | 3 +- 3 files changed, 86 insertions(+), 352 deletions(-) diff --git a/V2_FEATURES.md b/V2_FEATURES.md index cc232fd..0c0bf35 100644 --- a/V2_FEATURES.md +++ b/V2_FEATURES.md @@ -244,6 +244,13 @@ See the README for running the demo and mounting your own scripts. Most code needs no change. The breaking changes are deliberate and narrow. +**Python 3.10 is now the minimum.** 1.0 declared support for 3.9, but never tested it - CI has +run 3.10 to 3.12 only. That claim had also become impossible to honour: current `torch` +requires 3.10 or newer, so a 3.9 install had to silently resolve to an older, untested torch. +Raising the floor is a breaking change, which is why it belongs in a major release. Dropping +3.9 also lets three backport packages go - `importlib-metadata`, `importlib-resources` and +`zipp` - whose functionality is in the standard library from 3.10. + **Arguments after the first are now keyword-only** on `calculate_rare_class_affinity`, `from_texts` and `load`. This is what lets a new argument sit in a logical place instead of being appended to an eleven-parameter list forever. diff --git a/poetry.lock b/poetry.lock index 01a4cba..03994e0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -7,7 +7,6 @@ description = "Happy Eyeballs for asyncio" optional = false python-versions = ">=3.9" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"}, {file = "aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558"}, @@ -20,7 +19,6 @@ description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.9" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "aiohttp-3.13.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:02222e7e233295f40e011c1b00e3b0bd451f22cf853a0304c3595633ee47da4b"}, {file = "aiohttp-3.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bace460460ed20614fa6bc8cb09966c0b8517b8c58ad8046828c6078d25333b5"}, @@ -155,7 +153,7 @@ propcache = ">=0.2.0" yarl = ">=1.17.0,<2.0" [package.extras] -speedups = ["Brotli (>=1.2)", "aiodns (>=3.3.0)", "backports.zstd", "brotlicffi (>=1.2)"] +speedups = ["Brotli (>=1.2) ; platform_python_implementation == \"CPython\"", "aiodns (>=3.3.0)", "backports.zstd ; platform_python_implementation == \"CPython\" and python_version < \"3.14\"", "brotlicffi (>=1.2) ; platform_python_implementation != \"CPython\""] [[package]] name = "aiosignal" @@ -164,7 +162,6 @@ description = "aiosignal: a list of registered asynchronous callbacks" optional = false python-versions = ">=3.9" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e"}, {file = "aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7"}, @@ -181,7 +178,6 @@ description = "A light, configurable Sphinx theme" optional = false python-versions = ">=3.9" groups = ["docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"}, {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, @@ -194,7 +190,6 @@ description = "High-level concurrency and networking framework on top of asyncio optional = false python-versions = ">=3.9" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c"}, {file = "anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703"}, @@ -206,7 +201,7 @@ idna = ">=2.8" typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] -trio = ["trio (>=0.31.0)", "trio (>=0.32.0)"] +trio = ["trio (>=0.31.0) ; python_version < \"3.10\"", "trio (>=0.32.0) ; python_version >= \"3.10\""] [[package]] name = "appnope" @@ -215,7 +210,7 @@ description = "Disable App Nap on macOS >= 10.9" optional = false python-versions = ">=3.6" groups = ["examples"] -markers = "(python_version <= \"3.11\" or python_version >= \"3.12\") and platform_system == \"Darwin\"" +markers = "platform_system == \"Darwin\"" files = [ {file = "appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c"}, {file = "appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee"}, @@ -228,7 +223,6 @@ description = "Argon2 for Python" optional = false python-versions = ">=3.8" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741"}, {file = "argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1"}, @@ -244,7 +238,6 @@ description = "Low-level CFFI bindings for Argon2" optional = false python-versions = ">=3.9" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:3d3f05610594151994ca9ccb3c771115bdb4daef161976a266f0dd8aa9996b8f"}, {file = "argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8b8efee945193e667a396cbc7b4fb7d357297d6234d30a489905d96caabde56b"}, @@ -287,7 +280,6 @@ description = "Better dates & times for Python" optional = false python-versions = ">=3.8" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "arrow-1.4.0-py3-none-any.whl", hash = "sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205"}, {file = "arrow-1.4.0.tar.gz", hash = "sha256:ed0cc050e98001b8779e84d461b0098c4ac597e88704a655582b21d116e526d7"}, @@ -308,7 +300,6 @@ description = "Annotate AST trees with source code positions" optional = false python-versions = ">=3.8" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "asttokens-3.0.2-py3-none-any.whl", hash = "sha256:9da13157f5b28becde0bd374fc677dcd3c290614264eff096f167c469cd9f933"}, {file = "asttokens-3.0.2.tar.gz", hash = "sha256:3ecdbd8f2cc195f53ccada3a613538bb5f9ef6f6869129f13e03c30a677b8fe2"}, @@ -325,7 +316,6 @@ description = "Simple LRU cache for asyncio" optional = false python-versions = ">=3.9" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "async_lru-2.0.5-py3-none-any.whl", hash = "sha256:ab95404d8d2605310d345932697371a5f40def0487c03d6d0ad9138de52c9943"}, {file = "async_lru-2.0.5.tar.gz", hash = "sha256:481d52ccdd27275f42c43a928b4a50c3bfb2d67af4e78b170e3e0bb39c66e5bb"}, @@ -341,7 +331,7 @@ description = "Timeout context manager for asyncio programs" optional = false python-versions = ">=3.8" groups = ["examples"] -markers = "python_version < \"3.11\"" +markers = "python_version == \"3.10\"" files = [ {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, @@ -354,7 +344,6 @@ description = "Classes Without Boilerplate" optional = false python-versions = ">=3.9" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309"}, {file = "attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32"}, @@ -367,14 +356,13 @@ description = "Internationalization utilities" optional = false python-versions = ">=3.8" groups = ["docs", "examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35"}, {file = "babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d"}, ] [package.extras] -dev = ["backports.zoneinfo", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata"] +dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""] [[package]] name = "beautifulsoup4" @@ -383,7 +371,6 @@ description = "Screen-scraping library" optional = false python-versions = ">=3.7.0" groups = ["docs", "examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9"}, {file = "beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7"}, @@ -407,7 +394,6 @@ description = "The uncompromising code formatter." optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "black-25.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ec311e22458eec32a807f029b2646f661e6859c3f61bc6d9ffb67958779f392e"}, {file = "black-25.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1032639c90208c15711334d681de2e24821af0575573db2810b0763bcd62e0f0"}, @@ -460,7 +446,6 @@ description = "An easy safelist-based HTML-sanitizing tool." optional = false python-versions = ">=3.9" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "bleach-6.2.0-py3-none-any.whl", hash = "sha256:117d9c6097a7c3d22fd578fcd8d35ff1e125df6736f554da4e432fdd63f31e5e"}, {file = "bleach-6.2.0.tar.gz", hash = "sha256:123e894118b8a599fd80d3ec1a6d4cc7ce4e5882b1317a7e1ba69b56e95f991f"}, @@ -480,7 +465,6 @@ description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" groups = ["main", "docs", "examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db"}, {file = "certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432"}, @@ -493,7 +477,6 @@ description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.9" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, @@ -591,7 +574,6 @@ description = "Validate configuration and produce human readable error messages. optional = false python-versions = ">=3.8" groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, @@ -604,7 +586,6 @@ description = "The Real First Universal Charset Detector. Open, modern and activ optional = false python-versions = ">=3.7" groups = ["main", "docs", "examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "charset_normalizer-3.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a"}, {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616"}, @@ -708,7 +689,6 @@ description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, @@ -728,7 +708,7 @@ files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -markers = {main = "(python_version <= \"3.11\" or python_version >= \"3.12\") and platform_system == \"Windows\"", dev = "(sys_platform == \"win32\" or platform_system == \"Windows\") and (python_version <= \"3.11\" or python_version >= \"3.12\")", docs = "(python_version <= \"3.11\" or python_version >= \"3.12\") and sys_platform == \"win32\"", examples = "(platform_system == \"Windows\" or sys_platform == \"win32\") and (python_version <= \"3.11\" or python_version >= \"3.12\")"} +markers = {main = "platform_system == \"Windows\"", dev = "sys_platform == \"win32\" or platform_system == \"Windows\"", docs = "sys_platform == \"win32\"", examples = "platform_system == \"Windows\" or sys_platform == \"win32\""} [[package]] name = "comm" @@ -737,7 +717,6 @@ description = "Jupyter Python Comm implementation, for usage in ipykernel, xeus- optional = false python-versions = ">=3.8" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417"}, {file = "comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971"}, @@ -753,7 +732,6 @@ description = "Python library for calculating contours of 2D quadrilateral grids optional = false python-versions = ">=3.9" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "contourpy-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:880ea32e5c774634f9fcd46504bf9f080a41ad855f4fef54f5380f5133d343c7"}, {file = "contourpy-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:76c905ef940a4474a6289c71d53122a4f77766eef23c03cd57016ce19d0f7b42"}, @@ -839,7 +817,6 @@ description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a"}, {file = "coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5"}, @@ -951,7 +928,7 @@ files = [ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} [package.extras] -toml = ["tomli"] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "cycler" @@ -960,7 +937,6 @@ description = "Composable style cycles" optional = false python-versions = ">=3.8" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30"}, {file = "cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"}, @@ -977,7 +953,6 @@ description = "HuggingFace community-driven open-source library of datasets" optional = false python-versions = ">=3.8.0" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "datasets-2.21.0-py3-none-any.whl", hash = "sha256:25e4e097110ce28824b746a107727ada94024cba11db8bc588d468414692b65a"}, {file = "datasets-2.21.0.tar.gz", hash = "sha256:998f85a8460f1bd982e5bd058f8a0808eef424249e3df1e8cdd594ccd0dc8ba2"}, @@ -1001,9 +976,9 @@ xxhash = "*" [package.extras] apache-beam = ["apache-beam (>=2.26.0)"] -audio = ["librosa", "soundfile (>=0.12.1)", "soxr (>=0.4.0)"] +audio = ["librosa", "soundfile (>=0.12.1)", "soxr (>=0.4.0) ; python_version >= \"3.9\""] benchmarks = ["tensorflow (==2.12.0)", "torch (==2.0.1)", "transformers (==4.30.1)"] -dev = ["Pillow (>=9.4.0)", "absl-py", "decorator", "elasticsearch (<8.0.0)", "faiss-cpu (>=1.8.0.post1)", "jax (>=0.3.14)", "jaxlib (>=0.3.14)", "joblib (<1.3.0)", "joblibspark", "librosa", "lz4", "moto[server]", "polars[timezone] (>=0.20.0)", "protobuf (<4.0.0)", "py7zr", "pyspark (>=3.4)", "pytest", "pytest-datadir", "pytest-xdist", "rarfile (>=4.0)", "ruff (>=0.3.0)", "s3fs", "s3fs (>=2021.11.1)", "soundfile (>=0.12.1)", "soxr (>=0.4.0)", "sqlalchemy", "tensorflow (>=2.16.0)", "tensorflow (>=2.6.0)", "tensorflow (>=2.6.0)", "tiktoken", "torch", "torch (>=2.0.0)", "transformers", "transformers (>=4.42.0)", "typing-extensions (>=4.6.1)", "zstandard"] +dev = ["Pillow (>=9.4.0)", "absl-py", "decorator", "elasticsearch (<8.0.0)", "faiss-cpu (>=1.8.0.post1)", "jax (>=0.3.14) ; sys_platform != \"win32\"", "jaxlib (>=0.3.14) ; sys_platform != \"win32\"", "joblib (<1.3.0)", "joblibspark", "librosa", "lz4", "moto[server]", "polars[timezone] (>=0.20.0)", "protobuf (<4.0.0)", "py7zr", "pyspark (>=3.4)", "pytest", "pytest-datadir", "pytest-xdist", "rarfile (>=4.0)", "ruff (>=0.3.0)", "s3fs", "s3fs (>=2021.11.1)", "soundfile (>=0.12.1)", "soxr (>=0.4.0) ; python_version >= \"3.9\"", "sqlalchemy", "tensorflow (>=2.16.0) ; python_version >= \"3.10\"", "tensorflow (>=2.6.0)", "tensorflow (>=2.6.0) ; python_version < \"3.10\"", "tiktoken", "torch", "torch (>=2.0.0)", "transformers", "transformers (>=4.42.0)", "typing-extensions (>=4.6.1)", "zstandard"] docs = ["s3fs", "tensorflow (>=2.6.0)", "torch", "transformers"] jax = ["jax (>=0.3.14)", "jaxlib (>=0.3.14)"] metrics-tests = ["Werkzeug (>=1.0.1)", "accelerate", "bert-score (>=0.3.6)", "jiwer", "langdetect", "mauve-text", "nltk (<3.8.2)", "requests-file (>=1.5.1)", "rouge-score", "sacrebleu", "sacremoses", "scikit-learn", "scipy", "sentencepiece", "seqeval", "six (>=1.15.0,<1.16.0)", "spacy (>=3.0.0)", "texttable (>=1.6.3)", "tldextract", "tldextract (>=3.1.0)", "toml (>=0.10.1)", "typer (<0.5.0)"] @@ -1011,8 +986,8 @@ quality = ["ruff (>=0.3.0)"] s3 = ["s3fs"] tensorflow = ["tensorflow (>=2.6.0)"] tensorflow-gpu = ["tensorflow (>=2.6.0)"] -tests = ["Pillow (>=9.4.0)", "absl-py", "decorator", "elasticsearch (<8.0.0)", "faiss-cpu (>=1.8.0.post1)", "jax (>=0.3.14)", "jaxlib (>=0.3.14)", "joblib (<1.3.0)", "joblibspark", "librosa", "lz4", "moto[server]", "polars[timezone] (>=0.20.0)", "protobuf (<4.0.0)", "py7zr", "pyspark (>=3.4)", "pytest", "pytest-datadir", "pytest-xdist", "rarfile (>=4.0)", "s3fs (>=2021.11.1)", "soundfile (>=0.12.1)", "soxr (>=0.4.0)", "sqlalchemy", "tensorflow (>=2.16.0)", "tensorflow (>=2.6.0)", "tiktoken", "torch (>=2.0.0)", "transformers (>=4.42.0)", "typing-extensions (>=4.6.1)", "zstandard"] -tests-numpy2 = ["Pillow (>=9.4.0)", "absl-py", "decorator", "elasticsearch (<8.0.0)", "jax (>=0.3.14)", "jaxlib (>=0.3.14)", "joblib (<1.3.0)", "joblibspark", "librosa", "lz4", "moto[server]", "polars[timezone] (>=0.20.0)", "protobuf (<4.0.0)", "py7zr", "pyspark (>=3.4)", "pytest", "pytest-datadir", "pytest-xdist", "rarfile (>=4.0)", "s3fs (>=2021.11.1)", "soundfile (>=0.12.1)", "soxr (>=0.4.0)", "sqlalchemy", "tiktoken", "torch (>=2.0.0)", "typing-extensions (>=4.6.1)", "zstandard"] +tests = ["Pillow (>=9.4.0)", "absl-py", "decorator", "elasticsearch (<8.0.0)", "faiss-cpu (>=1.8.0.post1)", "jax (>=0.3.14) ; sys_platform != \"win32\"", "jaxlib (>=0.3.14) ; sys_platform != \"win32\"", "joblib (<1.3.0)", "joblibspark", "librosa", "lz4", "moto[server]", "polars[timezone] (>=0.20.0)", "protobuf (<4.0.0)", "py7zr", "pyspark (>=3.4)", "pytest", "pytest-datadir", "pytest-xdist", "rarfile (>=4.0)", "s3fs (>=2021.11.1)", "soundfile (>=0.12.1)", "soxr (>=0.4.0) ; python_version >= \"3.9\"", "sqlalchemy", "tensorflow (>=2.16.0) ; python_version >= \"3.10\"", "tensorflow (>=2.6.0) ; python_version < \"3.10\"", "tiktoken", "torch (>=2.0.0)", "transformers (>=4.42.0)", "typing-extensions (>=4.6.1)", "zstandard"] +tests-numpy2 = ["Pillow (>=9.4.0)", "absl-py", "decorator", "elasticsearch (<8.0.0)", "jax (>=0.3.14) ; sys_platform != \"win32\"", "jaxlib (>=0.3.14) ; sys_platform != \"win32\"", "joblib (<1.3.0)", "joblibspark", "librosa", "lz4", "moto[server]", "polars[timezone] (>=0.20.0)", "protobuf (<4.0.0)", "py7zr", "pyspark (>=3.4)", "pytest", "pytest-datadir", "pytest-xdist", "rarfile (>=4.0)", "s3fs (>=2021.11.1)", "soundfile (>=0.12.1)", "soxr (>=0.4.0) ; python_version >= \"3.9\"", "sqlalchemy", "tiktoken", "torch (>=2.0.0)", "typing-extensions (>=4.6.1)", "zstandard"] torch = ["torch"] vision = ["Pillow (>=9.4.0)"] @@ -1023,7 +998,6 @@ description = "An implementation of the Debug Adapter Protocol for Python" optional = false python-versions = ">=3.8" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "debugpy-1.8.21-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:8eeab7b5462f683452c57c0126aaa5ec4e974ddb705f39ba87dff8818c8e08f9"}, {file = "debugpy-1.8.21-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:0fddfdc130ac6d8bfc0415b0409822fa901c8f310e5c945ac5653a0352532344"}, @@ -1064,7 +1038,6 @@ description = "Decorators for Humans" optional = false python-versions = ">=3.8" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "decorator-5.3.1-py3-none-any.whl", hash = "sha256:f47fe6fdbd2edd623ecfe36875d37aba411624e2670dd395dddae1358689bb3c"}, {file = "decorator-5.3.1.tar.gz", hash = "sha256:4cbcdd55a6efadb9dbea26b858f4fb3264567b52d69ca0d25b721b553f60ea82"}, @@ -1077,7 +1050,6 @@ description = "XML bomb protection for Python stdlib modules" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, @@ -1090,7 +1062,6 @@ description = "serialize all of Python" optional = false python-versions = ">=3.8" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7"}, {file = "dill-0.3.8.tar.gz", hash = "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca"}, @@ -1107,7 +1078,6 @@ description = "Distribution utilities" optional = false python-versions = "*" groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "distlib-0.4.3-py2.py3-none-any.whl", hash = "sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b"}, {file = "distlib-0.4.3.tar.gz", hash = "sha256:f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed"}, @@ -1120,7 +1090,6 @@ description = "Docutils -- Python Documentation Utilities" optional = false python-versions = ">=3.9" groups = ["docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, @@ -1133,7 +1102,7 @@ description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" groups = ["dev", "examples"] -markers = "python_version < \"3.11\"" +markers = "python_version == \"3.10\"" files = [ {file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"}, {file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"}, @@ -1152,14 +1121,13 @@ description = "Get the currently executing AST node of a frame, and other inform optional = false python-versions = ">=3.8" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017"}, {file = "executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4"}, ] [package.extras] -tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"] +tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich ; python_version >= \"3.11\""] [[package]] name = "fastjsonschema" @@ -1168,7 +1136,6 @@ description = "Fastest Python implementation of JSON schema" optional = false python-versions = "*" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463"}, {file = "fastjsonschema-2.21.2.tar.gz", hash = "sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de"}, @@ -1179,15 +1146,14 @@ devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benc [[package]] name = "filelock" -version = "3.19.1" +version = "3.32.2" description = "A platform independent file lock." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main", "dev", "examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d"}, - {file = "filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58"}, + {file = "filelock-3.32.2-py3-none-any.whl", hash = "sha256:87dd94cf281e586d135fa51132b8e3d9a598b316e90377a288663c9321036c82"}, + {file = "filelock-3.32.2.tar.gz", hash = "sha256:c33351e1f49cae33414acbc6d56784e6ecee82514ec90795da1161fc4836b5b8"}, ] [[package]] @@ -1197,7 +1163,6 @@ description = "the modular source code checker: pep8 pyflakes and co" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e"}, {file = "flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872"}, @@ -1215,7 +1180,6 @@ description = "Tools to manipulate font files" optional = false python-versions = ">=3.9" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "fonttools-4.60.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e36fadcf7e8ca6e34d490eef86ed638d6fd9c55d2f514b05687622cfc4a7050"}, {file = "fonttools-4.60.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6e500fc9c04bee749ceabfc20cb4903f6981c2139050d85720ea7ada61b75d5c"}, @@ -1278,17 +1242,17 @@ files = [ ] [package.extras] -all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "pycairo", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.45.0)", "unicodedata2 (>=17.0.0)", "xattr", "zopfli (>=0.1.4)"] +all = ["brotli (>=1.0.1) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\"", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres ; platform_python_implementation == \"PyPy\"", "pycairo", "scipy ; platform_python_implementation != \"PyPy\"", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.45.0)", "unicodedata2 (>=17.0.0) ; python_version <= \"3.14\"", "xattr ; sys_platform == \"darwin\"", "zopfli (>=0.1.4)"] graphite = ["lz4 (>=1.7.4.2)"] -interpolatable = ["munkres", "pycairo", "scipy"] +interpolatable = ["munkres ; platform_python_implementation == \"PyPy\"", "pycairo", "scipy ; platform_python_implementation != \"PyPy\""] lxml = ["lxml (>=4.0)"] pathops = ["skia-pathops (>=0.5.0)"] plot = ["matplotlib"] repacker = ["uharfbuzz (>=0.45.0)"] symfont = ["sympy"] -type1 = ["xattr"] -unicode = ["unicodedata2 (>=17.0.0)"] -woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] +type1 = ["xattr ; sys_platform == \"darwin\""] +unicode = ["unicodedata2 (>=17.0.0) ; python_version <= \"3.14\""] +woff = ["brotli (>=1.0.1) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\"", "zopfli (>=0.1.4)"] [[package]] name = "fqdn" @@ -1297,7 +1261,6 @@ description = "Validates fully-qualified domain names against RFC 1123, so that optional = false python-versions = ">=2.7, !=3.0, !=3.1, !=3.2, !=3.3, !=3.4, <4" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014"}, {file = "fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f"}, @@ -1310,7 +1273,6 @@ description = "A list-like structure which implements collections.abc.MutableSeq optional = false python-versions = ">=3.9" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011"}, {file = "frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565"}, @@ -1451,7 +1413,6 @@ description = "File-system specification" optional = false python-versions = ">=3.8" groups = ["main", "examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "fsspec-2024.6.1-py3-none-any.whl", hash = "sha256:3cb443f8bcd2efb31295a5b9fdb02aee81d8452c80d28f97a6d0959e6cee101e"}, {file = "fsspec-2024.6.1.tar.gz", hash = "sha256:fad7d7e209dd4c1208e3bbfda706620e0da5142bebbd9c384afb95b07e798e49"}, @@ -1495,7 +1456,6 @@ description = "A clean customisable Sphinx documentation theme." optional = false python-versions = ">=3.8" groups = ["docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "furo-2024.8.6-py3-none-any.whl", hash = "sha256:6cd97c58b47813d3619e63e9081169880fbe331f0ca883c871ff1f3f11814f5c"}, {file = "furo-2024.8.6.tar.gz", hash = "sha256:b63e4cee8abfc3136d3bc03a3d45a76a850bada4d6374d24c1716b0e01394a01"}, @@ -1505,7 +1465,7 @@ files = [ beautifulsoup4 = "*" pygments = ">=2.7" sphinx = ">=6.0,<9.0" -sphinx-basic-ng = ">=1.0.0.beta2" +sphinx-basic-ng = ">=1.0.0b2" [[package]] name = "h11" @@ -1514,7 +1474,6 @@ description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.8" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, @@ -1527,7 +1486,7 @@ description = "Fast transfer of large files with the Hugging Face Hub." optional = false python-versions = ">=3.8" groups = ["main", "examples"] -markers = "(python_version <= \"3.11\" or python_version >= \"3.12\") and (platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"arm64\" or platform_machine == \"aarch64\")" +markers = "platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"arm64\" or platform_machine == \"aarch64\"" files = [ {file = "hf_xet-1.5.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:4a5ecb9cda8512ba2aa8ee5d37c87a1422992165892d653098c7b90247481c3b"}, {file = "hf_xet-1.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8764488197c1d7b1378c8438c18d2eea902e150dbca0b0f0d2d32603fb9b5576"}, @@ -1558,7 +1517,6 @@ description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, @@ -1581,7 +1539,6 @@ description = "The next generation HTTP client." optional = false python-versions = ">=3.8" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, @@ -1594,7 +1551,7 @@ httpcore = "==1.*" idna = "*" [package.extras] -brotli = ["brotli", "brotlicffi"] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] @@ -1607,7 +1564,6 @@ description = "Client library to download and publish models, datasets and other optional = false python-versions = ">=3.8.0" groups = ["main", "examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "huggingface_hub-0.36.2-py3-none-any.whl", hash = "sha256:48f0c8eac16145dfce371e9d2d7772854a4f591bcb56c9cf548accf531d54270"}, {file = "huggingface_hub-0.36.2.tar.gz", hash = "sha256:1934304d2fb224f8afa3b87007d58501acfda9215b334eed53072dd5e815ff7a"}, @@ -1624,16 +1580,16 @@ tqdm = ">=4.42.1" typing-extensions = ">=3.7.4.3" [package.extras] -all = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "authlib (>=1.3.2)", "fastapi", "fastapi", "gradio (>=4.0.0)", "httpx", "itsdangerous", "jedi", "libcst (>=1.4.0)", "mypy (==1.15.0)", "mypy (>=1.14.1,<1.15.0)", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures (<16.0)", "pytest-vcr", "pytest-xdist", "ruff (>=0.9.0)", "soundfile", "ty", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] +all = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "authlib (>=1.3.2)", "fastapi", "fastapi", "gradio (>=4.0.0)", "httpx", "itsdangerous", "jedi", "libcst (>=1.4.0)", "mypy (==1.15.0) ; python_version >= \"3.9\"", "mypy (>=1.14.1,<1.15.0) ; python_version == \"3.8\"", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures (<16.0)", "pytest-vcr", "pytest-xdist", "ruff (>=0.9.0)", "soundfile", "ty", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] cli = ["InquirerPy (==0.3.4)"] -dev = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "authlib (>=1.3.2)", "fastapi", "fastapi", "gradio (>=4.0.0)", "httpx", "itsdangerous", "jedi", "libcst (>=1.4.0)", "mypy (==1.15.0)", "mypy (>=1.14.1,<1.15.0)", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures (<16.0)", "pytest-vcr", "pytest-xdist", "ruff (>=0.9.0)", "soundfile", "ty", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] +dev = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "authlib (>=1.3.2)", "fastapi", "fastapi", "gradio (>=4.0.0)", "httpx", "itsdangerous", "jedi", "libcst (>=1.4.0)", "mypy (==1.15.0) ; python_version >= \"3.9\"", "mypy (>=1.14.1,<1.15.0) ; python_version == \"3.8\"", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures (<16.0)", "pytest-vcr", "pytest-xdist", "ruff (>=0.9.0)", "soundfile", "ty", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] fastai = ["fastai (>=2.4)", "fastcore (>=1.3.27)", "toml"] hf-transfer = ["hf_transfer (>=0.1.4)"] hf-xet = ["hf-xet (>=1.1.2,<2.0.0)"] inference = ["aiohttp"] mcp = ["aiohttp", "mcp (>=1.8.0)", "typer"] oauth = ["authlib (>=1.3.2)", "fastapi", "httpx", "itsdangerous"] -quality = ["libcst (>=1.4.0)", "mypy (==1.15.0)", "mypy (>=1.14.1,<1.15.0)", "ruff (>=0.9.0)", "ty"] +quality = ["libcst (>=1.4.0)", "mypy (==1.15.0) ; python_version >= \"3.9\"", "mypy (>=1.14.1,<1.15.0) ; python_version == \"3.8\"", "ruff (>=0.9.0)", "ty"] tensorflow = ["graphviz", "pydot", "tensorflow"] tensorflow-testing = ["keras (<3.0)", "tensorflow"] testing = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "authlib (>=1.3.2)", "fastapi", "fastapi", "gradio (>=4.0.0)", "httpx", "itsdangerous", "jedi", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures (<16.0)", "pytest-vcr", "pytest-xdist", "soundfile", "urllib3 (<2.0)"] @@ -1647,7 +1603,6 @@ description = "File identification library for Python" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757"}, {file = "identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf"}, @@ -1663,7 +1618,6 @@ description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.9" groups = ["main", "docs", "examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2"}, {file = "idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848"}, @@ -1679,61 +1633,11 @@ description = "Getting image size from png/jpeg/jpeg2000/gif file" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" groups = ["docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "imagesize-1.5.0-py2.py3-none-any.whl", hash = "sha256:32677681b3f434c2cb496f00e89c5a291247b35b1f527589909e008057da5899"}, {file = "imagesize-1.5.0.tar.gz", hash = "sha256:8bfc5363a7f2133a89f0098451e0bcb1cd71aba4dc02bbcecb39d99d40e1b94f"}, ] -[[package]] -name = "importlib-metadata" -version = "8.7.1" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.9" -groups = ["docs", "examples"] -markers = "python_version < \"3.10\"" -files = [ - {file = "importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151"}, - {file = "importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb"}, -] - -[package.dependencies] -zipp = ">=3.20" - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=3.4)"] -perf = ["ipython"] -test = ["flufl.flake8", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] -type = ["mypy (<1.19)", "pytest-mypy (>=1.0.1)"] - -[[package]] -name = "importlib-resources" -version = "6.5.2" -description = "Read resources from Python packages" -optional = false -python-versions = ">=3.9" -groups = ["examples"] -markers = "python_version < \"3.10\"" -files = [ - {file = "importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec"}, - {file = "importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c"}, -] - -[package.dependencies] -zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["jaraco.test (>=5.4)", "pytest (>=6,!=8.1.*)", "zipp (>=3.17)"] -type = ["pytest-mypy"] - [[package]] name = "iniconfig" version = "2.1.0" @@ -1741,7 +1645,6 @@ description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.8" groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, @@ -1754,7 +1657,6 @@ description = "IPython Kernel for Jupyter" optional = false python-versions = ">=3.9" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "ipykernel-6.31.0-py3-none-any.whl", hash = "sha256:abe5386f6ced727a70e0eb0cf1da801fa7c5fa6ff82147747d5a0406cd8c94af"}, {file = "ipykernel-6.31.0.tar.gz", hash = "sha256:2372ce8bc1ff4f34e58cafed3a0feb2194b91fc7cad0fc72e79e47b45ee9e8f6"}, @@ -1789,7 +1691,6 @@ description = "IPython: Productive Interactive Computing" optional = false python-versions = ">=3.9" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "ipython-8.18.1-py3-none-any.whl", hash = "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397"}, {file = "ipython-8.18.1.tar.gz", hash = "sha256:ca6f079bb33457c66e233e4580ebfc4128855b4cf6370dddd73842a9563e8a27"}, @@ -1806,7 +1707,6 @@ prompt-toolkit = ">=3.0.41,<3.1.0" pygments = ">=2.4.0" stack-data = "*" traitlets = ">=5" -typing-extensions = {version = "*", markers = "python_version < \"3.10\""} [package.extras] all = ["black", "curio", "docrepr", "exceptiongroup", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] @@ -1828,7 +1728,6 @@ description = "Jupyter interactive widgets" optional = false python-versions = ">=3.7" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "ipywidgets-8.1.8-py3-none-any.whl", hash = "sha256:ecaca67aed704a338f88f67b1181b58f821ab5dc89c1f0f5ef99db43c1c2921e"}, {file = "ipywidgets-8.1.8.tar.gz", hash = "sha256:61f969306b95f85fba6b6986b7fe45d73124d1d9e3023a8068710d47a22ea668"}, @@ -1851,7 +1750,6 @@ description = "Operations with ISO 8601 durations" optional = false python-versions = ">=3.7" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042"}, {file = "isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9"}, @@ -1867,7 +1765,6 @@ description = "An autocompletion tool for Python that can be used for text edito optional = false python-versions = ">=3.6" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9"}, {file = "jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0"}, @@ -1888,7 +1785,6 @@ description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" groups = ["main", "docs", "examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, @@ -1907,7 +1803,7 @@ description = "Lightweight pipelining with Python functions" optional = true python-versions = ">=3.9" groups = ["main"] -markers = "(python_version <= \"3.11\" or python_version >= \"3.12\") and extra == \"sbert\"" +markers = "extra == \"sbert\"" files = [ {file = "joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713"}, {file = "joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3"}, @@ -1920,7 +1816,6 @@ description = "A Python implementation of the JSON5 data format." optional = false python-versions = ">=3.8.0" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "json5-0.15.0-py3-none-any.whl", hash = "sha256:56636a30c0e8a4665fe2179c0212f32eae3796dea89ea6f649b9436ecdb39618"}, {file = "json5-0.15.0.tar.gz", hash = "sha256:7424d1f1eb1d56da6e3d70643f53619862b4ce81440bdb8ecfd6f875e5ba4a71"}, @@ -1933,7 +1828,6 @@ description = "Identify specific nodes in a JSON document (RFC 6901)" optional = false python-versions = ">=3.7" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942"}, {file = "jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef"}, @@ -1946,7 +1840,6 @@ description = "An implementation of JSON Schema validation for Python" optional = false python-versions = ">=3.9" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63"}, {file = "jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85"}, @@ -1958,7 +1851,7 @@ fqdn = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} idna = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} isoduration = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} jsonpointer = {version = ">1.13", optional = true, markers = "extra == \"format-nongpl\""} -jsonschema-specifications = ">=2023.03.6" +jsonschema-specifications = ">=2023.3.6" referencing = ">=0.28.4" rfc3339-validator = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} rfc3986-validator = {version = ">0.1.0", optional = true, markers = "extra == \"format-nongpl\""} @@ -1978,7 +1871,6 @@ description = "The JSON Schema meta-schemas and vocabularies, exposed as a Regis optional = false python-versions = ">=3.9" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe"}, {file = "jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d"}, @@ -1994,7 +1886,6 @@ description = "Jupyter metapackage. Install all the Jupyter components in one go optional = false python-versions = "*" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "jupyter-1.1.1-py2.py3-none-any.whl", hash = "sha256:7a59533c22af65439b24bbe60373a4e95af8f16ac65a6c00820ad378e3f7cc83"}, {file = "jupyter-1.1.1.tar.gz", hash = "sha256:d55467bceabdea49d7e3624af7e33d59c37fff53ed3a350e1ac957bed731de7a"}, @@ -2015,14 +1906,12 @@ description = "Jupyter protocol implementation and client libraries" optional = false python-versions = ">=3.8" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f"}, {file = "jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419"}, ] [package.dependencies] -importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" python-dateutil = ">=2.8.2" pyzmq = ">=23.0" @@ -2031,7 +1920,7 @@ traitlets = ">=5.3" [package.extras] docs = ["ipykernel", "myst-parser", "pydata-sphinx-theme", "sphinx (>=4)", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] -test = ["coverage", "ipykernel (>=6.14)", "mypy", "paramiko", "pre-commit", "pytest (<8.2.0)", "pytest-cov", "pytest-jupyter[client] (>=0.4.1)", "pytest-timeout"] +test = ["coverage", "ipykernel (>=6.14)", "mypy", "paramiko ; sys_platform == \"win32\"", "pre-commit", "pytest (<8.2.0)", "pytest-cov", "pytest-jupyter[client] (>=0.4.1)", "pytest-timeout"] [[package]] name = "jupyter-console" @@ -2040,7 +1929,6 @@ description = "Jupyter terminal console" optional = false python-versions = ">=3.7" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "jupyter_console-6.6.3-py3-none-any.whl", hash = "sha256:309d33409fcc92ffdad25f0bcdf9a4a9daa61b6f341177570fdac03de5352485"}, {file = "jupyter_console-6.6.3.tar.gz", hash = "sha256:566a4bf31c87adbfadf22cdf846e3069b59a71ed5da71d6ba4d8aaad14a53539"}, @@ -2066,7 +1954,6 @@ description = "Jupyter core package. A base package on which Jupyter projects re optional = false python-versions = ">=3.8" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "jupyter_core-5.8.1-py3-none-any.whl", hash = "sha256:c28d268fc90fb53f1338ded2eb410704c5449a358406e8a948b75706e24863d0"}, {file = "jupyter_core-5.8.1.tar.gz", hash = "sha256:0a5f9706f70e64786b75acba995988915ebd4601c8a52e534a40b51c95f59941"}, @@ -2088,7 +1975,6 @@ description = "Jupyter Event System library" optional = false python-versions = ">=3.9" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "jupyter_events-0.12.1-py3-none-any.whl", hash = "sha256:c366585253f537a627da52fa7ca7410c5b5301fe893f511e7b077c2d93ec8bcf"}, {file = "jupyter_events-0.12.1.tar.gz", hash = "sha256:faff25f77218335752f35f23c5fe6e4a392a7bd99a5939ccb9b8fbf594636cf3"}, @@ -2116,14 +2002,12 @@ description = "Multi-Language Server WebSocket proxy for Jupyter Notebook/Lab se optional = false python-versions = ">=3.8" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "jupyter_lsp-2.3.1-py3-none-any.whl", hash = "sha256:71b954d834e85ff3096400554f2eefaf7fe37053036f9a782b0f7c5e42dadb81"}, {file = "jupyter_lsp-2.3.1.tar.gz", hash = "sha256:fdf8a4aa7d85813976d6e29e95e6a2c8f752701f926f2715305249a3829805a6"}, ] [package.dependencies] -importlib_metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} jupyter_server = ">=1.1.2" [[package]] @@ -2133,7 +2017,6 @@ description = "The backend—i.e. core services, APIs, and REST endpoints—to J optional = false python-versions = ">=3.9" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "jupyter_server-2.18.2-py3-none-any.whl", hash = "sha256:fa5e46539ded65791838035a2b6001f13e54d5f64b8b3752eb1e91fdd641a5b8"}, {file = "jupyter_server-2.18.2.tar.gz", hash = "sha256:06b4f40d8a7a00bb39d5216859c81374a0e7cfefe6d8a5a7facc5a5c37c679a7"}, @@ -2171,7 +2054,6 @@ description = "A Jupyter Server Extension Providing Terminals." optional = false python-versions = ">=3.8" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "jupyter_server_terminals-0.5.4-py3-none-any.whl", hash = "sha256:55be353fc74a80bc7f3b20e6be50a55a61cd525626f578dcb66a5708e2007d14"}, {file = "jupyter_server_terminals-0.5.4.tar.gz", hash = "sha256:bbda128ed41d0be9020349f9f1f2a4ab9952a73ed5f5ac9f1419794761fb87f5"}, @@ -2192,7 +2074,6 @@ description = "JupyterLab computational environment" optional = false python-versions = ">=3.9" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "jupyterlab-4.5.9-py3-none-any.whl", hash = "sha256:5ff0f908e8ac0afbed32b106fdef360f101c0a6654d1bf4a81e98a293ae1b336"}, {file = "jupyterlab-4.5.9.tar.gz", hash = "sha256:dd79a073fecae7a39066ea99e4627ed6c76269ac926e95a810e1e1df6358d865"}, @@ -2201,7 +2082,6 @@ files = [ [package.dependencies] async-lru = ">=1.0.0" httpx = ">=0.25.0,<1" -importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} ipykernel = ">=6.5.0,<6.30.0 || >6.30.0" jinja2 = ">=3.0.3" jupyter-core = "*" @@ -2229,7 +2109,6 @@ description = "Pygments theme using JupyterLab CSS variables" optional = false python-versions = ">=3.8" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780"}, {file = "jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d"}, @@ -2242,7 +2121,6 @@ description = "A set of server components for JupyterLab and JupyterLab like app optional = false python-versions = ">=3.8" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "jupyterlab_server-2.28.0-py3-none-any.whl", hash = "sha256:e4355b148fdcf34d312bbbc80f22467d6d20460e8b8736bf235577dd18506968"}, {file = "jupyterlab_server-2.28.0.tar.gz", hash = "sha256:35baa81898b15f93573e2deca50d11ac0ae407ebb688299d3a5213265033712c"}, @@ -2250,7 +2128,6 @@ files = [ [package.dependencies] babel = ">=2.10" -importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} jinja2 = ">=3.0.3" json5 = ">=0.9.0" jsonschema = ">=4.18.0" @@ -2270,7 +2147,6 @@ description = "Jupyter interactive widgets for JupyterLab" optional = false python-versions = ">=3.7" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "jupyterlab_widgets-3.0.16-py3-none-any.whl", hash = "sha256:45fa36d9c6422cf2559198e4db481aa243c7a32d9926b500781c830c80f7ecf8"}, {file = "jupyterlab_widgets-3.0.16.tar.gz", hash = "sha256:423da05071d55cf27a9e602216d35a3a65a3e41cdf9c5d3b643b814ce38c19e0"}, @@ -2283,7 +2159,6 @@ description = "A fast implementation of the Cassowary constraint solver" optional = false python-versions = ">=3.8" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "kiwisolver-1.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8a9c83f75223d5e48b0bc9cb1bf2776cf01563e00ade8775ffe13b0b6e1af3a6"}, {file = "kiwisolver-1.4.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:58370b1ffbd35407444d57057b57da5d6549d2d854fa30249771775c63b5fe17"}, @@ -2408,7 +2283,6 @@ description = "a modern parsing library" optional = false python-versions = ">=3.8" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "lark-1.3.1-py3-none-any.whl", hash = "sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12"}, {file = "lark-1.3.1.tar.gz", hash = "sha256:b426a7a6d6d53189d318f2b6236ab5d6429eaf09259f1ca33eb716eed10d2905"}, @@ -2427,7 +2301,7 @@ description = "Mypyc runtime library" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "(python_version <= \"3.11\" or python_version >= \"3.12\") and platform_python_implementation != \"PyPy\"" +markers = "platform_python_implementation != \"PyPy\"" files = [ {file = "librt-0.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:34e47058fcc69a313293d6dee94216a4f30c929ae6f2476e58c5ba635aa639d5"}, {file = "librt-0.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dbdd5b6509d0c2a8fe72cf494c299a61dbd58142a90a4190664ae159e4a7b547"}, @@ -2530,7 +2404,6 @@ description = "Convert HTML to markdown." optional = false python-versions = "*" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "markdownify-0.11.6-py3-none-any.whl", hash = "sha256:ba35fe289d5e9073bcd7d2cad629278fe25f1a93741fcdc0bfb4f009076d8324"}, {file = "markdownify-0.11.6.tar.gz", hash = "sha256:009b240e0c9f4c8eaf1d085625dcd4011e12f0f8cec55dedf9ea6f7655e49bfe"}, @@ -2547,7 +2420,6 @@ description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" groups = ["main", "docs", "examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"}, {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"}, @@ -2647,7 +2519,6 @@ description = "Python plotting package" optional = false python-versions = ">=3.9" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "matplotlib-3.9.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c5fdd7abfb706dfa8d307af64a87f1a862879ec3cd8d0ec8637458f0885b9c50"}, {file = "matplotlib-3.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d89bc4e85e40a71d1477780366c27fb7c6494d293e1617788986f74e2a03d7ff"}, @@ -2696,7 +2567,6 @@ files = [ contourpy = ">=1.0.1" cycler = ">=0.10" fonttools = ">=4.22.0" -importlib-resources = {version = ">=3.2.0", markers = "python_version < \"3.10\""} kiwisolver = ">=1.3.1" numpy = ">=1.23" packaging = ">=20.0" @@ -2714,7 +2584,6 @@ description = "Inline Matplotlib backend for Jupyter" optional = false python-versions = ">=3.9" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "matplotlib_inline-0.2.2-py3-none-any.whl", hash = "sha256:3c821cf1c209f59fb2d2d64abbf5b23b67bcb2210d663f9918dd851c6da1fcf6"}, {file = "matplotlib_inline-0.2.2.tar.gz", hash = "sha256:72f3fe8fce36b70d4a5b612f899090cd0401deddc4ea90e1572b9f4bfb058c79"}, @@ -2733,7 +2602,6 @@ description = "McCabe checker, plugin for flake8" optional = false python-versions = ">=3.6" groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, @@ -2746,7 +2614,6 @@ description = "A sane and fast Markdown parser with useful plugins and renderers optional = false python-versions = ">=3.8" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "mistune-3.3.3-py3-none-any.whl", hash = "sha256:99de1585e42dcbd826faa9e11a202727a5e202e4e4722a4c69ac1ff615793dd7"}, {file = "mistune-3.3.3.tar.gz", hash = "sha256:c4c6c0c840b8637a2e9b8b6d607eb7c8f00888bf14c754409bcd339e848c2477"}, @@ -2762,7 +2629,6 @@ description = "Python library for arbitrary-precision floating-point arithmetic" optional = false python-versions = "*" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"}, {file = "mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f"}, @@ -2771,7 +2637,7 @@ files = [ [package.extras] develop = ["codecov", "pycodestyle", "pytest (>=4.6)", "pytest-cov", "wheel"] docs = ["sphinx"] -gmpy = ["gmpy2 (>=2.1.0a4)"] +gmpy = ["gmpy2 (>=2.1.0a4) ; platform_python_implementation != \"PyPy\""] tests = ["pytest (>=4.6)"] [[package]] @@ -2781,7 +2647,6 @@ description = "multidict implementation" optional = false python-versions = ">=3.9" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5"}, {file = "multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8"}, @@ -2941,7 +2806,6 @@ description = "better multiprocessing and multithreading in Python" optional = false python-versions = ">=3.8" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "multiprocess-0.70.16-pp310-pypy310_pp73-macosx_10_13_x86_64.whl", hash = "sha256:476887be10e2f59ff183c006af746cb6f1fd0eadcfd4ef49e605cbe2659920ee"}, {file = "multiprocess-0.70.16-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d951bed82c8f73929ac82c61f01a7b5ce8f3e5ef40f5b52553b4f547ce2b08ec"}, @@ -2967,7 +2831,6 @@ description = "Optional static typing for Python" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec"}, {file = "mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b"}, @@ -3030,7 +2893,6 @@ description = "Type system extensions for programs checked with the mypy type ch optional = false python-versions = ">=3.8" groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, @@ -3043,7 +2905,6 @@ description = "A client library for executing notebooks. Formerly nbconvert's Ex optional = false python-versions = ">=3.9.0" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "nbclient-0.10.2-py3-none-any.whl", hash = "sha256:4ffee11e788b4a27fabeb7955547e4318a5298f34342a4bfd01f2e1faaeadc3d"}, {file = "nbclient-0.10.2.tar.gz", hash = "sha256:90b7fc6b810630db87a6d0c2250b1f0ab4cf4d3c27a299b0cde78a4ed3fd9193"}, @@ -3067,7 +2928,6 @@ description = "Convert Jupyter Notebooks (.ipynb files) to other formats." optional = false python-versions = ">=3.9" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "nbconvert-7.17.1-py3-none-any.whl", hash = "sha256:aa85c087b435e7bf1ffd03319f658e285f2b89eccab33bc1ba7025495ab3e7c8"}, {file = "nbconvert-7.17.1.tar.gz", hash = "sha256:34d0d0a7e73ce3cbab6c5aae8f4f468797280b01fd8bd2ca746da8569eddd7d2"}, @@ -3077,7 +2937,6 @@ files = [ beautifulsoup4 = "*" bleach = {version = "!=5.0.0", extras = ["css"]} defusedxml = "*" -importlib-metadata = {version = ">=3.6", markers = "python_version < \"3.10\""} jinja2 = ">=3.0" jupyter-core = ">=4.7" jupyterlab-pygments = "*" @@ -3106,7 +2965,6 @@ description = "The Jupyter Notebook format" optional = false python-versions = ">=3.8" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b"}, {file = "nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a"}, @@ -3129,7 +2987,6 @@ description = "Patch asyncio to allow nested event loops" optional = false python-versions = ">=3.5" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c"}, {file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"}, @@ -3142,7 +2999,6 @@ description = "Python package for creating and manipulating graphs and networks" optional = false python-versions = ">=3.9" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "networkx-3.2.1-py3-none-any.whl", hash = "sha256:f18c69adc97877c42332c170849c96cefa91881c99a7cb3e95b7c659ebdc1ec2"}, {file = "networkx-3.2.1.tar.gz", hash = "sha256:9f1bb5cf3409bf324e0a722c20bdb4c20ee39bf1c30ce8ae499c8502b0b5e0c6"}, @@ -3162,7 +3018,6 @@ description = "Node.js virtual environment builder" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827"}, {file = "nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb"}, @@ -3175,7 +3030,6 @@ description = "Jupyter Notebook - A web-based notebook environment for interacti optional = false python-versions = ">=3.9" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "notebook-7.5.7-py3-none-any.whl", hash = "sha256:1f95f79d117e47d20b5555b5c85a397d2cfecf136978aaab767cf0314b09165b"}, {file = "notebook-7.5.7.tar.gz", hash = "sha256:d6d59288a25303b25e1dcb71e9b017ec3a785f7d92f38b9bc288ca1970d5b0a8"}, @@ -3191,7 +3045,7 @@ tornado = ">=6.2.0" [package.extras] dev = ["hatch", "pre-commit"] docs = ["myst-parser", "nbsphinx", "pydata-sphinx-theme", "sphinx (>=1.3.6)", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] -test = ["importlib-resources (>=5.0)", "ipykernel", "jupyter-server[test] (>=2.4.0,<3)", "jupyterlab-server[test] (>=2.28.0,<3)", "nbval", "pytest (>=7.0)", "pytest-console-scripts", "pytest-timeout", "pytest-tornasync", "requests"] +test = ["importlib-resources (>=5.0) ; python_version < \"3.10\"", "ipykernel", "jupyter-server[test] (>=2.4.0,<3)", "jupyterlab-server[test] (>=2.28.0,<3)", "nbval", "pytest (>=7.0)", "pytest-console-scripts", "pytest-timeout", "pytest-tornasync", "requests"] [[package]] name = "notebook-shim" @@ -3200,7 +3054,6 @@ description = "A shim layer for notebook traits and config" optional = false python-versions = ">=3.7" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "notebook_shim-0.2.4-py3-none-any.whl", hash = "sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef"}, {file = "notebook_shim-0.2.4.tar.gz", hash = "sha256:b4b2cfa1b65d98307ca24361f5b30fe785b53c3fd07b7a47e89acb5e6ac638cb"}, @@ -3219,7 +3072,6 @@ description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.9" groups = ["main", "examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, @@ -3266,7 +3118,7 @@ description = "CUBLAS native runtime libraries" optional = false python-versions = ">=3" groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and (python_version <= \"3.11\" or python_version >= \"3.12\")" +markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" files = [ {file = "nvidia_cublas_cu12-12.6.4.1-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08ed2686e9875d01b58e3cb379c6896df8e76c75e0d4a7f7dace3d7b6d9ef8eb"}, {file = "nvidia_cublas_cu12-12.6.4.1-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:235f728d6e2a409eddf1df58d5b0921cf80cfa9e72b9f2775ccb7b4a87984668"}, @@ -3280,7 +3132,7 @@ description = "CUDA profiling tools runtime libs." optional = false python-versions = ">=3" groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and (python_version <= \"3.11\" or python_version >= \"3.12\")" +markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" files = [ {file = "nvidia_cuda_cupti_cu12-12.6.80-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:166ee35a3ff1587f2490364f90eeeb8da06cd867bd5b701bf7f9a02b78bc63fc"}, {file = "nvidia_cuda_cupti_cu12-12.6.80-py3-none-manylinux2014_aarch64.whl", hash = "sha256:358b4a1d35370353d52e12f0a7d1769fc01ff74a191689d3870b2123156184c4"}, @@ -3296,7 +3148,7 @@ description = "NVRTC native runtime libraries" optional = false python-versions = ">=3" groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and (python_version <= \"3.11\" or python_version >= \"3.12\")" +markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" files = [ {file = "nvidia_cuda_nvrtc_cu12-12.6.77-py3-none-manylinux2014_aarch64.whl", hash = "sha256:5847f1d6e5b757f1d2b3991a01082a44aad6f10ab3c5c0213fa3e25bddc25a13"}, {file = "nvidia_cuda_nvrtc_cu12-12.6.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:35b0cc6ee3a9636d5409133e79273ce1f3fd087abb0532d2d2e8fff1fe9efc53"}, @@ -3310,7 +3162,7 @@ description = "CUDA Runtime native Libraries" optional = false python-versions = ">=3" groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and (python_version <= \"3.11\" or python_version >= \"3.12\")" +markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" files = [ {file = "nvidia_cuda_runtime_cu12-12.6.77-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6116fad3e049e04791c0256a9778c16237837c08b27ed8c8401e2e45de8d60cd"}, {file = "nvidia_cuda_runtime_cu12-12.6.77-py3-none-manylinux2014_aarch64.whl", hash = "sha256:d461264ecb429c84c8879a7153499ddc7b19b5f8d84c204307491989a365588e"}, @@ -3326,7 +3178,7 @@ description = "cuDNN runtime libraries" optional = false python-versions = ">=3" groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and (python_version <= \"3.11\" or python_version >= \"3.12\")" +markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" files = [ {file = "nvidia_cudnn_cu12-9.5.1.17-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:9fd4584468533c61873e5fda8ca41bac3a38bcb2d12350830c69b0a96a7e4def"}, {file = "nvidia_cudnn_cu12-9.5.1.17-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:30ac3869f6db17d170e0e556dd6cc5eee02647abc31ca856634d5a40f82c15b2"}, @@ -3343,7 +3195,7 @@ description = "CUFFT native runtime libraries" optional = false python-versions = ">=3" groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and (python_version <= \"3.11\" or python_version >= \"3.12\")" +markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" files = [ {file = "nvidia_cufft_cu12-11.3.0.4-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d16079550df460376455cba121db6564089176d9bac9e4f360493ca4741b22a6"}, {file = "nvidia_cufft_cu12-11.3.0.4-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8510990de9f96c803a051822618d42bf6cb8f069ff3f48d93a8486efdacb48fb"}, @@ -3362,7 +3214,7 @@ description = "cuFile GPUDirect libraries" optional = false python-versions = ">=3" groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and (python_version <= \"3.11\" or python_version >= \"3.12\")" +markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" files = [ {file = "nvidia_cufile_cu12-1.11.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc23469d1c7e52ce6c1d55253273d32c565dd22068647f3aa59b3c6b005bf159"}, {file = "nvidia_cufile_cu12-1.11.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:8f57a0051dcf2543f6dc2b98a98cb2719c37d3cee1baba8965d57f3bbc90d4db"}, @@ -3375,7 +3227,7 @@ description = "CURAND native runtime libraries" optional = false python-versions = ">=3" groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and (python_version <= \"3.11\" or python_version >= \"3.12\")" +markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" files = [ {file = "nvidia_curand_cu12-10.3.7.77-py3-none-manylinux2014_aarch64.whl", hash = "sha256:6e82df077060ea28e37f48a3ec442a8f47690c7499bff392a5938614b56c98d8"}, {file = "nvidia_curand_cu12-10.3.7.77-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a42cd1344297f70b9e39a1e4f467a4e1c10f1da54ff7a85c12197f6c652c8bdf"}, @@ -3391,7 +3243,7 @@ description = "CUDA solver native runtime libraries" optional = false python-versions = ">=3" groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and (python_version <= \"3.11\" or python_version >= \"3.12\")" +markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" files = [ {file = "nvidia_cusolver_cu12-11.7.1.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0ce237ef60acde1efc457335a2ddadfd7610b892d94efee7b776c64bb1cac9e0"}, {file = "nvidia_cusolver_cu12-11.7.1.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e9e49843a7707e42022babb9bcfa33c29857a93b88020c4e4434656a655b698c"}, @@ -3412,7 +3264,7 @@ description = "CUSPARSE native runtime libraries" optional = false python-versions = ">=3" groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and (python_version <= \"3.11\" or python_version >= \"3.12\")" +markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" files = [ {file = "nvidia_cusparse_cu12-12.5.4.2-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d25b62fb18751758fe3c93a4a08eff08effedfe4edf1c6bb5afd0890fe88f887"}, {file = "nvidia_cusparse_cu12-12.5.4.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7aa32fa5470cf754f72d1116c7cbc300b4e638d3ae5304cfa4a638a5b87161b1"}, @@ -3431,7 +3283,7 @@ description = "NVIDIA cuSPARSELt" optional = false python-versions = "*" groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and (python_version <= \"3.11\" or python_version >= \"3.12\")" +markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" files = [ {file = "nvidia_cusparselt_cu12-0.6.3-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8371549623ba601a06322af2133c4a44350575f5a3108fb75f3ef20b822ad5f1"}, {file = "nvidia_cusparselt_cu12-0.6.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:e5c8a26c36445dd2e6812f1177978a24e2d37cacce7e090f297a688d1ec44f46"}, @@ -3445,7 +3297,7 @@ description = "NVIDIA Collective Communication Library (NCCL) Runtime" optional = false python-versions = ">=3" groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and (python_version <= \"3.11\" or python_version >= \"3.12\")" +markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" files = [ {file = "nvidia_nccl_cu12-2.26.2-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c196e95e832ad30fbbb50381eb3cbd1fadd5675e587a548563993609af19522"}, {file = "nvidia_nccl_cu12-2.26.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:694cf3879a206553cc9d7dbda76b13efaf610fdb70a50cba303de1b0d1530ac6"}, @@ -3458,7 +3310,7 @@ description = "Nvidia JIT LTO Library" optional = false python-versions = ">=3" groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and (python_version <= \"3.11\" or python_version >= \"3.12\")" +markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" files = [ {file = "nvidia_nvjitlink_cu12-12.6.85-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:eedc36df9e88b682efe4309aa16b5b4e78c2407eac59e8c10a6a47535164369a"}, {file = "nvidia_nvjitlink_cu12-12.6.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cf4eaa7d4b6b543ffd69d6abfb11efdeb2db48270d94dfd3a452c24150829e41"}, @@ -3472,7 +3324,7 @@ description = "NVIDIA Tools Extension" optional = false python-versions = ">=3" groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and (python_version <= \"3.11\" or python_version >= \"3.12\")" +markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" files = [ {file = "nvidia_nvtx_cu12-12.6.77-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f44f8d86bb7d5629988d61c8d3ae61dddb2015dee142740536bc7481b022fe4b"}, {file = "nvidia_nvtx_cu12-12.6.77-py3-none-manylinux2014_aarch64.whl", hash = "sha256:adcaabb9d436c9761fca2b13959a2d237c5f9fd406c8e4b723c695409ff88059"}, @@ -3488,7 +3340,7 @@ description = "A decorator to automatically detect mismatch when overriding a me optional = false python-versions = ">=3.6" groups = ["examples"] -markers = "python_version <= \"3.11\"" +markers = "python_version < \"3.12\"" files = [ {file = "overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49"}, {file = "overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a"}, @@ -3501,7 +3353,6 @@ description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" groups = ["main", "dev", "docs", "examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e"}, {file = "packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661"}, @@ -3514,7 +3365,6 @@ description = "Powerful data structures for data analysis, time series, and stat optional = false python-versions = ">=3.9" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c"}, {file = "pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a"}, @@ -3615,7 +3465,6 @@ description = "Utilities for writing pandoc filters in python" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc"}, {file = "pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e"}, @@ -3628,7 +3477,6 @@ description = "A Python Parser" optional = false python-versions = ">=3.6" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "parso-0.8.7-py2.py3-none-any.whl", hash = "sha256:a8926eb2a1b915486941fdbd31e86a4baf88fe8c210f25f2f35ecec5b574ca1c"}, {file = "parso-0.8.7.tar.gz", hash = "sha256:eaaac4c9fdd5e9e8852dc778d2d7405897ec510f2a298071453e5e3a07914bb1"}, @@ -3645,7 +3493,6 @@ description = "Utility library for gitignore style pattern matching of file path optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189"}, {file = "pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a"}, @@ -3663,7 +3510,7 @@ description = "Pexpect allows easy control of interactive console applications." optional = false python-versions = "*" groups = ["examples"] -markers = "(python_version <= \"3.11\" or python_version >= \"3.12\") and sys_platform != \"win32\"" +markers = "sys_platform != \"win32\"" files = [ {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, @@ -3679,7 +3526,6 @@ description = "Python Imaging Library (Fork)" optional = false python-versions = ">=3.9" groups = ["main", "examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860"}, {file = "pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad"}, @@ -3795,7 +3641,7 @@ fpx = ["olefile"] mic = ["olefile"] test-arrow = ["pyarrow"] tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "trove-classifiers (>=2024.10.12)"] -typing = ["typing-extensions"] +typing = ["typing-extensions ; python_version < \"3.10\""] xmp = ["defusedxml"] [[package]] @@ -3805,7 +3651,6 @@ description = "A small Python package for determining appropriate platform-speci optional = false python-versions = ">=3.9" groups = ["dev", "examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85"}, {file = "platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf"}, @@ -3823,7 +3668,6 @@ description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, @@ -3840,7 +3684,6 @@ description = "A framework for managing and maintaining multi-language pre-commi optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8"}, {file = "pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16"}, @@ -3860,7 +3703,6 @@ description = "Python client for the Prometheus monitoring system." optional = false python-versions = ">=3.9" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "prometheus_client-0.25.0-py3-none-any.whl", hash = "sha256:d5aec89e349a6ec230805d0df882f3807f74fd6c1a2fa86864e3c2279059fed1"}, {file = "prometheus_client-0.25.0.tar.gz", hash = "sha256:5e373b75c31afb3c86f1a52fa1ad470c9aace18082d39ec0d2f918d11cc9ba28"}, @@ -3878,7 +3720,6 @@ description = "Library for building powerful interactive command lines in Python optional = false python-versions = ">=3.8" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955"}, {file = "prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855"}, @@ -3894,7 +3735,6 @@ description = "Accelerated property cache" optional = false python-versions = ">=3.9" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db"}, {file = "propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8"}, @@ -4027,7 +3867,6 @@ description = "Cross-platform lib for process and system monitoring." optional = false python-versions = ">=3.6" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b"}, {file = "psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea"}, @@ -4053,8 +3892,8 @@ files = [ ] [package.extras] -dev = ["abi3audit", "black", "check-manifest", "colorama", "coverage", "packaging", "psleak", "pylint", "pyperf", "pypinfo", "pyreadline3", "pytest", "pytest-cov", "pytest-instafail", "pytest-xdist", "pywin32", "requests", "rstcheck", "ruff", "setuptools", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "validate-pyproject[all]", "virtualenv", "vulture", "wheel", "wheel", "wmi"] -test = ["psleak", "pytest", "pytest-instafail", "pytest-xdist", "pywin32", "setuptools", "wheel", "wmi"] +dev = ["abi3audit", "black", "check-manifest", "colorama ; os_name == \"nt\"", "coverage", "packaging", "psleak", "pylint", "pyperf", "pypinfo", "pyreadline3 ; os_name == \"nt\"", "pytest", "pytest-cov", "pytest-instafail", "pytest-xdist", "pywin32 ; os_name == \"nt\" and implementation_name != \"pypy\"", "requests", "rstcheck", "ruff", "setuptools", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "validate-pyproject[all]", "virtualenv", "vulture", "wheel", "wheel ; os_name == \"nt\" and implementation_name != \"pypy\"", "wmi ; os_name == \"nt\" and implementation_name != \"pypy\""] +test = ["psleak", "pytest", "pytest-instafail", "pytest-xdist", "pywin32 ; os_name == \"nt\" and implementation_name != \"pypy\"", "setuptools", "wheel ; os_name == \"nt\" and implementation_name != \"pypy\"", "wmi ; os_name == \"nt\" and implementation_name != \"pypy\""] [[package]] name = "ptyprocess" @@ -4063,7 +3902,7 @@ description = "Run a subprocess in a pseudo terminal" optional = false python-versions = "*" groups = ["examples"] -markers = "(os_name != \"nt\" or sys_platform != \"win32\") and (python_version <= \"3.11\" or python_version >= \"3.12\")" +markers = "os_name != \"nt\" or sys_platform != \"win32\"" files = [ {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, @@ -4076,7 +3915,6 @@ description = "Safely evaluate AST nodes without side effects" optional = false python-versions = "*" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0"}, {file = "pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42"}, @@ -4092,7 +3930,6 @@ description = "Python library for Apache Arrow" optional = false python-versions = ">=3.8" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pyarrow-15.0.2-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:88b340f0a1d05b5ccc3d2d986279045655b1fe8e41aba6ca44ea28da0d1455d8"}, {file = "pyarrow-15.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eaa8f96cecf32da508e6c7f69bb8401f03745c050c1dd42ec2596f2e98deecac"}, @@ -4142,7 +3979,6 @@ description = "Python style guide checker" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d"}, {file = "pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783"}, @@ -4155,7 +3991,7 @@ description = "C parser in Python" optional = false python-versions = ">=3.8" groups = ["examples"] -markers = "(python_version <= \"3.11\" or python_version >= \"3.12\") and implementation_name != \"PyPy\"" +markers = "implementation_name != \"PyPy\"" files = [ {file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"}, {file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"}, @@ -4168,7 +4004,6 @@ description = "passive checker of Python programs" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f"}, {file = "pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58"}, @@ -4181,7 +4016,6 @@ description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.9" groups = ["dev", "docs", "examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, @@ -4197,7 +4031,6 @@ description = "pyparsing - Classes and methods to define and execute parsing gra optional = false python-versions = ">=3.9" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d"}, {file = "pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc"}, @@ -4213,7 +4046,6 @@ description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, @@ -4238,7 +4070,6 @@ description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pytest_cov-6.3.0-py3-none-any.whl", hash = "sha256:440db28156d2468cafc0415b4f8e50856a0d11faefa38f30906048fe490f1749"}, {file = "pytest_cov-6.3.0.tar.gz", hash = "sha256:35c580e7800f87ce892e687461166e1ac2bcb8fb9e13aea79032518d6e503ff2"}, @@ -4259,7 +4090,6 @@ description = "pytest plugin to abort hanging tests" optional = false python-versions = ">=3.7" groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2"}, {file = "pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a"}, @@ -4275,7 +4105,6 @@ description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -4291,7 +4120,6 @@ description = "Python interpreter discovery" optional = false python-versions = ">=3.8" groups = ["dev"] -markers = "python_version < \"3.10\"" files = [ {file = "python_discovery-1.4.4-py3-none-any.whl", hash = "sha256:abebe9120b43453b68c908acfb1e72a19d1a959ed2cb620ad38fc57d08056dbe"}, {file = "python_discovery-1.4.4.tar.gz", hash = "sha256:5cad33982d412c1f3ffb8f9ca4ea292c9680bca3942451d30b69c37fce53a4a3"}, @@ -4312,17 +4140,13 @@ description = "JSON Log Formatter for the Python Logging Package" optional = false python-versions = ">=3.8" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "python_json_logger-4.0.0-py3-none-any.whl", hash = "sha256:af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2"}, {file = "python_json_logger-4.0.0.tar.gz", hash = "sha256:f58e68eb46e1faed27e0f574a55a0455eecd7b8a5b88b85a784519ba3cff047f"}, ] -[package.dependencies] -typing_extensions = {version = "*", markers = "python_version < \"3.10\""} - [package.extras] -dev = ["backports.zoneinfo", "black", "build", "freezegun", "mdx_truly_sane_lists", "mike", "mkdocs", "mkdocs-awesome-pages-plugin", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-material (>=8.5)", "mkdocstrings[python]", "msgspec", "mypy", "orjson", "pylint", "pytest", "tzdata", "validate-pyproject[all]"] +dev = ["backports.zoneinfo ; python_version < \"3.9\"", "black", "build", "freezegun", "mdx_truly_sane_lists", "mike", "mkdocs", "mkdocs-awesome-pages-plugin", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-material (>=8.5)", "mkdocstrings[python]", "msgspec ; implementation_name != \"pypy\"", "mypy", "orjson ; implementation_name != \"pypy\"", "pylint", "pytest", "tzdata", "validate-pyproject[all]"] [[package]] name = "python-slugify" @@ -4331,7 +4155,6 @@ description = "A Python slugify application that also handles Unicode" optional = false python-versions = ">=3.7" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856"}, {file = "python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8"}, @@ -4350,7 +4173,6 @@ description = "A Fast, spec compliant Python 3.14+ tokenizer that runs on older optional = false python-versions = ">=3.8" groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pytokens-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a44ed93ea23415c54f3face3b65ef2b844d96aeb3455b8a69b3df6beab6acc5"}, {file = "pytokens-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:add8bf86b71a5d9fb5b89f023a80b791e04fba57960aa790cc6125f7f1d39dfe"}, @@ -4406,7 +4228,6 @@ description = "World timezone definitions, modern and historical" optional = false python-versions = "*" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pytz-2026.2-py2.py3-none-any.whl", hash = "sha256:04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126"}, {file = "pytz-2026.2.tar.gz", hash = "sha256:0e60b47b29f21574376f218fe21abc009894a2321ea16c6754f3cad6eb7cdd6a"}, @@ -4419,7 +4240,7 @@ description = "Python for Windows Extensions" optional = false python-versions = ">=3.9" groups = ["examples"] -markers = "sys_platform == \"win32\" and platform_python_implementation != \"PyPy\" and (python_version <= \"3.11\" or python_version >= \"3.12\")" +markers = "sys_platform == \"win32\" and platform_python_implementation != \"PyPy\"" files = [ {file = "pywin32-312-cp310-cp310-win32.whl", hash = "sha256:772235332b5d1024c696f11cea1ae4be7930f0a8b894bb43db14e3f435f1ff7e"}, {file = "pywin32-312-cp310-cp310-win_amd64.whl", hash = "sha256:5dbc35d2b5320dc07f25fa31269cfb767471002b17de5eb067d03da68c7cb2db"}, @@ -4451,7 +4272,7 @@ description = "Pseudo terminal support for Windows from Python." optional = false python-versions = ">=3.9" groups = ["examples"] -markers = "(python_version <= \"3.11\" or python_version >= \"3.12\") and os_name == \"nt\"" +markers = "os_name == \"nt\"" files = [ {file = "pywinpty-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:ff05f12d775b142b11c6fe085129bdd759b61cf7d41da6c745e78e3a1ef5bf40"}, {file = "pywinpty-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:340ccacb4d74278a631923794ccd758471cfc8eeeeee4610b280420a17ad1e82"}, @@ -4478,7 +4299,6 @@ description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" groups = ["main", "dev", "examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, @@ -4562,7 +4382,6 @@ description = "Python bindings for 0MQ" optional = false python-versions = ">=3.8" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pyzmq-27.1.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:508e23ec9bc44c0005c4946ea013d9317ae00ac67778bd47519fdf5a0e930ff4"}, {file = "pyzmq-27.1.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:507b6f430bdcf0ee48c0d30e734ea89ce5567fd7b8a0f0044a369c176aa44556"}, @@ -4668,7 +4487,6 @@ description = "JSON Referencing + Python" optional = false python-versions = ">=3.9" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0"}, {file = "referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa"}, @@ -4686,7 +4504,6 @@ description = "Alternative regular expression module, to replace re." optional = false python-versions = ">=3.9" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "regex-2026.1.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e3dd93c8f9abe8aa4b6c652016da9a3afa190df5ad822907efe6b206c09896e"}, {file = "regex-2026.1.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:97499ff7862e868b1977107873dd1a06e151467129159a6ffd07b66706ba3a9f"}, @@ -4828,7 +4645,6 @@ description = "Python HTTP for Humans." optional = false python-versions = ">=3.9" groups = ["main", "docs", "examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, @@ -4851,7 +4667,6 @@ description = "A pure python RFC3339 validator" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa"}, {file = "rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b"}, @@ -4867,7 +4682,6 @@ description = "Pure python rfc3986 validator" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9"}, {file = "rfc3986_validator-0.1.1.tar.gz", hash = "sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055"}, @@ -4880,7 +4694,6 @@ description = "Helper functions to syntactically validate strings according to R optional = false python-versions = ">=3.9" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "rfc3987_syntax-1.1.0-py3-none-any.whl", hash = "sha256:6c3d97604e4c5ce9f714898e05401a0445a641cfa276432b0a648c80856f6a3f"}, {file = "rfc3987_syntax-1.1.0.tar.gz", hash = "sha256:717a62cbf33cffdd16dfa3a497d81ce48a660ea691b1ddd7be710c22f00b4a0d"}, @@ -4899,7 +4712,6 @@ description = "Python bindings to Rust's persistent data structures (rpds)" optional = false python-versions = ">=3.9" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "rpds_py-0.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:68afeec26d42ab3b47e541b272166a0b4400313946871cba3ed3a4fc0cab1cef"}, {file = "rpds_py-0.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74e5b2f7bb6fa38b1b10546d27acbacf2a022a8b5543efb06cfebc72a59c85be"}, @@ -5065,7 +4877,6 @@ description = "" optional = false python-versions = ">=3.9" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517"}, {file = "safetensors-0.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57"}, @@ -5113,7 +4924,7 @@ description = "A set of python modules for machine learning and data mining" optional = true python-versions = ">=3.9" groups = ["main"] -markers = "(python_version <= \"3.11\" or python_version >= \"3.12\") and extra == \"sbert\"" +markers = "extra == \"sbert\"" files = [ {file = "scikit_learn-1.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d056391530ccd1e501056160e3c9673b4da4805eb67eb2bdf4e983e1f9c9204e"}, {file = "scikit_learn-1.6.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:0c8d036eb937dbb568c6242fa598d551d88fb4399c0344d95c001980ec1c7d36"}, @@ -5169,7 +4980,7 @@ description = "Fundamental algorithms for scientific computing in Python" optional = true python-versions = ">=3.9" groups = ["main"] -markers = "(python_version <= \"3.11\" or python_version >= \"3.12\") and extra == \"sbert\"" +markers = "extra == \"sbert\"" files = [ {file = "scipy-1.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:20335853b85e9a49ff7572ab453794298bcf0354d8068c5f6775a0eabf350aca"}, {file = "scipy-1.13.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d605e9c23906d1994f55ace80e0125c587f96c020037ea6aa98d01b4bd2e222f"}, @@ -5213,14 +5024,13 @@ description = "Send file to trash natively under Mac OS X, Windows and Linux" optional = false python-versions = ">=3.8" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "send2trash-2.1.0-py3-none-any.whl", hash = "sha256:0da2f112e6d6bb22de6aa6daa7e144831a4febf2a87261451c4ad849fe9a873c"}, {file = "send2trash-2.1.0.tar.gz", hash = "sha256:1c72b39f09457db3c05ce1d19158c2cbef4c32b8bedd02c155e49282b7ea7459"}, ] [package.extras] -nativelib = ["pyobjc (>=9.0)", "pywin32 (>=305)"] +nativelib = ["pyobjc (>=9.0) ; sys_platform == \"darwin\"", "pywin32 (>=305) ; sys_platform == \"win32\""] test = ["pytest (>=8)"] [[package]] @@ -5230,7 +5040,7 @@ description = "State-of-the-Art Text Embeddings" optional = true python-versions = ">=3.9" groups = ["main"] -markers = "(python_version <= \"3.11\" or python_version >= \"3.12\") and extra == \"sbert\"" +markers = "extra == \"sbert\"" files = [ {file = "sentence_transformers-3.4.1-py3-none-any.whl", hash = "sha256:e026dc6d56801fd83f74ad29a30263f401b4b522165c19386d8bc10dcca805da"}, {file = "sentence_transformers-3.4.1.tar.gz", hash = "sha256:68daa57504ff548340e54ff117bd86c1d2f784b21e0fb2689cf3272b8937b24b"}, @@ -5263,16 +5073,16 @@ files = [ {file = "setuptools-83.0.0-py3-none-any.whl", hash = "sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3"}, {file = "setuptools-83.0.0.tar.gz", hash = "sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef"}, ] -markers = {main = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version <= \"3.11\" or python_version >= \"3.12\"", examples = "python_version <= \"3.11\" or python_version >= \"3.12\""} +markers = {main = "platform_system == \"Linux\" and platform_machine == \"x86_64\" or python_version >= \"3.12\""} [package.extras] -check = ["pytest-checkdocs (>=2.14)", "pytest-ruff (>=0.2.1)", "ruff (>=0.13.0)"] -core = ["importlib_metadata (>=6)", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +check = ["pytest-checkdocs (>=2.14)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.13.0) ; sys_platform != \"cygwin\""] +core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=3.4)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.18.*)", "pytest-mypy (>=1.0.1)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.18.*)", "pytest-mypy (>=1.0.1) ; platform_python_implementation != \"PyPy\""] [[package]] name = "six" @@ -5281,7 +5091,6 @@ description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, @@ -5294,7 +5103,6 @@ description = "Utils for streaming large files (S3, HDFS, GCS, SFTP, Azure Blob optional = false python-versions = "<4.0,>=3.9" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "smart_open-7.5.0-py3-none-any.whl", hash = "sha256:87e695c5148bbb988f15cec00971602765874163be85acb1c9fb8abc012e6599"}, {file = "smart_open-7.5.0.tar.gz", hash = "sha256:f394b143851d8091011832ac8113ea4aba6b92e6c35f6e677ddaaccb169d7cb9"}, @@ -5306,13 +5114,13 @@ wrapt = "*" [package.extras] all = ["smart_open[azure,gcs,http,s3,ssh,webhdfs,zst]"] azure = ["azure-common", "azure-core", "azure-storage-blob"] -gcs = ["google-api-core (<2.28)", "google-cloud-storage (>=2.6.0)"] +gcs = ["google-api-core (<2.28) ; python_version < \"3.10\"", "google-cloud-storage (>=2.6.0)"] http = ["requests"] s3 = ["boto3 (>=1.9.17)"] ssh = ["paramiko"] test = ["awscli", "flake8", "moto[server]", "numpy", "pyopenssl", "pytest", "pytest-rerunfailures", "pytest-timeout", "pytest-xdist[psutil]", "pytest_benchmark", "responses", "smart_open[all]"] webhdfs = ["requests"] -zst = ["backports.zstd (>=1.0.0)"] +zst = ["backports.zstd (>=1.0.0) ; python_version < \"3.14\""] [[package]] name = "snowballstemmer" @@ -5321,7 +5129,6 @@ description = "This package provides 36 stemmers for 34 languages generated from optional = false python-versions = ">=3.3" groups = ["docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "snowballstemmer-3.1.1-py3-none-any.whl", hash = "sha256:7e207fa178741da09cdee59d3ecec3827ad5f92b1fc5c9ff3755b639f71f5752"}, {file = "snowballstemmer-3.1.1.tar.gz", hash = "sha256:e07bbc54a0d798fe6010a12398422e62a8bfbba95c394fd0956ef58cb4d3e260"}, @@ -5334,7 +5141,6 @@ description = "A modern CSS selector implementation for Beautiful Soup." optional = false python-versions = ">=3.9" groups = ["docs", "examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "soupsieve-2.8.4-py3-none-any.whl", hash = "sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65"}, {file = "soupsieve-2.8.4.tar.gz", hash = "sha256:e121fd02e975c695e4e9e8774a5ee35d74714b59307868dcc5319ad2d9e3328e"}, @@ -5347,7 +5153,6 @@ description = "Python documentation generator" optional = false python-versions = ">=3.9" groups = ["docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239"}, {file = "sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe"}, @@ -5359,7 +5164,6 @@ babel = ">=2.13" colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} docutils = ">=0.20,<0.22" imagesize = ">=1.3" -importlib-metadata = {version = ">=6.0", markers = "python_version < \"3.10\""} Jinja2 = ">=3.1" packaging = ">=23.0" Pygments = ">=2.17" @@ -5385,7 +5189,6 @@ description = "A modern skeleton for Sphinx themes." optional = false python-versions = ">=3.7" groups = ["docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "sphinx_basic_ng-1.0.0b2-py3-none-any.whl", hash = "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b"}, {file = "sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9"}, @@ -5404,7 +5207,6 @@ description = "Add a copy button to each of your code cells." optional = false python-versions = ">=3.7" groups = ["docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "sphinx-copybutton-0.5.2.tar.gz", hash = "sha256:4cf17c82fb9646d1bc9ca92ac280813a3b605d8c421225fd9913154103ee1fbd"}, {file = "sphinx_copybutton-0.5.2-py3-none-any.whl", hash = "sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e"}, @@ -5424,7 +5226,6 @@ description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple optional = false python-versions = ">=3.9" groups = ["docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}, {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}, @@ -5442,7 +5243,6 @@ description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp optional = false python-versions = ">=3.9" groups = ["docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}, {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}, @@ -5460,7 +5260,6 @@ description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML h optional = false python-versions = ">=3.9" groups = ["docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, @@ -5478,7 +5277,6 @@ description = "A sphinx extension which renders display math in HTML via JavaScr optional = false python-versions = ">=3.5" groups = ["docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, @@ -5494,7 +5292,6 @@ description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp d optional = false python-versions = ">=3.9" groups = ["docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}, {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}, @@ -5512,7 +5309,6 @@ description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs optional = false python-versions = ">=3.9" groups = ["docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, @@ -5530,7 +5326,6 @@ description = "Extract data from python stack frames and tracebacks for informat optional = false python-versions = "*" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"}, {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"}, @@ -5551,7 +5346,6 @@ description = "Computer algebra system (CAS) in Python" optional = false python-versions = ">=3.9" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5"}, {file = "sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517"}, @@ -5570,7 +5364,6 @@ description = "Tornado websocket backend for the Xterm.js Javascript terminal em optional = false python-versions = ">=3.8" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "terminado-0.18.1-py3-none-any.whl", hash = "sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0"}, {file = "terminado-0.18.1.tar.gz", hash = "sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e"}, @@ -5593,7 +5386,6 @@ description = "The most basic Text::Unidecode port" optional = false python-versions = "*" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, @@ -5606,7 +5398,7 @@ description = "threadpoolctl" optional = true python-versions = ">=3.9" groups = ["main"] -markers = "(python_version <= \"3.11\" or python_version >= \"3.12\") and extra == \"sbert\"" +markers = "extra == \"sbert\"" files = [ {file = "threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb"}, {file = "threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e"}, @@ -5619,7 +5411,6 @@ description = "A tiny CSS parser" optional = false python-versions = ">=3.8" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289"}, {file = "tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7"}, @@ -5639,7 +5430,6 @@ description = "" optional = false python-versions = ">=3.9" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c"}, {file = "tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001"}, @@ -5682,7 +5472,7 @@ description = "A lil' TOML parser" optional = false python-versions = ">=3.8" groups = ["dev", "docs", "examples"] -markers = "python_version < \"3.11\"" +markers = "python_version == \"3.10\"" files = [ {file = "tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30"}, {file = "tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a"}, @@ -5740,7 +5530,6 @@ description = "Tensors and Dynamic neural networks in Python with strong GPU acc optional = false python-versions = ">=3.9.0" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "torch-2.7.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:a103b5d782af5bd119b81dbcc7ffc6fa09904c423ff8db397a1e6ea8fd71508f"}, {file = "torch-2.7.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:fe955951bdf32d182ee8ead6c3186ad54781492bf03d547d31771a01b3d6fb7d"}, @@ -5803,7 +5592,6 @@ description = "Tornado is a Python web framework and asynchronous networking lib optional = false python-versions = ">=3.9" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "tornado-6.5.7-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:148b2eb15c2c765a50796172c1e499649b35f30d2e3c3d3e15913cfa56bfb163"}, {file = "tornado-6.5.7-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9da38de27f1da3b78a966f0dae12b5a1ea9afe72ca805d84ff06508272ddf100"}, @@ -5824,7 +5612,6 @@ description = "Fast, Extensible Progress Meter" optional = false python-versions = ">=3.8" groups = ["main", "examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "tqdm-4.68.4-py3-none-any.whl", hash = "sha256:5168118b2368f48c561afda8020fd79195b1bdb0bdf8086b88442c267a315dc2"}, {file = "tqdm-4.68.4.tar.gz", hash = "sha256:19829c9673638f2a0b8617da4cdcb927e831cd88bcfcb6e78d42a4d1af131520"}, @@ -5846,7 +5633,6 @@ description = "Traitlets Python configuration system" optional = false python-versions = ">=3.9" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "traitlets-5.15.1-py3-none-any.whl", hash = "sha256:770a53705f84b81ac107e83a1b3328ff2dae16094d8fc3cfc004e4b22dfd8e92"}, {file = "traitlets-5.15.1.tar.gz", hash = "sha256:7b1c07854fe25acb39e009bae49f11b79ff6cbb2f27999104e9110e7a6b53722"}, @@ -5863,7 +5649,6 @@ description = "State-of-the-art Machine Learning for JAX, PyTorch and TensorFlow optional = false python-versions = ">=3.9.0" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "transformers-4.57.6-py3-none-any.whl", hash = "sha256:4c9e9de11333ddfe5114bc872c9f370509198acf0b87a832a0ab9458e2bd0550"}, {file = "transformers-4.57.6.tar.gz", hash = "sha256:55e44126ece9dc0a291521b7e5492b572e6ef2766338a610b9ab5afbb70689d3"}, @@ -5939,7 +5724,7 @@ description = "A language and compiler for custom Deep Learning operations" optional = false python-versions = "*" groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and (python_version <= \"3.11\" or python_version >= \"3.12\")" +markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" files = [ {file = "triton-3.3.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b74db445b1c562844d3cfad6e9679c72e93fdfb1a90a24052b03bb5c49d1242e"}, {file = "triton-3.3.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b31e3aa26f8cb3cc5bf4e187bf737cbacf17311e1112b781d4a059353dfd731b"}, @@ -5964,7 +5749,6 @@ description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" groups = ["main", "dev", "docs", "examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8"}, {file = "typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5"}, @@ -5977,7 +5761,6 @@ description = "Provider of IANA time zone data" optional = false python-versions = ">=2" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931"}, {file = "tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415"}, @@ -5990,7 +5773,6 @@ description = "RFC 6570 URI Template Processor" optional = false python-versions = ">=3.7" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "uri-template-1.3.0.tar.gz", hash = "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7"}, {file = "uri_template-1.3.0-py3-none-any.whl", hash = "sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363"}, @@ -6006,40 +5788,16 @@ description = "HTTP library with thread-safe connection pooling, file post, and optional = false python-versions = ">=3.9" groups = ["main", "docs", "examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, ] [package.extras] -brotli = ["brotli (>=1.2.0)", "brotlicffi (>=1.2.0.0)"] +brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["backports-zstd (>=1.0.0)"] - -[[package]] -name = "virtualenv" -version = "20.35.4" -description = "Virtual Python Environment builder" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -markers = "python_version <= \"3.11\" and python_version >= \"3.10\" or python_version >= \"3.12\"" -files = [ - {file = "virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b"}, - {file = "virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c"}, -] - -[package.dependencies] -distlib = ">=0.3.7,<1" -filelock = ">=3.12.2,<4" -platformdirs = ">=3.9.1,<5" -typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\""} - -[package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] +zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] [[package]] name = "virtualenv" @@ -6048,7 +5806,6 @@ description = "Virtual Python Environment builder" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version < \"3.10\"" files = [ {file = "virtualenv-21.6.1-py3-none-any.whl", hash = "sha256:afe991df855715a2b2f60edfcc0107ef95a79fdfd8cb4cdaa71603d1c12e463b"}, {file = "virtualenv-21.6.1.tar.gz", hash = "sha256:15f978b7cd329f24855ff4a0c4b4899cc7678589f49adbdcbbb4d3232e641128"}, @@ -6056,7 +5813,7 @@ files = [ [package.dependencies] distlib = ">=0.3.7,<1" -filelock = {version = ">=3.16.1,<=3.19.1", markers = "python_version < \"3.10\""} +filelock = {version = ">=3.24.2,<4", markers = "python_version >= \"3.10\""} platformdirs = ">=3.9.1,<5" python-discovery = ">=1.4.2" typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\""} @@ -6068,7 +5825,6 @@ description = "Measures the displayed width of unicode strings in a terminal" optional = false python-versions = ">=3.8" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "wcwidth-0.8.2-py3-none-any.whl", hash = "sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85"}, {file = "wcwidth-0.8.2.tar.gz", hash = "sha256:91fbef97204b96a3d4d421609b80340b760cf33e26da123ff243d76b1fda8dda"}, @@ -6081,7 +5837,6 @@ description = "A library for working with the color formats defined by HTML and optional = false python-versions = ">=3.9" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "webcolors-24.11.1-py3-none-any.whl", hash = "sha256:515291393b4cdf0eb19c155749a096f779f7d909f7cceea072791cb9095b92e9"}, {file = "webcolors-24.11.1.tar.gz", hash = "sha256:ecb3d768f32202af770477b8b65f318fa4f566c22948673a977b00d589dd80f6"}, @@ -6094,7 +5849,6 @@ description = "Character encoding aliases for legacy web content" optional = false python-versions = "*" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, @@ -6107,7 +5861,6 @@ description = "WebSocket client for Python with low level API options" optional = false python-versions = ">=3.9" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef"}, {file = "websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98"}, @@ -6125,7 +5878,6 @@ description = "Jupyter interactive widgets for Jupyter Notebook" optional = false python-versions = ">=3.7" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "widgetsnbextension-4.0.15-py3-none-any.whl", hash = "sha256:8156704e4346a571d9ce73b84bee86a29906c9abfd7223b7228a28899ccf3366"}, {file = "widgetsnbextension-4.0.15.tar.gz", hash = "sha256:de8610639996f1567952d763a5a41af8af37f2575a41f9852a38f947eb82a3b9"}, @@ -6138,7 +5890,6 @@ description = "Module for decorators, wrappers and monkey patching." optional = false python-versions = ">=3.9" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "wrapt-2.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:055e6fcfaa28e58c6a8c247d48b92be9d56f818b7068aa4f22b15b3343a09931"}, {file = "wrapt-2.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8374eb6b1a58809211e84ff835a182bb17ab2807a5bfef23204c8cff38178a00"}, @@ -6242,7 +5993,6 @@ description = "Python binding for xxHash" optional = false python-versions = ">=3.8" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "xxhash-3.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:27a9e475157f7315826118e3f3127909a0fe25f1b43d3d3be9c584f9d265f937"}, {file = "xxhash-3.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9b2ce44bf8f4a1d01f418b3110ff8dff32fd3f3e836c0e06333c3725f243fa6c"}, @@ -6440,7 +6190,6 @@ description = "Yet another URL library" optional = false python-versions = ">=3.9" groups = ["examples"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "yarl-1.22.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c7bd6683587567e5a49ee6e336e0612bec8329be1b7d4c8af5687dcdeb67ee1e"}, {file = "yarl-1.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5cdac20da754f3a723cceea5b3448e1a2074866406adeb4ef35b469d089adb8f"}, @@ -6579,31 +6328,10 @@ idna = ">=2.0" multidict = ">=4.0" propcache = ">=0.2.1" -[[package]] -name = "zipp" -version = "3.23.1" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.9" -groups = ["docs", "examples"] -markers = "python_version < \"3.10\"" -files = [ - {file = "zipp-3.23.1-py3-none-any.whl", hash = "sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc"}, - {file = "zipp-3.23.1.tar.gz", hash = "sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] -type = ["pytest-mypy"] - [extras] sbert = ["sentence-transformers"] [metadata] lock-version = "2.1" -python-versions = ">=3.9,<4.0" -content-hash = "5036e15a2000c6239d284fa557eaa832bc8a61f0d7189e71d14cf9bef4bdf25c" +python-versions = ">=3.10,<4.0" +content-hash = "7fa833c37c93c8375c3afad6122a8164c56620f52723c0659c13293a74f12de2" diff --git a/pyproject.toml b/pyproject.toml index c00ccae..0812c66 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,6 @@ classifiers = [ "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -20,7 +19,7 @@ classifiers = [ ] [tool.poetry.dependencies] -python = ">=3.9,<4.0" +python = ">=3.10,<4.0" numpy = ">=1.21,<2.0" safetensors = ">=0.4.0" smart_open = ">=6.4.0"