From 1812a75f31f599721a285526121f08039a656273 Mon Sep 17 00:00:00 2001 From: maskedsyntax Date: Thu, 19 Feb 2026 16:21:20 +0530 Subject: [PATCH] refactor: centralize config and remove dead code - Create hashprep/config.py with all thresholds as typed dataclasses - Replace 80+ magic numbers across check files with config references - Remove ~160 lines of commented-out code in summaries/variables.py - Remove dead _check_feature_correlation() function in correlations.py - Remove unused imports (f_oneway, infer_types, numpy) in correlations.py and type_inference.py - Fix redundant infer_types() call: pass column_types from analyzer to summarize_variables() - Remove unused dependencies: fastapi, starlette, brotli from pyproject.toml - Clean up unused variables (text_cols, corr_methods, default_methods) in correlations.py --- hashprep/checks/columns.py | 7 +- hashprep/checks/correlations.py | 72 ++--------- hashprep/checks/distribution.py | 10 +- hashprep/checks/drift.py | 12 +- hashprep/checks/imbalance.py | 3 +- hashprep/checks/leakage.py | 11 +- hashprep/checks/missing_values.py | 25 ++-- hashprep/checks/outliers.py | 25 ++-- hashprep/config.py | 168 +++++++++++++++++++++++++ hashprep/core/analyzer.py | 2 +- hashprep/summaries/variables.py | 179 ++------------------------- hashprep/utils/sampling.py | 7 +- hashprep/utils/type_inference.py | 16 +-- pyproject.toml | 3 - uv.lock | 199 ------------------------------ 15 files changed, 258 insertions(+), 481 deletions(-) create mode 100644 hashprep/config.py diff --git a/hashprep/checks/columns.py b/hashprep/checks/columns.py index 799f452..b37a5eb 100644 --- a/hashprep/checks/columns.py +++ b/hashprep/checks/columns.py @@ -1,4 +1,7 @@ from .core import Issue +from ..config import DEFAULT_CONFIG + +_COL_THRESHOLDS = DEFAULT_CONFIG.columns def _check_single_value_columns(analyzer): issues = [] @@ -23,7 +26,7 @@ def _check_single_value_columns(analyzer): ) return issues -def _check_high_cardinality(analyzer, threshold: int = 100, critical_threshold: float = 0.9): +def _check_high_cardinality(analyzer, threshold: int = _COL_THRESHOLDS.high_cardinality_count, critical_threshold: float = _COL_THRESHOLDS.high_cardinality_ratio_critical): issues = [] categorical_cols = analyzer.df.select_dtypes(include="object").columns.tolist() for col in categorical_cols: @@ -54,7 +57,7 @@ def _check_duplicates(analyzer): duplicate_rows = int(analyzer.df.duplicated().sum()) if duplicate_rows > 0: duplicate_ratio = float(duplicate_rows / len(analyzer.df)) - severity = "critical" if duplicate_ratio > 0.1 else "warning" + severity = "critical" if duplicate_ratio > _COL_THRESHOLDS.duplicate_ratio_critical else "warning" impact = "high" if severity == "critical" else "medium" quick_fix = ( "Options: \n- Drop duplicates: Ensures data integrity (Pros: Cleaner data; Cons: May lose valid repeats).\n- Verify duplicates: Check if intentional (e.g., time-series) (Pros: Validates data; Cons: Time-consuming)." diff --git a/hashprep/checks/correlations.py b/hashprep/checks/correlations.py index 04e7483..4854403 100644 --- a/hashprep/checks/correlations.py +++ b/hashprep/checks/correlations.py @@ -1,24 +1,16 @@ from .core import Issue import pandas as pd import numpy as np -from scipy.stats import f_oneway, spearmanr, pearsonr, kendalltau, chi2_contingency +from scipy.stats import spearmanr, pearsonr, kendalltau, chi2_contingency from itertools import combinations from .discretizer import Discretizer, DiscretizationType -from ..utils.type_inference import infer_types, is_usable_for_corr - - -# Thresholds -CORR_THRESHOLDS = { - 'numeric': { - 'spearman': {'warning': 0.7, 'critical': 0.95}, - 'pearson': {'warning': 0.7, 'critical': 0.95}, - 'kendall': {'warning': 0.6, 'critical': 0.85}, # Lower for Kendall (typically smaller values) - }, - 'categorical': {'warning': 0.5, 'critical': 0.8}, - 'mixed': {'warning': 0.5, 'critical': 0.8}, # Updated to coefficient thresholds (matching categorical) for Cramer's V -} -CAT_MAX_DISTINCT = 50 -LOW_CARD_NUM_THRESHOLD = 10 # From type_inference.py +from ..utils.type_inference import is_usable_for_corr +from ..config import DEFAULT_CONFIG + +_CORR = DEFAULT_CONFIG.correlations +CORR_THRESHOLDS = _CORR.as_nested_dict() +CAT_MAX_DISTINCT = _CORR.max_distinct_categories +LOW_CARD_NUM_THRESHOLD = _CORR.low_cardinality_numeric def _cramers_v_corrected(table: pd.DataFrame) -> float: if table.empty or (table.shape[0] == 1 or table.shape[1] == 1): @@ -52,28 +44,20 @@ def calculate_correlations(analyzer, thresholds=None): typ == 'Numeric' and is_usable_for_corr(analyzer.df[col])] cat_cols = [col for col, typ in inferred_types.items() if typ == 'Categorical' and 1 < analyzer.df[col].nunique() <= CAT_MAX_DISTINCT and is_usable_for_corr(analyzer.df[col])] - text_cols = [col for col, typ in inferred_types.items() if typ == 'Text'] - # Internal default methods - default_methods = ['spearman', 'pearson'] - issues.extend(_check_numeric_correlation(analyzer, numeric_cols, thresholds['numeric'], default_methods)) + issues.extend(_check_numeric_correlation(analyzer, numeric_cols, thresholds['numeric'])) issues.extend(_check_categorical_correlation(analyzer, cat_cols, thresholds['categorical'])) issues.extend(_check_mixed_correlation(analyzer, numeric_cols, cat_cols, thresholds['mixed'])) return issues -def _check_numeric_correlation(analyzer, numeric_cols: list, thresholds: dict, methods: list): +def _check_numeric_correlation(analyzer, numeric_cols: list, thresholds: dict): issues = [] if len(numeric_cols) < 2: return issues num_df = analyzer.df[numeric_cols].dropna(how='all') - corr_methods = { - 'spearman': lambda x, y: spearmanr(x, y), - 'pearson': lambda x, y: pearsonr(x, y), - 'kendall': lambda x, y: kendalltau(x, y) - } for col1, col2 in combinations(numeric_cols, 2): series1, series2 = num_df[col1].dropna(), num_df[col2].dropna() @@ -125,42 +109,6 @@ def _check_numeric_correlation(analyzer, numeric_cols: list, thresholds: dict, m return issues -def _check_feature_correlation( - analyzer, threshold: float = 0.95, critical_threshold: float = 0.98 -): - issues = [] - numeric_df = analyzer.df.select_dtypes(include="number") - if numeric_df.empty: - return issues - corr_matrix = numeric_df.corr().abs() - upper = corr_matrix.where(np.tril(np.ones(corr_matrix.shape)).astype(bool)) - correlated_pairs = [ - (col, row, float(val)) - for row in upper.index - for col, val in upper[row].dropna().items() - if val > threshold and col != row - ] - for col1, col2, corr in correlated_pairs: - severity = "critical" if corr > critical_threshold else "warning" - impact = "high" if severity == "critical" else "medium" - quick_fix = ( - "Options: \n- Drop one feature: Reduces multicollinearity (Pros: Simplifies model; Cons: Loses info).\n- Combine features: Create composite feature (e.g., PCA) (Pros: Retains info; Cons: Less interpretable).\n- Retain and test: Use robust models (e.g., trees) (Pros: Keeps info; Cons: May affect sensitive models)." - if severity == "critical" - else "Options: \n- Drop one feature: If less predictive (Pros: Simplifies model; Cons: Loses info).\n- Retain and test: Evaluate with robust models (Pros: Keeps info; Cons: Risk of multicollinearity).\n- Engineer feature: Combine or transform features (Pros: Reduces redundancy; Cons: Adds complexity)." - ) - issues.append( - Issue( - category="feature_correlation", - severity=severity, - column=f"{col1},{col2}", - description=f"Columns '{col1}' and '{col2}' are highly correlated ({corr:.2f})", - impact_score=impact, - quick_fix=quick_fix, - ) - ) - return issues - - def _check_categorical_correlation(analyzer, cat_cols: list, thresholds: dict): issues = [] if len(cat_cols) < 2: diff --git a/hashprep/checks/distribution.py b/hashprep/checks/distribution.py index 475b304..82fa866 100644 --- a/hashprep/checks/distribution.py +++ b/hashprep/checks/distribution.py @@ -3,9 +3,11 @@ from scipy.stats import kstest from .core import Issue +from ..config import DEFAULT_CONFIG +_DIST = DEFAULT_CONFIG.distribution -def _check_uniform_distribution(analyzer, p_threshold: float = 0.1) -> List[Issue]: +def _check_uniform_distribution(analyzer, p_threshold: float = _DIST.uniform_p_value) -> List[Issue]: """ Detect uniformly distributed numeric columns using Kolmogorov-Smirnov test. Uniform distributions often indicate synthetic IDs or sequential data. @@ -14,7 +16,7 @@ def _check_uniform_distribution(analyzer, p_threshold: float = 0.1) -> List[Issu for col in analyzer.df.select_dtypes(include="number").columns: series = analyzer.df[col].dropna() - if len(series) < 20: + if len(series) < _DIST.uniform_min_samples: continue min_val, max_val = series.min(), series.max() @@ -46,7 +48,7 @@ def _check_uniform_distribution(analyzer, p_threshold: float = 0.1) -> List[Issu return issues -def _check_unique_values(analyzer, threshold: float = 0.95) -> List[Issue]: +def _check_unique_values(analyzer, threshold: float = _DIST.unique_value_ratio) -> List[Issue]: """ Detect columns where nearly all values are unique. High uniqueness often indicates identifiers, names, or free-text fields. @@ -55,7 +57,7 @@ def _check_unique_values(analyzer, threshold: float = 0.95) -> List[Issue]: for col in analyzer.df.columns: series = analyzer.df[col].dropna() - if len(series) < 10: + if len(series) < _DIST.unique_min_samples: continue unique_count = series.nunique() diff --git a/hashprep/checks/drift.py b/hashprep/checks/drift.py index cea28f7..e50b8fe 100644 --- a/hashprep/checks/drift.py +++ b/hashprep/checks/drift.py @@ -3,15 +3,17 @@ from scipy.stats import chisquare, ks_2samp from .core import Issue +from ..config import DEFAULT_CONFIG -CRITICAL_P_VALUE = 0.001 -MAX_CATEGORIES_FOR_CHI2 = 50 +_DRIFT = DEFAULT_CONFIG.drift +CRITICAL_P_VALUE = _DRIFT.critical_p_value +MAX_CATEGORIES_FOR_CHI2 = _DRIFT.max_categories_for_chi2 def check_drift( df_train: pd.DataFrame, df_test: pd.DataFrame, - threshold: float = 0.05, + threshold: float = _DRIFT.p_value, ) -> list[Issue]: """ Check for distribution shift between two datasets. @@ -80,13 +82,13 @@ def _check_categorical_drift( new_categories = set(test_counts.index) - set(train_counts.index) if new_categories: - sample_new = list(new_categories)[:5] + sample_new = list(new_categories)[:_DRIFT.max_new_category_samples] issues.append( Issue( category="dataset_drift", severity="warning", column=col, - description=f"New categories in test set for '{col}': {sample_new}{'...' if len(new_categories) > 5 else ''}", + description=f"New categories in test set for '{col}': {sample_new}{'...' if len(new_categories) > _DRIFT.max_new_category_samples else ''}", impact_score="medium", quick_fix="Handle unseen categories in preprocessing pipeline (e.g., OrdinalEncoder with unknown_value).", ) diff --git a/hashprep/checks/imbalance.py b/hashprep/checks/imbalance.py index 15c2507..081ee62 100644 --- a/hashprep/checks/imbalance.py +++ b/hashprep/checks/imbalance.py @@ -1,6 +1,7 @@ from .core import Issue +from ..config import DEFAULT_CONFIG -def _check_class_imbalance(analyzer, threshold: float = 0.9): +def _check_class_imbalance(analyzer, threshold: float = DEFAULT_CONFIG.imbalance.majority_class_ratio): issues = [] if analyzer.target_col and analyzer.target_col in analyzer.df.columns: counts = analyzer.df[analyzer.target_col].value_counts(normalize=True) diff --git a/hashprep/checks/leakage.py b/hashprep/checks/leakage.py index 304e518..26d5901 100644 --- a/hashprep/checks/leakage.py +++ b/hashprep/checks/leakage.py @@ -2,6 +2,9 @@ import pandas as pd from scipy.stats import chi2_contingency, f_oneway import numpy as np +from ..config import DEFAULT_CONFIG + +_LEAK = DEFAULT_CONFIG.leakage def _check_data_leakage(analyzer): issues = [] @@ -36,7 +39,7 @@ def _check_target_leakage_patterns(analyzer): corrs = numeric_cols.corrwith(target).abs() for col, corr in corrs.items(): severity = ( - "critical" if corr > 0.98 else "warning" if corr > 0.95 else None + "critical" if corr > _LEAK.numeric_critical else "warning" if corr > _LEAK.numeric_warning else None ) if severity: impact = "high" if severity == "critical" else "medium" @@ -69,7 +72,7 @@ def _check_target_leakage_patterns(analyzer): r, k = table.shape cramers_v = np.sqrt(phi2 / min(k - 1, r - 1)) severity = ( - "critical" if cramers_v > 0.95 else "warning" if cramers_v > 0.8 else None + "critical" if cramers_v > _LEAK.categorical_critical else "warning" if cramers_v > _LEAK.categorical_warning else None ) if severity: impact = "high" if severity == "critical" else "medium" @@ -104,8 +107,8 @@ def _check_target_leakage_patterns(analyzer): try: f_stat, p_val = f_oneway(*groups) severity = ( - "critical" if f_stat > 20.0 and p_val < 0.001 - else "warning" if f_stat > 10.0 and p_val < 0.001 else None + "critical" if f_stat > _LEAK.f_stat_critical and p_val < _LEAK.f_stat_p_value + else "warning" if f_stat > _LEAK.f_stat_warning and p_val < _LEAK.f_stat_p_value else None ) if severity: impact = "high" if severity == "critical" else "medium" diff --git a/hashprep/checks/missing_values.py b/hashprep/checks/missing_values.py index 2e4e1d5..36f7ab5 100644 --- a/hashprep/checks/missing_values.py +++ b/hashprep/checks/missing_values.py @@ -3,8 +3,11 @@ import pandas as pd from collections import defaultdict import numpy as np +from ..config import DEFAULT_CONFIG -def _check_high_missing_values(analyzer, threshold: float = 0.4, critical_threshold: float = 0.7): +_THRESHOLDS = DEFAULT_CONFIG.missing_values + +def _check_high_missing_values(analyzer, threshold: float = _THRESHOLDS.warning, critical_threshold: float = _THRESHOLDS.critical): issues = [] for col in analyzer.df.columns: missing_pct = float(analyzer.df[col].isna().mean()) @@ -44,7 +47,7 @@ def _check_empty_columns(analyzer): ) return issues -def _check_dataset_missingness(analyzer, threshold: float = 20.0, critical_threshold: float = 50.0): +def _check_dataset_missingness(analyzer, threshold: float = _THRESHOLDS.dataset_warning_pct, critical_threshold: float = _THRESHOLDS.dataset_critical_pct): issues = [] missing_pct = float( (analyzer.df.isnull().sum().sum() / (analyzer.df.shape[0] * analyzer.df.shape[1])) * 100 @@ -70,11 +73,11 @@ def _check_dataset_missingness(analyzer, threshold: float = 20.0, critical_thres return issues -def _check_missing_patterns(analyzer, threshold: float = 0.01, - critical_p_threshold: float = 0.001): +def _check_missing_patterns(analyzer, threshold: float = _THRESHOLDS.pattern_p_value, + critical_p_threshold: float = _THRESHOLDS.pattern_critical_p_value): issues = [] missing_cols = [ - col for col in analyzer.df.columns if int(analyzer.df[col].isna().sum()) >= 10 + col for col in analyzer.df.columns if int(analyzer.df[col].isna().sum()) >= _THRESHOLDS.pattern_min_missing_count ] # grouping logic @@ -89,7 +92,7 @@ def _check_missing_patterns(analyzer, threshold: float = 0.01, continue try: value_counts = analyzer.df[other_col].value_counts() - rare_cats = value_counts[value_counts < 5].index + rare_cats = value_counts[value_counts < _THRESHOLDS.pattern_rare_category_count].index temp_col = analyzer.df[other_col].copy() if not rare_cats.empty: temp_col = temp_col.where(~temp_col.isin(rare_cats), "Other") @@ -112,7 +115,7 @@ def cramers_v(table): return np.sqrt(phi2corr / rkcorr) cramers = cramers_v(table) - if p_val < threshold and cramers > 0.1: + if p_val < threshold and cramers > _THRESHOLDS.pattern_cramers_v_min: cat_patterns[col].append((other_col, p_val, cramers)) except Exception: continue @@ -125,7 +128,7 @@ def cramers_v(table): try: missing = analyzer.df[analyzer.df[col].isna()][other_col].dropna() non_missing = analyzer.df[analyzer.df[col].notna()][other_col].dropna() - if len(missing) < 10 or len(non_missing) < 10: + if len(missing) < _THRESHOLDS.pattern_min_group_size or len(non_missing) < _THRESHOLDS.pattern_min_group_size: continue # Replaced f_oneway with mannwhitneyu @@ -135,7 +138,7 @@ def cramers_v(table): pooled_std = np.sqrt((np.std(missing) ** 2 + np.std(non_missing) ** 2) / 2) cohens_d = abs(np.mean(missing) - np.mean(non_missing)) / pooled_std if pooled_std > 0 else 0 - if p_val < threshold and cohens_d > 0.2: + if p_val < threshold and cohens_d > _THRESHOLDS.pattern_cohens_d_min: num_patterns[col].append((other_col, p_val, cohens_d)) except Exception: continue @@ -151,7 +154,7 @@ def cramers_v(table): if all_patterns: # Sort by effect size (descending) and take top 3 all_patterns.sort(key=lambda x: x[2], reverse=True) # x[2] is effect size - top_corrs = [pat[0] for pat in all_patterns[:3]] + top_corrs = [pat[0] for pat in all_patterns[:_THRESHOLDS.pattern_top_correlations]] total_count = len(all_patterns) desc = f"Missingness in '{col}' correlates with {total_count} columns ({', '.join(top_corrs)})" @@ -161,7 +164,7 @@ def cramers_v(table): is_target_correlated = any(pat[0] == analyzer.target_col for pat in all_patterns) severity = ( "critical" - if p_val < critical_p_threshold and is_target_correlated and max_effect > 0.3 # medium effect threshold + if p_val < critical_p_threshold and is_target_correlated and max_effect > _THRESHOLDS.pattern_effect_critical else "warning" ) impact = "high" if severity == "critical" else "medium" diff --git a/hashprep/checks/outliers.py b/hashprep/checks/outliers.py index debca39..9f79638 100644 --- a/hashprep/checks/outliers.py +++ b/hashprep/checks/outliers.py @@ -1,8 +1,11 @@ from .core import Issue import pandas as pd import numpy as np +from ..config import DEFAULT_CONFIG -def _check_outliers(analyzer, z_threshold: float = 4.0): +_THRESHOLDS = DEFAULT_CONFIG.outliers + +def _check_outliers(analyzer, z_threshold: float = _THRESHOLDS.z_score): issues = [] for col in analyzer.df.select_dtypes(include="number").columns: series = analyzer.df[col].dropna() @@ -12,7 +15,7 @@ def _check_outliers(analyzer, z_threshold: float = 4.0): outlier_count = int((abs(z_scores) > z_threshold).sum()) if outlier_count > 0: outlier_ratio = float(outlier_count / len(series)) - severity = "critical" if outlier_ratio > 0.1 else "warning" + severity = "critical" if outlier_ratio > _THRESHOLDS.outlier_ratio_critical else "warning" impact = "high" if severity == "critical" else "medium" quick_fix = ( "Options: \n- Remove outliers: Improves model stability (Pros: Reduces noise; Cons: Loses data).\n- Winsorize: Cap extreme values (Pros: Retains data; Cons: Alters distribution).\n- Transform: Apply log/sqrt to reduce impact (Pros: Preserves info; Cons: Changes interpretation)." @@ -31,7 +34,7 @@ def _check_outliers(analyzer, z_threshold: float = 4.0): ) return issues -def _check_high_zero_counts(analyzer, threshold: float = 0.5, critical_threshold: float = 0.8): +def _check_high_zero_counts(analyzer, threshold: float = _THRESHOLDS.zero_count_warning, critical_threshold: float = _THRESHOLDS.zero_count_critical): issues = [] for col in analyzer.df.select_dtypes(include="number").columns: series = analyzer.df[col].dropna() @@ -58,7 +61,7 @@ def _check_high_zero_counts(analyzer, threshold: float = 0.5, critical_threshold ) return issues -def _check_extreme_text_lengths(analyzer, max_threshold: int = 1000, min_threshold: int = 1): +def _check_extreme_text_lengths(analyzer, max_threshold: int = _THRESHOLDS.text_length_max, min_threshold: int = _THRESHOLDS.text_length_min): issues = [] for col in analyzer.df.select_dtypes(include="object").columns: series = analyzer.df[col].dropna().astype(str) @@ -69,7 +72,7 @@ def _check_extreme_text_lengths(analyzer, max_threshold: int = 1000, min_thresho extreme_ratio = float( ((lengths > max_threshold) | (lengths < min_threshold)).mean() ) - severity = "critical" if extreme_ratio > 0.1 else "warning" + severity = "critical" if extreme_ratio > _THRESHOLDS.extreme_ratio_critical else "warning" impact = "high" if severity == "critical" else "medium" quick_fix = ( "Options: \n- Truncate values: Cap extreme lengths (Pros: Stabilizes model; Cons: Loses info).\n- Filter outliers: Remove extreme entries (Pros: Reduces noise; Cons: Loses data).\n- Transform: Normalize lengths (e.g., log) (Pros: Retains info; Cons: Changes interpretation)." @@ -88,11 +91,11 @@ def _check_extreme_text_lengths(analyzer, max_threshold: int = 1000, min_thresho ) return issues -def _check_skewness(analyzer, skew_threshold: float = 3.0, critical_skew_threshold: float = 10.0): +def _check_skewness(analyzer, skew_threshold: float = _THRESHOLDS.skewness_warning, critical_skew_threshold: float = _THRESHOLDS.skewness_critical): issues = [] for col in analyzer.df.select_dtypes(include="number").columns: series = analyzer.df[col].dropna() - if len(series) < 10: + if len(series) < _THRESHOLDS.min_sample_size: continue skewness = float(series.skew()) abs_skew = abs(skewness) @@ -117,7 +120,7 @@ def _check_skewness(analyzer, skew_threshold: float = 3.0, critical_skew_thresho ) return issues -def _check_datetime_skew(analyzer, threshold: float = 0.8): +def _check_datetime_skew(analyzer, threshold: float = _THRESHOLDS.datetime_skew): issues = [] for col in analyzer.df.select_dtypes(include="datetime64").columns: series = pd.to_datetime(analyzer.df[col], errors="coerce").dropna() @@ -138,7 +141,7 @@ def _check_datetime_skew(analyzer, threshold: float = 0.8): return issues -def _check_infinite_values(analyzer, threshold: float = 0.01): +def _check_infinite_values(analyzer, threshold: float = _THRESHOLDS.infinite_ratio_critical): """Detect columns with infinite values.""" issues = [] for col in analyzer.df.select_dtypes(include="number").columns: @@ -166,12 +169,12 @@ def _check_infinite_values(analyzer, threshold: float = 0.01): return issues -def _check_constant_length(analyzer, threshold: float = 0.95): +def _check_constant_length(analyzer, threshold: float = _THRESHOLDS.constant_length_ratio): """Detect string columns where all values have the same length (e.g., IDs, codes).""" issues = [] for col in analyzer.df.select_dtypes(include="object").columns: series = analyzer.df[col].dropna().astype(str) - if len(series) < 10: + if len(series) < _THRESHOLDS.min_sample_size: continue lengths = series.str.len() most_common_length_ratio = lengths.value_counts(normalize=True).iloc[0] if len(lengths) > 0 else 0 diff --git a/hashprep/config.py b/hashprep/config.py new file mode 100644 index 0000000..9c73c00 --- /dev/null +++ b/hashprep/config.py @@ -0,0 +1,168 @@ +"""Centralized configuration for all HashPrep thresholds and defaults. + +All magic numbers and thresholds are defined here to make them: +- Discoverable: one place to find all tuning knobs +- Configurable: easy to override for different use cases +- Documented: each threshold explains its purpose +""" + +from dataclasses import dataclass, field +from typing import Dict + + +@dataclass(frozen=True) +class MissingValueThresholds: + """Thresholds for missing value detection.""" + warning: float = 0.4 + critical: float = 0.7 + dataset_warning_pct: float = 20.0 + dataset_critical_pct: float = 50.0 + pattern_p_value: float = 0.01 + pattern_critical_p_value: float = 0.001 + pattern_cramers_v_min: float = 0.1 + pattern_cohens_d_min: float = 0.2 + pattern_effect_critical: float = 0.3 + pattern_min_missing_count: int = 10 + pattern_min_group_size: int = 10 + pattern_rare_category_count: int = 5 + pattern_top_correlations: int = 3 + + +@dataclass(frozen=True) +class OutlierThresholds: + """Thresholds for outlier detection.""" + z_score: float = 4.0 + outlier_ratio_critical: float = 0.1 + zero_count_warning: float = 0.5 + zero_count_critical: float = 0.8 + text_length_max: int = 1000 + text_length_min: int = 1 + extreme_ratio_critical: float = 0.1 + skewness_warning: float = 3.0 + skewness_critical: float = 10.0 + datetime_skew: float = 0.8 + infinite_ratio_critical: float = 0.01 + constant_length_ratio: float = 0.95 + min_sample_size: int = 10 + + +@dataclass(frozen=True) +class ColumnThresholds: + """Thresholds for column-level checks.""" + high_cardinality_count: int = 100 + high_cardinality_ratio_critical: float = 0.9 + duplicate_ratio_critical: float = 0.1 + + +@dataclass(frozen=True) +class CorrelationThresholds: + """Thresholds for correlation analysis.""" + spearman_warning: float = 0.7 + spearman_critical: float = 0.95 + pearson_warning: float = 0.7 + pearson_critical: float = 0.95 + kendall_warning: float = 0.6 + kendall_critical: float = 0.85 + categorical_warning: float = 0.5 + categorical_critical: float = 0.8 + mixed_warning: float = 0.5 + mixed_critical: float = 0.8 + max_distinct_categories: int = 50 + low_cardinality_numeric: int = 10 + + def as_nested_dict(self) -> dict: + """Return thresholds in the nested dict format used by correlation checks.""" + return { + 'numeric': { + 'spearman': {'warning': self.spearman_warning, 'critical': self.spearman_critical}, + 'pearson': {'warning': self.pearson_warning, 'critical': self.pearson_critical}, + 'kendall': {'warning': self.kendall_warning, 'critical': self.kendall_critical}, + }, + 'categorical': {'warning': self.categorical_warning, 'critical': self.categorical_critical}, + 'mixed': {'warning': self.mixed_warning, 'critical': self.mixed_critical}, + } + + +@dataclass(frozen=True) +class LeakageThresholds: + """Thresholds for data leakage detection.""" + numeric_critical: float = 0.98 + numeric_warning: float = 0.95 + categorical_critical: float = 0.95 + categorical_warning: float = 0.8 + f_stat_critical: float = 20.0 + f_stat_warning: float = 10.0 + f_stat_p_value: float = 0.001 + + +@dataclass(frozen=True) +class DriftThresholds: + """Thresholds for dataset drift detection.""" + p_value: float = 0.05 + critical_p_value: float = 0.001 + max_categories_for_chi2: int = 50 + max_new_category_samples: int = 5 + + +@dataclass(frozen=True) +class DistributionThresholds: + """Thresholds for distribution checks.""" + uniform_p_value: float = 0.1 + uniform_min_samples: int = 20 + unique_value_ratio: float = 0.95 + unique_min_samples: int = 10 + + +@dataclass(frozen=True) +class ImbalanceThresholds: + """Thresholds for class imbalance detection.""" + majority_class_ratio: float = 0.9 + + +@dataclass(frozen=True) +class TypeInferenceConfig: + """Configuration for type inference.""" + cat_cardinality_threshold: int = 50 + cat_percentage_threshold: float = 0.05 + num_low_cat_threshold: int = 10 + bool_mappings: Dict[str, bool] = field(default_factory=lambda: { + 'true': True, 'false': False, + 'yes': True, 'no': False, + 't': True, 'f': False, + }) + + +@dataclass(frozen=True) +class SamplingDefaults: + """Default values for dataset sampling.""" + max_rows: int = 100_000 + memory_threshold_mb: float = 500.0 + + +@dataclass(frozen=True) +class SummaryDefaults: + """Defaults for summary generation.""" + histogram_bins: int = 10 + top_n_values: int = 10 + extreme_values_count: int = 10 + top_n_words: int = 10 + + +@dataclass(frozen=True) +class HashPrepConfig: + """Root configuration aggregating all threshold groups.""" + missing_values: MissingValueThresholds = field(default_factory=MissingValueThresholds) + outliers: OutlierThresholds = field(default_factory=OutlierThresholds) + columns: ColumnThresholds = field(default_factory=ColumnThresholds) + correlations: CorrelationThresholds = field(default_factory=CorrelationThresholds) + leakage: LeakageThresholds = field(default_factory=LeakageThresholds) + drift: DriftThresholds = field(default_factory=DriftThresholds) + distribution: DistributionThresholds = field(default_factory=DistributionThresholds) + imbalance: ImbalanceThresholds = field(default_factory=ImbalanceThresholds) + type_inference: TypeInferenceConfig = field(default_factory=TypeInferenceConfig) + sampling: SamplingDefaults = field(default_factory=SamplingDefaults) + summaries: SummaryDefaults = field(default_factory=SummaryDefaults) + + +# Global default config instance +DEFAULT_CONFIG = HashPrepConfig() diff --git a/hashprep/core/analyzer.py b/hashprep/core/analyzer.py index 32c7f24..5062c6a 100644 --- a/hashprep/core/analyzer.py +++ b/hashprep/core/analyzer.py @@ -112,7 +112,7 @@ def analyze(self) -> Dict: self.df, column_types=self.column_types ) self.summaries["reproduction_info"] = add_reproduction_info(self.df) - self.summaries["variables"] = summarize_variables(self.df) + self.summaries["variables"] = summarize_variables(self.df, column_types=self.column_types) self.summaries.update(summarize_interactions(self.df)) self.summaries.update(summarize_missing_values(self.df)) diff --git a/hashprep/summaries/variables.py b/hashprep/summaries/variables.py index fbbbfe3..6b5243f 100644 --- a/hashprep/summaries/variables.py +++ b/hashprep/summaries/variables.py @@ -4,9 +4,9 @@ import re from collections import defaultdict from scipy.stats import median_abs_deviation +from ..config import DEFAULT_CONFIG -from ..utils.type_inference import infer_types - +_SUMMARY = DEFAULT_CONFIG.summaries def get_monotonicity(series: pd.Series) -> str: if series.is_monotonic_increasing: @@ -17,8 +17,11 @@ def get_monotonicity(series: pd.Series) -> str: return "none" -def summarize_variables(df): - inferred_types = infer_types(df) +def summarize_variables(df, column_types=None): + if column_types is None: + from ..utils.type_inference import infer_types + column_types = infer_types(df) + inferred_types = column_types variables = {} for column in df.columns: typ = inferred_types.get(column, "Unsupported") @@ -107,19 +110,19 @@ def _summarize_numeric(df, col): "variance": float(series.var()), "monotonicity": get_monotonicity(series), } - hist, bin_edges = np.histogram(series, bins=10, range=(min_val, max_val)) + hist, bin_edges = np.histogram(series, bins=_SUMMARY.histogram_bins, range=(min_val, max_val)) histogram = { "bin_edges": [float(x) for x in bin_edges], "counts": [int(x) for x in hist], } - vc = series.value_counts().head(10) + vc = series.value_counts().head(_SUMMARY.top_n_values) common_values = { str(v): {"count": int(c), "percentage": float(c / n * 100)} for v, c in vc.items() } extremes = { - "minimum_10": [float(x) for x in sorted(series)[:10]], - "maximum_10": [float(x) for x in sorted(series)[-10:]], + "minimum_10": [float(x) for x in sorted(series)[:_SUMMARY.extreme_values_count]], + "maximum_10": [float(x) for x in sorted(series)[-_SUMMARY.extreme_values_count:]], } stats = { "infinite_count": infinite_count, @@ -375,163 +378,3 @@ def _summarize_boolean(df, col): } stats = {"common_values": common_values} return stats - - -# def summarize_variables(df, include_plots=False): -# variables = {} -# for column in df.columns: -# if pd.api.types.is_numeric_dtype(df[column]): -# variables[column] = _summarize_numeric_column(df, column, include_plots) -# elif pd.api.types.is_datetime64_any_dtype(df[column]): -# variables[column] = _summarize_datetime_column(df, column, include_plots) -# elif pd.api.types.is_string_dtype(df[column]): -# variables[column] = _summarize_text_column(df, column, include_plots) -# else: -# variables[column] = _summarize_categorical_column(df, column, include_plots) -# return variables - - -# def _summarize_numeric_column(df, col, include_plots): -# series = df[col].dropna() -# stats = { -# "count": int(series.count()), -# "mean": float(series.mean().item()) if not series.empty else None, -# "std": float(series.std().item()) if not series.empty else None, -# "min": float(series.min().item()) if not series.empty else None, -# "max": float(series.max().item()) if not series.empty else None, -# "quantiles": ( -# { -# "25%": float(series.quantile(0.25).item()), -# "50%": float(series.quantile(0.50).item()), -# "75%": float(series.quantile(0.75).item()), -# } -# if not series.empty -# else None -# ), -# "missing": int(df[col].isna().sum()), -# "zeros": int((series == 0).sum()), -# } -# if not series.empty: -# hist, bin_edges = np.histogram( -# series, bins=10, range=(series.min(), series.max()) -# ) -# stats["histogram"] = { -# "bin_edges": [float(x) for x in bin_edges], -# "counts": [int(x) for x in hist], -# } -# if include_plots: -# fig, ax = plt.subplots(figsize=(4, 3)) -# sns.histplot(series, bins=10, ax=ax) -# ax.set_title(f"Histogram of {col}") -# ax.set_xlabel(col) -# ax.set_ylabel("Count") -# buf = io.BytesIO() -# fig.savefig(buf, format="png", bbox_inches="tight") -# buf.seek(0) -# img_str = base64.b64encode(buf.getvalue()).decode("utf-8") -# plt.close(fig) -# stats["plot"] = img_str # Store plot directly in stats["plot"] -# else: -# stats["histogram"] = {"bin_edges": None, "counts": None} -# return stats - - -# def _summarize_categorical_column(df, col, include_plots): -# series = df[col].dropna().astype(str) -# stats = { -# "count": int(series.count()), -# "unique": int(series.nunique()), -# "top_values": series.value_counts().head(10).to_dict(), -# "most_frequent": str(series.mode().iloc[0]) if not series.empty else None, -# "missing": int(df[col].isna().sum()), -# } -# if include_plots and not series.empty: -# fig, ax = plt.subplots(figsize=(4, 3)) -# series.value_counts().head(10).plot(kind="bar", ax=ax) -# ax.set_title(f"Top Values of {col}") -# ax.set_xlabel(col) -# ax.set_ylabel("Count") -# plt.xticks(rotation=45, ha="right") -# buf = io.BytesIO() -# fig.savefig(buf, format="png", bbox_inches="tight") -# buf.seek(0) -# img_str = base64.b64encode(buf.getvalue()).decode("utf-8") -# plt.close(fig) -# stats["plot"] = img_str -# return stats - - -# def _summarize_text_column(df, col, include_plots): -# series = df[col].dropna().astype(str) -# lengths = series.str.len() -# stats = { -# "count": int(series.count()), -# "missing": int(df[col].isna().sum()), -# "avg_length": float(lengths.mean().item()) if not lengths.empty else None, -# "min_length": float(lengths.min().item()) if not lengths.empty else None, -# "max_length": float(lengths.max().item()) if not lengths.empty else None, -# "common_lengths": lengths.value_counts().head(5).to_dict(), -# "char_freq": ( -# dict( -# zip( -# list( -# pd.Series(list("".join(series))).value_counts().head(10).index -# ), -# [ -# int(x) -# for x in pd.Series(list("".join(series))) -# .value_counts() -# .head(10) -# .values -# ], -# ) -# ) -# if not series.empty -# else None -# ), -# } -# if include_plots and not lengths.empty: -# fig, ax = plt.subplots(figsize=(4, 3)) -# sns.histplot(lengths, bins=10, ax=ax) -# ax.set_title(f"Length Distribution of {col}") -# ax.set_xlabel("Length") -# ax.set_ylabel("Count") -# buf = io.BytesIO() -# fig.savefig(buf, format="png", bbox_inches="tight") -# buf.seek(0) -# img_str = base64.b64encode(buf.getvalue()).decode("utf-8") -# plt.close(fig) -# stats["plot"] = img_str -# return stats - - -# def _summarize_datetime_column(df, col, include_plots): -# series = pd.to_datetime(df[col], errors="coerce").dropna() -# stats = { -# "count": int(series.count()), -# "missing": int(df[col].isna().sum()), -# "min": str(series.min()) if not series.empty else None, -# "max": str(series.max()) if not series.empty else None, -# "year_counts": ( -# series.dt.year.value_counts().to_dict() if not series.empty else None -# ), -# "month_counts": ( -# series.dt.month.value_counts().to_dict() if not series.empty else None -# ), -# "day_counts": ( -# series.dt.day.value_counts().to_dict() if not series.empty else None -# ), -# } -# if include_plots and not series.empty: -# fig, ax = plt.subplots(figsize=(4, 3)) -# series.dt.year.value_counts().sort_index().plot(kind="bar", ax=ax) -# ax.set_title(f"Year Distribution of {col}") -# ax.set_xlabel("Year") -# ax.set_ylabel("Count") -# buf = io.BytesIO() -# fig.savefig(buf, format="png", bbox_inches="tight") -# buf.seek(0) -# img_str = base64.b64encode(buf.getvalue()).decode("utf-8") -# plt.close(fig) -# stats["plot"] = img_str -# return stats diff --git a/hashprep/utils/sampling.py b/hashprep/utils/sampling.py index dfe6e86..8c7aa5f 100644 --- a/hashprep/utils/sampling.py +++ b/hashprep/utils/sampling.py @@ -3,8 +3,11 @@ import pandas as pd -DEFAULT_MAX_ROWS = 100_000 -DEFAULT_MEMORY_THRESHOLD_MB = 500.0 +from ..config import DEFAULT_CONFIG + +_SAMPLING = DEFAULT_CONFIG.sampling +DEFAULT_MAX_ROWS = _SAMPLING.max_rows +DEFAULT_MEMORY_THRESHOLD_MB = _SAMPLING.memory_threshold_mb @dataclass diff --git a/hashprep/utils/type_inference.py b/hashprep/utils/type_inference.py index 7fa4233..ded5d3f 100644 --- a/hashprep/utils/type_inference.py +++ b/hashprep/utils/type_inference.py @@ -1,14 +1,14 @@ import pandas as pd -import numpy as np -from typing import Dict, List -from scipy.stats import chi2_contingency # For potential future use +from typing import Dict -# Config-like thresholds (mimic ydata's Settings; tune as needed) +from ..config import DEFAULT_CONFIG + +_TYPE_CFG = DEFAULT_CONFIG.type_inference CONFIG = { - 'cat_cardinality_threshold': 50, # Max unique for Categorical (ydata default ~50) - 'cat_percentage_threshold': 0.05, # % unique for Categorical - 'num_low_cat_threshold': 10, # Low unique numerics → Categorical - 'bool_mappings': {'true': True, 'false': False, 'yes': True, 'no': False, 't': True, 'f': False}, # For bool inference + 'cat_cardinality_threshold': _TYPE_CFG.cat_cardinality_threshold, + 'cat_percentage_threshold': _TYPE_CFG.cat_percentage_threshold, + 'num_low_cat_threshold': _TYPE_CFG.num_low_cat_threshold, + 'bool_mappings': _TYPE_CFG.bool_mappings, } def infer_types(df: pd.DataFrame) -> Dict[str, str]: diff --git a/pyproject.toml b/pyproject.toml index 9c61596..9438449 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,6 @@ authors = [ dependencies = [ "click>=8.3.0", - "fastapi>=0.116.1", "fuzzybunny", "jinja2>=3.1.6", "matplotlib>=3.10.6", @@ -25,8 +24,6 @@ dependencies = [ "scikit-learn>=1.7.2", "scipy>=1.15.3", "seaborn>=0.13.2", - "starlette>=0.49.1", - "brotli>=1.2.0", "fonttools>=4.60.2", "tabulate>=0.9.0", "weasyprint>=68.0", diff --git a/uv.lock b/uv.lock index 2cd7cd1..674d497 100644 --- a/uv.lock +++ b/uv.lock @@ -7,39 +7,6 @@ resolution-markers = [ "python_full_version < '3.11'", ] -[[package]] -name = "annotated-doc" -version = "0.0.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, -] - -[[package]] -name = "annotated-types" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, -] - -[[package]] -name = "anyio" -version = "4.10.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, - { name = "idna" }, - { name = "sniffio" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload-time = "2025-08-04T08:54:26.451Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" }, -] - [[package]] name = "brotli" version = "1.2.0" @@ -413,21 +380,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, ] -[[package]] -name = "fastapi" -version = "0.128.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-doc" }, - { name = "pydantic" }, - { name = "starlette" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/52/08/8c8508db6c7b9aae8f7175046af41baad690771c9bcde676419965e338c7/fastapi-0.128.0.tar.gz", hash = "sha256:1cc179e1cef10a6be60ffe429f79b829dce99d8de32d7acb7e6c8dfdf7f2645a", size = 365682, upload-time = "2025-12-27T15:21:13.714Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/05/5cbb59154b093548acd0f4c7c474a118eda06da25aa75c616b72d8fcd92a/fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d", size = 103094, upload-time = "2025-12-27T15:21:12.154Z" }, -] - [[package]] name = "fonttools" version = "4.61.1" @@ -522,9 +474,7 @@ wheels = [ name = "hashprep" source = { editable = "." } dependencies = [ - { name = "brotli" }, { name = "click" }, - { name = "fastapi" }, { name = "fonttools" }, { name = "fuzzybunny" }, { name = "jinja2" }, @@ -537,7 +487,6 @@ dependencies = [ { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "scipy", version = "1.16.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "seaborn" }, - { name = "starlette" }, { name = "tabulate" }, { name = "weasyprint" }, ] @@ -549,9 +498,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "brotli", specifier = ">=1.2.0" }, { name = "click", specifier = ">=8.3.0" }, - { name = "fastapi", specifier = ">=0.116.1" }, { name = "fonttools", specifier = ">=4.60.2" }, { name = "fuzzybunny" }, { name = "jinja2", specifier = ">=3.1.6" }, @@ -562,7 +509,6 @@ requires-dist = [ { name = "scikit-learn", specifier = ">=1.7.2" }, { name = "scipy", specifier = ">=1.15.3" }, { name = "seaborn", specifier = ">=0.13.2" }, - { name = "starlette", specifier = ">=0.49.1" }, { name = "tabulate", specifier = ">=0.9.0" }, { name = "weasyprint", specifier = ">=68.0" }, ] @@ -570,15 +516,6 @@ requires-dist = [ [package.metadata.requires-dev] dev = [{ name = "pytest", specifier = ">=9.0.2" }] -[[package]] -name = "idna" -version = "3.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, -] - [[package]] name = "iniconfig" version = "2.3.0" @@ -1178,108 +1115,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, ] -[[package]] -name = "pydantic" -version = "2.11.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, -] - -[[package]] -name = "pydantic-core" -version = "2.33.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/92/b31726561b5dae176c2d2c2dc43a9c5bfba5d32f96f8b4c0a600dd492447/pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8", size = 2028817, upload-time = "2025-04-23T18:30:43.919Z" }, - { url = "https://files.pythonhosted.org/packages/a3/44/3f0b95fafdaca04a483c4e685fe437c6891001bf3ce8b2fded82b9ea3aa1/pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d", size = 1861357, upload-time = "2025-04-23T18:30:46.372Z" }, - { url = "https://files.pythonhosted.org/packages/30/97/e8f13b55766234caae05372826e8e4b3b96e7b248be3157f53237682e43c/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d", size = 1898011, upload-time = "2025-04-23T18:30:47.591Z" }, - { url = "https://files.pythonhosted.org/packages/9b/a3/99c48cf7bafc991cc3ee66fd544c0aae8dc907b752f1dad2d79b1b5a471f/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572", size = 1982730, upload-time = "2025-04-23T18:30:49.328Z" }, - { url = "https://files.pythonhosted.org/packages/de/8e/a5b882ec4307010a840fb8b58bd9bf65d1840c92eae7534c7441709bf54b/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02", size = 2136178, upload-time = "2025-04-23T18:30:50.907Z" }, - { url = "https://files.pythonhosted.org/packages/e4/bb/71e35fc3ed05af6834e890edb75968e2802fe98778971ab5cba20a162315/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b", size = 2736462, upload-time = "2025-04-23T18:30:52.083Z" }, - { url = "https://files.pythonhosted.org/packages/31/0d/c8f7593e6bc7066289bbc366f2235701dcbebcd1ff0ef8e64f6f239fb47d/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2", size = 2005652, upload-time = "2025-04-23T18:30:53.389Z" }, - { url = "https://files.pythonhosted.org/packages/d2/7a/996d8bd75f3eda405e3dd219ff5ff0a283cd8e34add39d8ef9157e722867/pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a", size = 2113306, upload-time = "2025-04-23T18:30:54.661Z" }, - { url = "https://files.pythonhosted.org/packages/ff/84/daf2a6fb2db40ffda6578a7e8c5a6e9c8affb251a05c233ae37098118788/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac", size = 2073720, upload-time = "2025-04-23T18:30:56.11Z" }, - { url = "https://files.pythonhosted.org/packages/77/fb/2258da019f4825128445ae79456a5499c032b55849dbd5bed78c95ccf163/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a", size = 2244915, upload-time = "2025-04-23T18:30:57.501Z" }, - { url = "https://files.pythonhosted.org/packages/d8/7a/925ff73756031289468326e355b6fa8316960d0d65f8b5d6b3a3e7866de7/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b", size = 2241884, upload-time = "2025-04-23T18:30:58.867Z" }, - { url = "https://files.pythonhosted.org/packages/0b/b0/249ee6d2646f1cdadcb813805fe76265745c4010cf20a8eba7b0e639d9b2/pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22", size = 1910496, upload-time = "2025-04-23T18:31:00.078Z" }, - { url = "https://files.pythonhosted.org/packages/66/ff/172ba8f12a42d4b552917aa65d1f2328990d3ccfc01d5b7c943ec084299f/pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640", size = 1955019, upload-time = "2025-04-23T18:31:01.335Z" }, - { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584, upload-time = "2025-04-23T18:31:03.106Z" }, - { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071, upload-time = "2025-04-23T18:31:04.621Z" }, - { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823, upload-time = "2025-04-23T18:31:06.377Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792, upload-time = "2025-04-23T18:31:07.93Z" }, - { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338, upload-time = "2025-04-23T18:31:09.283Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998, upload-time = "2025-04-23T18:31:11.7Z" }, - { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200, upload-time = "2025-04-23T18:31:13.536Z" }, - { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890, upload-time = "2025-04-23T18:31:15.011Z" }, - { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359, upload-time = "2025-04-23T18:31:16.393Z" }, - { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883, upload-time = "2025-04-23T18:31:17.892Z" }, - { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074, upload-time = "2025-04-23T18:31:19.205Z" }, - { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538, upload-time = "2025-04-23T18:31:20.541Z" }, - { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909, upload-time = "2025-04-23T18:31:22.371Z" }, - { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786, upload-time = "2025-04-23T18:31:24.161Z" }, - { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, - { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, - { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, - { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, - { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, - { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, - { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, - { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, - { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, - { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, - { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, - { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, - { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, - { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, - { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, - { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, - { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, - { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, - { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, - { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, - { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, - { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, - { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, - { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, - { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, - { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, - { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, - { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, - { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, - { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, - { url = "https://files.pythonhosted.org/packages/30/68/373d55e58b7e83ce371691f6eaa7175e3a24b956c44628eb25d7da007917/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa", size = 2023982, upload-time = "2025-04-23T18:32:53.14Z" }, - { url = "https://files.pythonhosted.org/packages/a4/16/145f54ac08c96a63d8ed6442f9dec17b2773d19920b627b18d4f10a061ea/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29", size = 1858412, upload-time = "2025-04-23T18:32:55.52Z" }, - { url = "https://files.pythonhosted.org/packages/41/b1/c6dc6c3e2de4516c0bb2c46f6a373b91b5660312342a0cf5826e38ad82fa/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d", size = 1892749, upload-time = "2025-04-23T18:32:57.546Z" }, - { url = "https://files.pythonhosted.org/packages/12/73/8cd57e20afba760b21b742106f9dbdfa6697f1570b189c7457a1af4cd8a0/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e", size = 2067527, upload-time = "2025-04-23T18:32:59.771Z" }, - { url = "https://files.pythonhosted.org/packages/e3/d5/0bb5d988cc019b3cba4a78f2d4b3854427fc47ee8ec8e9eaabf787da239c/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c", size = 2108225, upload-time = "2025-04-23T18:33:04.51Z" }, - { url = "https://files.pythonhosted.org/packages/f1/c5/00c02d1571913d496aabf146106ad8239dc132485ee22efe08085084ff7c/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec", size = 2069490, upload-time = "2025-04-23T18:33:06.391Z" }, - { url = "https://files.pythonhosted.org/packages/22/a8/dccc38768274d3ed3a59b5d06f59ccb845778687652daa71df0cab4040d7/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052", size = 2237525, upload-time = "2025-04-23T18:33:08.44Z" }, - { url = "https://files.pythonhosted.org/packages/d4/e7/4f98c0b125dda7cf7ccd14ba936218397b44f50a56dd8c16a3091df116c3/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c", size = 2238446, upload-time = "2025-04-23T18:33:10.313Z" }, - { url = "https://files.pythonhosted.org/packages/ce/91/2ec36480fdb0b783cd9ef6795753c1dea13882f2e68e73bce76ae8c21e6a/pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808", size = 2066678, upload-time = "2025-04-23T18:33:12.224Z" }, - { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200, upload-time = "2025-04-23T18:33:14.199Z" }, - { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123, upload-time = "2025-04-23T18:33:16.555Z" }, - { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852, upload-time = "2025-04-23T18:33:18.513Z" }, - { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484, upload-time = "2025-04-23T18:33:20.475Z" }, - { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896, upload-time = "2025-04-23T18:33:22.501Z" }, - { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475, upload-time = "2025-04-23T18:33:24.528Z" }, - { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013, upload-time = "2025-04-23T18:33:26.621Z" }, - { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715, upload-time = "2025-04-23T18:33:28.656Z" }, - { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" }, -] - [[package]] name = "pydyf" version = "0.11.0" @@ -1603,28 +1438,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] -[[package]] -name = "sniffio" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, -] - -[[package]] -name = "starlette" -version = "0.50.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" }, -] - [[package]] name = "tabulate" version = "0.9.0" @@ -1730,18 +1543,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] -[[package]] -name = "typing-inspection" -version = "0.4.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, -] - [[package]] name = "tzdata" version = "2025.2"