From 491bd0dd7c1b0a8606a9145b3b9744c1fa6ccc5e Mon Sep 17 00:00:00 2001 From: novis10813 Date: Thu, 12 Feb 2026 19:13:11 +0800 Subject: [PATCH 01/25] feat: add example notebooks for common research workflows (issue #9) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 01_momentum_factor_research.ipynb: complete workflow (data → factor → IC → backtest) - 02_mean_reversion_factor.ipynb: mean reversion + cross-sectional processing - 03_data_loading_and_exploration.ipynb: AggBar + timebar deep dive - 04_multi_factor_combination.ipynb: multi-factor correlation, combo, selection - examples/README.md: index and getting started guide --- examples/01_momentum_factor_research.ipynb | 485 ++++++++++++++++ examples/02_mean_reversion_factor.ipynb | 394 +++++++++++++ .../03_data_loading_and_exploration.ipynb | 430 ++++++++++++++ examples/04_multi_factor_combination.ipynb | 549 ++++++++++++++++++ examples/README.md | 46 ++ 5 files changed, 1904 insertions(+) create mode 100644 examples/01_momentum_factor_research.ipynb create mode 100644 examples/02_mean_reversion_factor.ipynb create mode 100644 examples/03_data_loading_and_exploration.ipynb create mode 100644 examples/04_multi_factor_combination.ipynb create mode 100644 examples/README.md diff --git a/examples/01_momentum_factor_research.ipynb b/examples/01_momentum_factor_research.ipynb new file mode 100644 index 0000000..8a81281 --- /dev/null +++ b/examples/01_momentum_factor_research.ipynb @@ -0,0 +1,485 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Momentum Factor Research\n", + "\n", + "This notebook walks through a **complete factor research workflow** using Factorium:\n", + "\n", + "1. Load data from Binance\n", + "2. Explore the AggBar data structure\n", + "3. Build momentum factors (code-based & expression-based)\n", + "4. Visualize factor behavior\n", + "5. Run IC (Information Coefficient) analysis\n", + "6. Analyze quantile returns\n", + "7. Run a vectorized backtest\n", + "8. Generate a quick report\n", + "\n", + "**Prerequisites**: `pip install factorium` (or `uv add factorium`)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Setup & Data Loading\n", + "\n", + "We'll download 30 days of 1-minute futures data for 10 crypto symbols from Binance Vision." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from factorium import BinanceDataLoader, ResearchSession\n", + "from factorium.factors import FactorAnalyzer, CompositeFactor\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import pandas as pd\n", + "import numpy as np\n", + "\n", + "%matplotlib inline\n", + "plt.style.use(\"seaborn-v0_8-whitegrid\")\n", + "plt.rcParams[\"figure.figsize\"] = (14, 6)\n", + "plt.rcParams[\"figure.dpi\"] = 100" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "SYMBOLS = [\n", + " \"BTCUSDT\", \"ETHUSDT\", \"BNBUSDT\", \"SOLUSDT\", \"XRPUSDT\",\n", + " \"DOGEUSDT\", \"ADAUSDT\", \"AVAXUSDT\", \"DOTUSDT\", \"CAKEUSDT\",\n", + "]\n", + "\n", + "loader = BinanceDataLoader()\n", + "\n", + "agg = loader.load_aggbar(\n", + " symbols=SYMBOLS,\n", + " data_type=\"aggTrades\",\n", + " market_type=\"futures\",\n", + " futures_type=\"um\",\n", + " days=30,\n", + " bar_type=\"time\",\n", + " interval=60_000, # 1-minute bars\n", + ")\n", + "\n", + "print(f\"Loaded {len(agg):,} bars\")\n", + "print(f\"Symbols: {agg.symbols}\")\n", + "print(f\"Columns: {agg.cols}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Exploring the AggBar\n", + "\n", + "`AggBar` is a **multi-symbol OHLCV container** stored in long format. You can inspect it, convert to Polars/Pandas, and slice by time or symbols." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Summary info per symbol\n", + "agg.info()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# View raw data as Polars DataFrame\n", + "agg.to_polars().head(10)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Slice by symbols — creates a new AggBar\n", + "btc_eth = agg.slice(symbols=[\"BTCUSDT\", \"ETHUSDT\"])\n", + "print(f\"Sliced AggBar: {btc_eth.symbols}, {len(btc_eth):,} bars\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Building Momentum Factors\n", + "\n", + "### 3.1 Code-Based Factor Construction\n", + "\n", + "Extract a column from `AggBar` with `agg[\"close\"]` to get a `Factor` object, then chain time-series and cross-sectional operators." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "close = agg[\"close\"]\n", + "volume = agg[\"volume\"]\n", + "\n", + "# Simple momentum: percentage change over 60 periods\n", + "momentum_60 = close.ts_delta(60) / close.ts_shift(60)\n", + "momentum_60.name = \"momentum_60\"\n", + "\n", + "# Short-term momentum (20 periods)\n", + "momentum_20 = close.ts_delta(20) / close.ts_shift(20)\n", + "momentum_20.name = \"momentum_20\"\n", + "\n", + "# Volatility-adjusted momentum\n", + "volatility = (close.ts_delta(1) / close.ts_shift(1)).ts_std(60)\n", + "vol_adj_momentum = momentum_60 / volatility\n", + "vol_adj_momentum.name = \"vol_adj_momentum\"\n", + "\n", + "# Cross-sectional rank (normalized to [0, 1])\n", + "momentum_rank = momentum_60.cs_rank()\n", + "momentum_rank.name = \"momentum_rank\"\n", + "\n", + "print(\"Factor shapes:\")\n", + "print(f\" momentum_60: {len(momentum_60):,} rows\")\n", + "print(f\" vol_adj_momentum: {len(vol_adj_momentum):,} rows\")\n", + "print(f\" momentum_rank: {len(momentum_rank):,} rows\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 3.2 Expression-Based Factor Construction\n", + "\n", + "You can also define factors using string expressions via `ResearchSession.create_factor()`. This is useful for rapid experimentation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "session = ResearchSession(agg, default_frequency=\"1min\")\n", + "\n", + "# Same momentum factor, defined via expression\n", + "mom_expr = session.create_factor(\n", + " \"ts_delta(close, 60) / ts_shift(close, 60)\",\n", + " name=\"momentum_60_expr\",\n", + ")\n", + "\n", + "# A more complex expression: MA crossover signal\n", + "ma_cross = session.create_factor(\n", + " \"ts_mean(close, 10) - ts_mean(close, 30)\",\n", + " name=\"ma_crossover\",\n", + ")\n", + "\n", + "print(f\"Expression-based momentum: {len(mom_expr):,} rows\")\n", + "print(f\"MA crossover signal: {len(ma_cross):,} rows\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Visualizing Factors\n", + "\n", + "Use the `.plot` accessor on Factor objects to produce time series, heatmaps, and distributions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Time series of momentum for a few symbols\n", + "momentum_60.plot.plot_timeseries(symbols=[\"BTCUSDT\", \"ETHUSDT\", \"SOLUSDT\"])\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Distribution of momentum values across all symbols\n", + "momentum_60.plot.plot_distribution()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Heatmap: momentum intensity across symbols and time\n", + "momentum_rank.plot.plot_heatmap()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. IC (Information Coefficient) Analysis\n", + "\n", + "The **IC** measures the rank correlation between factor values and subsequent returns. A consistently positive (or negative) IC indicates predictive power.\n", + "\n", + "### 5.1 Single-Period IC" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Use ResearchSession for streamlined analysis\n", + "signal = momentum_rank # Use ranked momentum as our signal\n", + "\n", + "analysis = session.analyze(signal, periods=1)\n", + "\n", + "print(\"IC Summary:\")\n", + "print(analysis.ic_summary)\n", + "print()\n", + "print(f\"IC Series shape: {analysis.ic_series.shape}\")\n", + "print(analysis.ic_series.head())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 5.2 Multi-Horizon IC Analysis\n", + "\n", + "To understand how quickly a factor's signal decays, we compute IC across multiple forward horizons." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Compute IC for multiple horizons\n", + "horizons = [1, 5, 10, 20, 60]\n", + "ic_results = {}\n", + "\n", + "for h in horizons:\n", + " result = session.analyze(signal, periods=h)\n", + " ic_results[h] = result.ic_summary.get(h, {})\n", + "\n", + "# Display IC decay table\n", + "ic_decay_df = pd.DataFrame(ic_results).T\n", + "ic_decay_df.index.name = \"horizon\"\n", + "print(\"IC Decay Analysis:\")\n", + "ic_decay_df" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Plot IC decay curve\n", + "fig, ax = plt.subplots(figsize=(10, 5))\n", + "ax.plot(ic_decay_df.index, ic_decay_df[\"mean_ic\"], \"o-\", linewidth=2, markersize=8)\n", + "ax.axhline(y=0, color=\"gray\", linestyle=\"--\", alpha=0.5)\n", + "ax.set_xlabel(\"Forward Horizon (periods)\")\n", + "ax.set_ylabel(\"Mean IC\")\n", + "ax.set_title(\"IC Decay Curve — Momentum Factor\")\n", + "ax.grid(True, alpha=0.3)\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 6. Quantile Analysis\n", + "\n", + "Split the cross-section into quantiles by factor value and examine the mean returns of each group. A monotonic pattern (Q1 < Q2 < ... < Q5) indicates strong linear predictive power." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Use FactorAnalyzer directly for more control\n", + "analyzer = FactorAnalyzer(signal, agg, quantiles=5)\n", + "\n", + "# Quantile returns\n", + "analyzer.plot_quantile_returns(quantiles=5, period=1)\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Cumulative returns by quantile (includes Long-Short portfolio)\n", + "analyzer.plot_cumulative_returns(quantiles=5, period=1, long_short=True)\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# IC time series plot\n", + "analyzer.plot_ic(period=1, method=\"rank\", plot_type=\"ts\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 7. Backtesting\n", + "\n", + "Run a **market-neutral vectorized backtest** using the momentum signal. The backtester:\n", + "- Converts signals to portfolio weights (cross-sectional normalization)\n", + "- Handles transaction costs\n", + "- Tracks equity, returns, and positions over time" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Market-neutral backtest\n", + "result = session.backtest(\n", + " signal,\n", + " neutralization=\"market\",\n", + " transaction_cost=0.0003,\n", + ")\n", + "\n", + "# Key performance metrics\n", + "print(\"Backtest Metrics:\")\n", + "for key, val in result.metrics.items():\n", + " if isinstance(val, float):\n", + " print(f\" {key:25s}: {val:>10.4f}\")\n", + " else:\n", + " print(f\" {key:25s}: {val}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Plot equity curve\n", + "equity = result.equity_curve.to_pandas()\n", + "equity[\"timestamp\"] = pd.to_datetime(equity[\"start_time\"], unit=\"ms\")\n", + "\n", + "fig, ax = plt.subplots(figsize=(14, 6))\n", + "ax.plot(equity[\"timestamp\"], equity[\"equity\"], linewidth=1.5)\n", + "ax.set_xlabel(\"Date\")\n", + "ax.set_ylabel(\"Portfolio Equity\")\n", + "ax.set_title(\"Momentum Factor — Equity Curve (Market Neutral)\")\n", + "ax.grid(True, alpha=0.3)\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Plot returns distribution\n", + "returns = result.returns.to_pandas()\n", + "\n", + "fig, ax = plt.subplots(figsize=(10, 5))\n", + "ax.hist(returns[\"portfolio_return\"].dropna(), bins=100, alpha=0.75, edgecolor=\"black\", linewidth=0.5)\n", + "ax.axvline(x=0, color=\"red\", linestyle=\"--\", alpha=0.7)\n", + "ax.set_xlabel(\"Return\")\n", + "ax.set_ylabel(\"Frequency\")\n", + "ax.set_title(\"Distribution of Portfolio Returns\")\n", + "ax.grid(True, alpha=0.3)\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 8. Quick Report\n", + "\n", + "`ResearchSession.quick_report()` combines IC analysis and backtesting into a single text summary — great for rapid iteration." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "report = session.quick_report(signal)\n", + "print(report)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 9. Summary & Next Steps\n", + "\n", + "In this notebook we:\n", + "- Loaded multi-symbol 1-minute data from Binance using `BinanceDataLoader`\n", + "- Built momentum factors via both **code-based** and **expression-based** methods\n", + "- Visualized factor behavior with time series, distributions, and heatmaps\n", + "- Computed IC and analyzed its decay across multiple horizons\n", + "- Ran a market-neutral backtest and examined performance metrics\n", + "\n", + "**Next notebooks to explore:**\n", + "- `02_mean_reversion_factor.ipynb` — Mean reversion with volatility normalization\n", + "- `03_data_loading_and_exploration.ipynb` — Deep dive into AggBar and data loading\n", + "- `04_multi_factor_combination.ipynb` — Combine multiple factors and compare performance" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.13.2" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/examples/02_mean_reversion_factor.ipynb b/examples/02_mean_reversion_factor.ipynb new file mode 100644 index 0000000..4a9a67b --- /dev/null +++ b/examples/02_mean_reversion_factor.ipynb @@ -0,0 +1,394 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Mean Reversion Factor Research\n", + "\n", + "This notebook demonstrates how to build and evaluate **mean reversion** factors, including:\n", + "\n", + "1. Z-score distance from moving average\n", + "2. Volatility normalization\n", + "3. Cross-sectional processing (`cs_rank`, `cs_zscore`, `cs_winsorize`, `cs_demean`)\n", + "4. Comparing raw vs. processed signals\n", + "5. Market-neutral vs. un-neutralized backtesting\n", + "6. Advanced statistical operators (`ts_autocorr`, `ts_kurtosis`, `ts_skewness`)\n", + "\n", + "**Prerequisites**: `pip install factorium`" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Setup & Data Loading" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from factorium import BinanceDataLoader, ResearchSession\n", + "from factorium.factors import FactorAnalyzer\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import pandas as pd\n", + "import numpy as np\n", + "\n", + "%matplotlib inline\n", + "plt.style.use(\"seaborn-v0_8-whitegrid\")\n", + "plt.rcParams[\"figure.figsize\"] = (14, 6)\n", + "plt.rcParams[\"figure.dpi\"] = 100" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "SYMBOLS = [\n", + " \"BTCUSDT\", \"ETHUSDT\", \"BNBUSDT\", \"SOLUSDT\", \"XRPUSDT\",\n", + " \"DOGEUSDT\", \"ADAUSDT\", \"AVAXUSDT\", \"DOTUSDT\", \"CAKEUSDT\",\n", + "]\n", + "\n", + "loader = BinanceDataLoader()\n", + "\n", + "agg = loader.load_aggbar(\n", + " symbols=SYMBOLS,\n", + " data_type=\"aggTrades\",\n", + " market_type=\"futures\",\n", + " futures_type=\"um\",\n", + " days=30,\n", + " bar_type=\"time\",\n", + " interval=60_000,\n", + ")\n", + "\n", + "print(f\"Loaded {len(agg):,} bars for {len(agg.symbols)} symbols\")\n", + "\n", + "session = ResearchSession(agg, default_frequency=\"1min\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Building Mean Reversion Factors\n", + "\n", + "The core idea: prices that deviate far from their moving average tend to **revert back**. We measure this deviation as a z-score.\n", + "\n", + "$$\\text{MeanRev}(t) = \\frac{P(t) - \\text{MA}(t, w)}{\\sigma(t, w)}$$" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "close = agg[\"close\"]\n", + "volume = agg[\"volume\"]\n", + "\n", + "# --- Mean Reversion Factor (z-score from 60-period MA) ---\n", + "ma_60 = close.ts_mean(60)\n", + "std_60 = close.ts_std(60)\n", + "mean_rev_raw = (close - ma_60) / std_60\n", + "mean_rev_raw.name = \"mean_rev_raw\"\n", + "\n", + "# For mean reversion, we go OPPOSITE to the deviation:\n", + "# high z-score → price is above MA → expect reversion down → SHORT\n", + "# So we negate the signal\n", + "mean_rev = -mean_rev_raw\n", + "mean_rev.name = \"mean_rev\"\n", + "\n", + "print(f\"Raw mean reversion factor: {len(mean_rev):,} rows\")\n", + "mean_rev.to_pandas().head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2.1 Volatility-Normalized Mean Reversion\n", + "\n", + "Normalizing by realized volatility ensures the signal is comparable across assets with different volatility levels." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Compute returns and rolling volatility\n", + "returns = close.ts_delta(1) / close.ts_shift(1)\n", + "realized_vol = returns.ts_std(60)\n", + "\n", + "# Volatility-normalized mean reversion\n", + "mean_rev_vol_norm = mean_rev / realized_vol\n", + "mean_rev_vol_norm.name = \"mean_rev_vol_norm\"\n", + "\n", + "print(f\"Vol-normalized mean reversion: {len(mean_rev_vol_norm):,} rows\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Cross-Sectional Processing\n", + "\n", + "Before using a factor as a trading signal, we typically apply **cross-sectional transformations** to normalize across assets at each point in time." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Cross-sectional rank: maps to [0, 1] across symbols at each timestamp\n", + "signal_ranked = mean_rev.cs_rank()\n", + "signal_ranked.name = \"mean_rev_ranked\"\n", + "\n", + "# Cross-sectional z-score: standardize across symbols\n", + "signal_zscore = mean_rev.cs_zscore()\n", + "signal_zscore.name = \"mean_rev_zscore\"\n", + "\n", + "# Winsorize: clip extreme values (2.5th and 97.5th percentiles)\n", + "signal_winsorized = mean_rev.cs_winsorize(limits=0.025)\n", + "signal_winsorized.name = \"mean_rev_winsorized\"\n", + "\n", + "# Demean: remove cross-sectional mean\n", + "signal_demeaned = mean_rev.cs_demean()\n", + "signal_demeaned.name = \"mean_rev_demeaned\"\n", + "\n", + "print(\"Cross-sectional transformations applied:\")\n", + "print(f\" Ranked: [{signal_ranked.data['factor'].min():.3f}, {signal_ranked.data['factor'].max():.3f}]\")\n", + "print(f\" Z-scored: mean={signal_zscore.data['factor'].mean():.6f}\")\n", + "print(f\" Demeaned: mean={signal_demeaned.data['factor'].mean():.6f}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Comparing Raw vs. Processed Signals\n", + "\n", + "Let's see how cross-sectional processing affects the factor's predictive power (IC)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "factors_to_compare = {\n", + " \"Raw Mean Rev\": mean_rev,\n", + " \"Vol-Normalized\": mean_rev_vol_norm,\n", + " \"CS Ranked\": signal_ranked,\n", + " \"CS Z-Score\": signal_zscore,\n", + " \"CS Winsorized\": signal_winsorized,\n", + "}\n", + "\n", + "comparison_results = []\n", + "for name, factor in factors_to_compare.items():\n", + " analysis = session.analyze(factor, periods=1)\n", + " ic_stats = analysis.ic_summary.get(1, {})\n", + " comparison_results.append({\n", + " \"Factor\": name,\n", + " \"Mean IC\": ic_stats.get(\"mean_ic\", np.nan),\n", + " \"IC Std\": ic_stats.get(\"ic_std\", np.nan),\n", + " \"IC IR\": ic_stats.get(\"ic_ir\", np.nan),\n", + " })\n", + "\n", + "comparison_df = pd.DataFrame(comparison_results).set_index(\"Factor\")\n", + "print(\"IC Comparison — Raw vs. Processed Signals:\")\n", + "comparison_df" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Visualize IC comparison\n", + "fig, axes = plt.subplots(1, 2, figsize=(14, 5))\n", + "\n", + "comparison_df[\"Mean IC\"].plot.barh(ax=axes[0], color=\"steelblue\")\n", + "axes[0].set_title(\"Mean IC\")\n", + "axes[0].axvline(x=0, color=\"gray\", linestyle=\"--\")\n", + "\n", + "comparison_df[\"IC IR\"].plot.barh(ax=axes[1], color=\"coral\")\n", + "axes[1].set_title(\"IC IR (Information Ratio)\")\n", + "axes[1].axvline(x=0, color=\"gray\", linestyle=\"--\")\n", + "\n", + "plt.suptitle(\"Mean Reversion Factor — Signal Processing Comparison\", fontsize=14)\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. Backtest Comparison: Market-Neutral vs. Un-Neutralized\n", + "\n", + "Compare how the factor performs under different portfolio construction approaches." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Use the best-performing signal variant for backtesting\n", + "best_signal = signal_ranked\n", + "\n", + "# Market-neutral backtest\n", + "result_neutral = session.backtest(best_signal, neutralization=\"market\")\n", + "\n", + "# Un-neutralized (long-only) backtest\n", + "result_long = session.backtest(best_signal, neutralization=\"none\")\n", + "\n", + "# Compare metrics\n", + "metrics_comparison = pd.DataFrame({\n", + " \"Market Neutral\": result_neutral.metrics,\n", + " \"Long Only\": result_long.metrics,\n", + "}).T\n", + "\n", + "key_metrics = [\"total_return\", \"annual_return\", \"sharpe_ratio\", \"max_drawdown\", \"sortino_ratio\"]\n", + "available_metrics = [m for m in key_metrics if m in metrics_comparison.columns]\n", + "metrics_comparison[available_metrics]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Plot equity curves side-by-side\n", + "fig, ax = plt.subplots(figsize=(14, 6))\n", + "\n", + "eq_neutral = result_neutral.equity_curve.to_pandas()\n", + "eq_neutral[\"ts\"] = pd.to_datetime(eq_neutral[\"start_time\"], unit=\"ms\")\n", + "ax.plot(eq_neutral[\"ts\"], eq_neutral[\"equity\"], label=\"Market Neutral\", linewidth=1.5)\n", + "\n", + "eq_long = result_long.equity_curve.to_pandas()\n", + "eq_long[\"ts\"] = pd.to_datetime(eq_long[\"start_time\"], unit=\"ms\")\n", + "ax.plot(eq_long[\"ts\"], eq_long[\"equity\"], label=\"Long Only\", linewidth=1.5)\n", + "\n", + "ax.set_xlabel(\"Date\")\n", + "ax.set_ylabel(\"Equity\")\n", + "ax.set_title(\"Mean Reversion — Equity Curves\")\n", + "ax.legend()\n", + "ax.grid(True, alpha=0.3)\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 6. Advanced Statistical Operators\n", + "\n", + "Factorium provides advanced time-series operators that are useful for characterizing market microstructure and regime analysis." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "returns = close.ts_delta(1) / close.ts_shift(1)\n", + "\n", + "# Autocorrelation: measures serial dependence in returns\n", + "# Positive autocorrelation → trending, Negative → mean reverting\n", + "autocorr = returns.ts_autocorr(60, lag=1)\n", + "autocorr.name = \"return_autocorr\"\n", + "\n", + "# Kurtosis: measures tail heaviness\n", + "# High kurtosis → more extreme events (fat tails)\n", + "kurtosis = returns.ts_kurtosis(60)\n", + "kurtosis.name = \"return_kurtosis\"\n", + "\n", + "# Skewness: measures asymmetry of the return distribution\n", + "# Negative skew → more downside risk\n", + "skewness = returns.ts_skewness(60)\n", + "skewness.name = \"return_skewness\"\n", + "\n", + "print(\"Advanced statistical factors computed:\")\n", + "for f in [autocorr, kurtosis, skewness]:\n", + " print(f\" {f.name}: {len(f):,} rows\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Visualize autocorrelation for a few symbols\n", + "autocorr.plot.plot_timeseries(symbols=[\"BTCUSDT\", \"ETHUSDT\", \"SOLUSDT\"])\n", + "plt.axhline(y=0, color=\"red\", linestyle=\"--\", alpha=0.5)\n", + "plt.title(\"Rolling Return Autocorrelation (60-period window, lag=1)\")\n", + "plt.ylabel(\"Autocorrelation\")\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Quick IC check on autocorrelation as a factor\n", + "# (negative autocorrelation → mean reverting → potential signal)\n", + "autocorr_signal = (-autocorr).cs_rank()\n", + "autocorr_signal.name = \"neg_autocorr_rank\"\n", + "\n", + "autocorr_analysis = session.analyze(autocorr_signal, periods=1)\n", + "print(\"Autocorrelation as factor:\")\n", + "print(autocorr_analysis.ic_summary)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 7. Summary\n", + "\n", + "In this notebook we:\n", + "- Built a **mean reversion factor** using z-score distance from a moving average\n", + "- Applied **volatility normalization** to make the signal stable across assets\n", + "- Demonstrated **cross-sectional processing** methods (`cs_rank`, `cs_zscore`, `cs_winsorize`, `cs_demean`)\n", + "- Compared IC across different signal processing methods\n", + "- Ran backtests under **market-neutral** and **long-only** regimes\n", + "- Explored **advanced statistical operators** for regime characterization\n", + "\n", + "**Key takeaway:** Cross-sectional normalization often improves factor stability (IC IR), even if it doesn't always increase mean IC." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} \ No newline at end of file diff --git a/examples/03_data_loading_and_exploration.ipynb b/examples/03_data_loading_and_exploration.ipynb new file mode 100644 index 0000000..23d48e0 --- /dev/null +++ b/examples/03_data_loading_and_exploration.ipynb @@ -0,0 +1,430 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Data Loading & AggBar Exploration\n", + "\n", + "This notebook is a **deep dive into data handling** in Factorium:\n", + "\n", + "1. Downloading data with `BinanceDataLoader`\n", + "2. Understanding the `AggBar` container\n", + "3. Time-bar aggregation at different intervals\n", + "4. Slicing, filtering, and exporting data\n", + "5. Working with different data formats (Polars / Pandas / CSV / Parquet)\n", + "\n", + "**Prerequisites**: `pip install factorium`" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Downloading Data with BinanceDataLoader\n", + "\n", + "`BinanceDataLoader` fetches historical trade data from [Binance Vision](https://data.binance.vision/) and aggregates it into OHLCV bars on the fly." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from factorium import BinanceDataLoader, AggBar\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import pandas as pd\n", + "import polars as pl\n", + "\n", + "%matplotlib inline\n", + "plt.style.use(\"seaborn-v0_8-whitegrid\")\n", + "plt.rcParams[\"figure.figsize\"] = (14, 6)\n", + "plt.rcParams[\"figure.dpi\"] = 100" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "loader = BinanceDataLoader()\n", + "\n", + "# Load 7 days of 1-minute time bars for 5 symbols\n", + "agg = loader.load_aggbar(\n", + " symbols=[\"BTCUSDT\", \"ETHUSDT\", \"BNBUSDT\", \"SOLUSDT\", \"XRPUSDT\"],\n", + " data_type=\"aggTrades\",\n", + " market_type=\"futures\",\n", + " futures_type=\"um\",\n", + " days=7,\n", + " bar_type=\"time\", # Time-based bars\n", + " interval=60_000, # 1 minute = 60,000 ms\n", + ")\n", + "\n", + "print(f\"Type: {type(agg).__name__}\")\n", + "print(f\"Total bars: {len(agg):,}\")\n", + "print(f\"Symbols: {agg.symbols}\")\n", + "print(f\"Columns: {agg.cols}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Understanding AggBar\n", + "\n", + "`AggBar` is a **multi-symbol OHLCV data container** that stores data in **long format**:\n", + "\n", + "| Column | Description |\n", + "|--------|-------------|\n", + "| `start_time` | Bar open timestamp (epoch ms) |\n", + "| `end_time` | Bar close timestamp (epoch ms) |\n", + "| `symbol` | Trading pair identifier |\n", + "| `open` | Opening price |\n", + "| `high` | Highest price |\n", + "| `low` | Lowest price |\n", + "| `close` | Closing price |\n", + "| `volume` | Trading volume |" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# View as Polars DataFrame\n", + "agg.to_polars().head(10)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# View as Pandas DataFrame\n", + "agg.to_df().head(10)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Per-symbol summary: bar count, time range, missing data\n", + "agg.info()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Metadata\n", + "meta = agg.metadata\n", + "print(f\"Number of rows: {meta.num_rows:,}\")\n", + "print(f\"Number of symbols: {meta.num_symbols}\")\n", + "print(f\"Time range: {meta.min_time} → {meta.max_time}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Extracting Factors from AggBar\n", + "\n", + "Use `agg[\"column_name\"]` to extract a column as a `Factor` object. This is the starting point for all computations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "close = agg[\"close\"]\n", + "volume = agg[\"volume\"]\n", + "\n", + "print(f\"close factor: {type(close).__name__}, name='{close.name}', rows={len(close):,}\")\n", + "print(f\"volume factor: {type(volume).__name__}, name='{volume.name}', rows={len(volume):,}\")\n", + "\n", + "# Preview factor data\n", + "close.data.head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Extract multiple columns as a new AggBar\n", + "ohlc = agg[[\"open\", \"high\", \"low\", \"close\"]]\n", + "print(f\"Sub-AggBar columns: {ohlc.cols}\")\n", + "print(f\"Sub-AggBar bars: {len(ohlc):,}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Slicing Data\n", + "\n", + "`AggBar.slice()` lets you filter by **time range** and/or **symbols** to create a new AggBar subset." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Slice by symbols only\n", + "btc_only = agg.slice(symbols=[\"BTCUSDT\"])\n", + "print(f\"BTC only: {len(btc_only):,} bars, symbols={btc_only.symbols}\")\n", + "\n", + "# Slice by multiple symbols\n", + "btc_eth = agg.slice(symbols=[\"BTCUSDT\", \"ETHUSDT\"])\n", + "print(f\"BTC+ETH: {len(btc_eth):,} bars, symbols={btc_eth.symbols}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. Different Time Intervals\n", + "\n", + "By changing the `interval` parameter, you can aggregate raw trade data into bars of different durations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# 1-minute bars (already loaded)\n", + "print(f\"1-min bars: {len(agg):,}\")\n", + "\n", + "# 5-minute bars\n", + "agg_5m = loader.load_aggbar(\n", + " symbols=[\"BTCUSDT\", \"ETHUSDT\"],\n", + " data_type=\"aggTrades\",\n", + " market_type=\"futures\",\n", + " futures_type=\"um\",\n", + " days=7,\n", + " bar_type=\"time\",\n", + " interval=300_000, # 5 minutes = 300,000 ms\n", + ")\n", + "print(f\"5-min bars: {len(agg_5m):,}\")\n", + "\n", + "# 1-hour bars\n", + "agg_1h = loader.load_aggbar(\n", + " symbols=[\"BTCUSDT\", \"ETHUSDT\"],\n", + " data_type=\"aggTrades\",\n", + " market_type=\"futures\",\n", + " futures_type=\"um\",\n", + " days=7,\n", + " bar_type=\"time\",\n", + " interval=3_600_000, # 1 hour = 3,600,000 ms\n", + ")\n", + "print(f\"1-hour bars: {len(agg_1h):,}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Compare bar counts\n", + "comparison = pd.DataFrame({\n", + " \"Interval\": [\"1 min\", \"5 min\", \"1 hour\"],\n", + " \"interval_ms\": [60_000, 300_000, 3_600_000],\n", + " \"Total Bars\": [len(agg.slice(symbols=[\"BTCUSDT\", \"ETHUSDT\"])), len(agg_5m), len(agg_1h)],\n", + "})\n", + "comparison" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Visualize the same price data at different resolutions\n", + "fig, axes = plt.subplots(3, 1, figsize=(14, 12), sharex=False)\n", + "\n", + "for ax, (label, data) in zip(axes, [\n", + " (\"1-min\", agg.slice(symbols=[\"BTCUSDT\"])),\n", + " (\"5-min\", agg_5m.slice(symbols=[\"BTCUSDT\"])),\n", + " (\"1-hour\", agg_1h.slice(symbols=[\"BTCUSDT\"])),\n", + "]):\n", + " df = data.to_df()\n", + " df[\"datetime\"] = pd.to_datetime(df[\"start_time\"], unit=\"ms\")\n", + " ax.plot(df[\"datetime\"], df[\"close\"], linewidth=0.8)\n", + " ax.set_title(f\"BTCUSDT Close Price — {label} bars ({len(data):,} bars)\")\n", + " ax.set_ylabel(\"Price\")\n", + " ax.grid(True, alpha=0.3)\n", + "\n", + "axes[-1].set_xlabel(\"Date\")\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 6. Saving and Loading Data\n", + "\n", + "`AggBar` can be exported to CSV or Parquet and loaded back." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "\n", + "# Save to Parquet (efficient binary format)\n", + "output_dir = Path(\"./sample_data\")\n", + "output_dir.mkdir(exist_ok=True)\n", + "\n", + "agg.to_parquet(output_dir / \"crypto_1min.parquet\")\n", + "print(f\"Saved to {output_dir / 'crypto_1min.parquet'}\")\n", + "\n", + "# Save to CSV\n", + "agg.to_csv(output_dir / \"crypto_1min.csv\")\n", + "print(f\"Saved to {output_dir / 'crypto_1min.csv'}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Load back from CSV\n", + "agg_loaded = AggBar.from_csv(output_dir / \"crypto_1min.csv\")\n", + "print(f\"Loaded from CSV: {len(agg_loaded):,} bars, symbols={agg_loaded.symbols}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# You can also create AggBar from a Polars or Pandas DataFrame\n", + "df = agg.to_polars()\n", + "agg_from_df = AggBar.from_df(df)\n", + "print(f\"From DataFrame: {len(agg_from_df):,} bars\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 7. Using ResearchSession for Quick Analysis\n", + "\n", + "`ResearchSession` wraps an AggBar and provides a high-level API. It can also be created directly from files." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from factorium import ResearchSession\n", + "\n", + "# Create from AggBar\n", + "session = ResearchSession(agg, default_frequency=\"1min\")\n", + "\n", + "print(f\"Session symbols: {session.symbols}\")\n", + "print(f\"Session columns: {session.cols}\")\n", + "\n", + "# Quick factor creation and analysis\n", + "signal = session.factor(\"close\").cs_rank()\n", + "report = session.quick_report(signal)\n", + "print(report)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# ResearchSession can also load from files directly\n", + "session_from_csv = ResearchSession.from_csv(\n", + " output_dir / \"crypto_1min.csv\",\n", + " default_frequency=\"1min\",\n", + ")\n", + "print(f\"Session from CSV: {len(session_from_csv.symbols)} symbols\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 8. Cleanup\n", + "\n", + "Remove sample data files created during this notebook." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import shutil\n", + "\n", + "if output_dir.exists():\n", + " shutil.rmtree(output_dir)\n", + " print(f\"Cleaned up {output_dir}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "In this notebook we covered:\n", + "- **`BinanceDataLoader`**: Download historical trade data and aggregate into time bars\n", + "- **`AggBar`**: Multi-symbol OHLCV container with `info()`, `slice()`, `to_polars()`, `to_df()`\n", + "- **Time intervals**: Aggregating the same data into 1-min, 5-min, and 1-hour bars\n", + "- **Persistence**: Saving to CSV/Parquet and loading back\n", + "- **`ResearchSession`**: High-level API that wraps AggBar for streamlined research\n", + "\n", + "**Next notebooks:**\n", + "- `01_momentum_factor_research.ipynb` — Full factor research workflow\n", + "- `02_mean_reversion_factor.ipynb` — Mean reversion with volatility normalization\n", + "- `04_multi_factor_combination.ipynb` — Combine multiple factors" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} \ No newline at end of file diff --git a/examples/04_multi_factor_combination.ipynb b/examples/04_multi_factor_combination.ipynb new file mode 100644 index 0000000..90e9324 --- /dev/null +++ b/examples/04_multi_factor_combination.ipynb @@ -0,0 +1,549 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Multi-Factor Combination\n", + "\n", + "This notebook demonstrates how to **combine multiple factors** into a single trading signal:\n", + "\n", + "1. Build individual factors (momentum, volatility, volume)\n", + "2. Analyze factor correlations\n", + "3. Orthogonalize factors to remove redundancy\n", + "4. Combine factors using `CompositeFactor`\n", + "5. Compare single-factor vs. composite-factor performance\n", + "6. Factor selection workflow\n", + "\n", + "**Prerequisites**: `pip install factorium`" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Setup & Data Loading" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from factorium import BinanceDataLoader, ResearchSession\n", + "from factorium.factors import FactorAnalyzer, CompositeFactor\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import pandas as pd\n", + "import numpy as np\n", + "import seaborn as sns\n", + "\n", + "%matplotlib inline\n", + "plt.style.use(\"seaborn-v0_8-whitegrid\")\n", + "plt.rcParams[\"figure.figsize\"] = (14, 6)\n", + "plt.rcParams[\"figure.dpi\"] = 100" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "SYMBOLS = [\n", + " \"BTCUSDT\", \"ETHUSDT\", \"BNBUSDT\", \"SOLUSDT\", \"XRPUSDT\",\n", + " \"DOGEUSDT\", \"ADAUSDT\", \"AVAXUSDT\", \"DOTUSDT\", \"CAKEUSDT\",\n", + "]\n", + "\n", + "loader = BinanceDataLoader()\n", + "\n", + "agg = loader.load_aggbar(\n", + " symbols=SYMBOLS,\n", + " data_type=\"aggTrades\",\n", + " market_type=\"futures\",\n", + " futures_type=\"um\",\n", + " days=30,\n", + " bar_type=\"time\",\n", + " interval=60_000,\n", + ")\n", + "\n", + "session = ResearchSession(agg, default_frequency=\"1min\")\n", + "print(f\"Loaded {len(agg):,} bars for {len(agg.symbols)} symbols\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Building Individual Factors\n", + "\n", + "We'll create three conceptually different factors to combine." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "close = agg[\"close\"]\n", + "volume = agg[\"volume\"]\n", + "\n", + "# --- Factor 1: Momentum (60-period return, ranked) ---\n", + "momentum = (close.ts_delta(60) / close.ts_shift(60)).cs_rank()\n", + "momentum.name = \"momentum\"\n", + "\n", + "# --- Factor 2: Volatility (inverse of rolling std, ranked) ---\n", + "# Low volatility tends to outperform (\"low vol anomaly\")\n", + "returns = close.ts_delta(1) / close.ts_shift(1)\n", + "inv_vol = -returns.ts_std(60) # negate so lower vol → higher signal\n", + "inv_vol_ranked = inv_vol.cs_rank()\n", + "inv_vol_ranked.name = \"inv_volatility\"\n", + "\n", + "# --- Factor 3: Volume Ratio (relative volume, ranked) ---\n", + "# High relative volume can indicate conviction\n", + "vol_ratio = volume / volume.ts_mean(60)\n", + "vol_ratio_ranked = vol_ratio.cs_rank()\n", + "vol_ratio_ranked.name = \"volume_ratio\"\n", + "\n", + "factors = {\n", + " \"Momentum\": momentum,\n", + " \"Inv Volatility\": inv_vol_ranked,\n", + " \"Volume Ratio\": vol_ratio_ranked,\n", + "}\n", + "\n", + "print(\"Individual factors created:\")\n", + "for name, f in factors.items():\n", + " print(f\" {name}: {len(f):,} rows\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2.1 Individual Factor IC Analysis" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Compute IC for each factor\n", + "ic_table = []\n", + "for name, factor in factors.items():\n", + " analysis = session.analyze(factor, periods=1)\n", + " ic_stats = analysis.ic_summary.get(1, {})\n", + " ic_table.append({\n", + " \"Factor\": name,\n", + " \"Mean IC\": ic_stats.get(\"mean_ic\", np.nan),\n", + " \"IC Std\": ic_stats.get(\"ic_std\", np.nan),\n", + " \"IC IR\": ic_stats.get(\"ic_ir\", np.nan),\n", + " })\n", + "\n", + "ic_df = pd.DataFrame(ic_table).set_index(\"Factor\")\n", + "print(\"Individual Factor IC Analysis:\")\n", + "ic_df" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Factor Correlation Analysis\n", + "\n", + "Before combining factors, we need to understand how correlated they are. Highly correlated factors add little diversification benefit." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Build a merged DataFrame with all factor values aligned by time and symbol\n", + "factor_dfs = []\n", + "for name, factor in factors.items():\n", + " df = factor.to_pandas()[[\"start_time\", \"symbol\", \"factor\"]].rename(\n", + " columns={\"factor\": name}\n", + " )\n", + " factor_dfs.append(df)\n", + "\n", + "# Merge all factors on (start_time, symbol)\n", + "merged = factor_dfs[0]\n", + "for df in factor_dfs[1:]:\n", + " merged = merged.merge(df, on=[\"start_time\", \"symbol\"], how=\"inner\")\n", + "\n", + "# Compute correlation matrix\n", + "factor_cols = list(factors.keys())\n", + "corr_matrix = merged[factor_cols].corr()\n", + "\n", + "print(\"Factor Correlation Matrix:\")\n", + "corr_matrix" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Heatmap visualization\n", + "fig, ax = plt.subplots(figsize=(8, 6))\n", + "sns.heatmap(\n", + " corr_matrix,\n", + " annot=True,\n", + " fmt=\".3f\",\n", + " cmap=\"RdBu_r\",\n", + " center=0,\n", + " vmin=-1,\n", + " vmax=1,\n", + " square=True,\n", + " ax=ax,\n", + ")\n", + "ax.set_title(\"Factor Correlation Matrix\")\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 3.1 Rolling Cross-Sectional Correlation\n", + "\n", + "Use `ts_corr()` to compute the time-varying correlation between two factors." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Rolling correlation between momentum and inverse volatility\n", + "rolling_corr = momentum.ts_corr(inv_vol_ranked, window=120)\n", + "rolling_corr.name = \"mom_vol_corr\"\n", + "\n", + "rolling_corr.plot.plot_timeseries(symbols=[\"BTCUSDT\", \"ETHUSDT\"])\n", + "plt.axhline(y=0, color=\"red\", linestyle=\"--\", alpha=0.5)\n", + "plt.title(\"Rolling Correlation: Momentum vs. Inverse Volatility (120-period window)\")\n", + "plt.ylabel(\"Correlation\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Factor Orthogonalization\n", + "\n", + "Use `cs_neutralize()` to remove the effect of one factor from another. This is useful when you want a factor that captures **unique** information." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Neutralize inverse volatility from momentum\n", + "# This gives us the \"momentum\" signal that is orthogonal to volatility\n", + "mom_ortho = momentum.cs_neutralize(inv_vol_ranked)\n", + "mom_ortho.name = \"momentum_orthogonalized\"\n", + "\n", + "# Check IC of orthogonalized factor\n", + "ortho_analysis = session.analyze(mom_ortho, periods=1)\n", + "ortho_ic = ortho_analysis.ic_summary.get(1, {})\n", + "\n", + "print(\"Orthogonalized Momentum IC:\")\n", + "print(f\" Mean IC: {ortho_ic.get('mean_ic', 0):.4f}\")\n", + "print(f\" IC IR: {ortho_ic.get('ic_ir', 0):.4f}\")\n", + "print()\n", + "print(\"Compare with original Momentum:\")\n", + "print(f\" Mean IC: {ic_df.loc['Momentum', 'Mean IC']:.4f}\")\n", + "print(f\" IC IR: {ic_df.loc['Momentum', 'IC IR']:.4f}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. Combining Factors with CompositeFactor\n", + "\n", + "`CompositeFactor` provides three ways to combine factors:\n", + "\n", + "| Method | Description |\n", + "|--------|-------------|\n", + "| `from_equal_weights()` | Equal weight to all factors |\n", + "| `from_weights()` | Custom weights (must sum to 1) |\n", + "| `from_zscore()` | Z-score standardize each factor first, then equal weight |" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "factor_list = [momentum, inv_vol_ranked, vol_ratio_ranked]\n", + "\n", + "# --- Method 1: Equal Weights ---\n", + "composite_equal = CompositeFactor.from_equal_weights(\n", + " factor_list, name=\"equal_weight\"\n", + ").to_factor()\n", + "\n", + "# --- Method 2: Custom Weights ---\n", + "# Give more weight to momentum, less to volume\n", + "composite_custom = CompositeFactor.from_weights(\n", + " factor_list, weights=[0.5, 0.3, 0.2], name=\"custom_weight\"\n", + ").to_factor()\n", + "\n", + "# --- Method 3: Z-Score Standardization ---\n", + "composite_zscore = CompositeFactor.from_zscore(\n", + " factor_list, name=\"zscore_combo\"\n", + ").to_factor()\n", + "\n", + "composites = {\n", + " \"Equal Weight\": composite_equal,\n", + " \"Custom Weight (0.5/0.3/0.2)\": composite_custom,\n", + " \"Z-Score Combo\": composite_zscore,\n", + "}\n", + "\n", + "print(\"Composite factors created:\")\n", + "for name, f in composites.items():\n", + " print(f\" {name}: {len(f):,} rows\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 5.1 Composite Factor IC Analysis" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Compare IC of all factors (individual + composite)\n", + "all_factors = {**factors, **composites}\n", + "\n", + "full_comparison = []\n", + "for name, factor in all_factors.items():\n", + " # Rank the composite factors for fair comparison\n", + " signal = factor.cs_rank() if name in composites else factor\n", + " analysis = session.analyze(signal, periods=1)\n", + " ic_stats = analysis.ic_summary.get(1, {})\n", + " full_comparison.append({\n", + " \"Factor\": name,\n", + " \"Mean IC\": ic_stats.get(\"mean_ic\", np.nan),\n", + " \"IC Std\": ic_stats.get(\"ic_std\", np.nan),\n", + " \"IC IR\": ic_stats.get(\"ic_ir\", np.nan),\n", + " \"Type\": \"Composite\" if name in composites else \"Single\",\n", + " })\n", + "\n", + "full_df = pd.DataFrame(full_comparison).set_index(\"Factor\")\n", + "print(\"Full Factor IC Comparison:\")\n", + "full_df" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Visualize: IC IR comparison\n", + "fig, ax = plt.subplots(figsize=(10, 6))\n", + "colors = [\"steelblue\" if t == \"Single\" else \"coral\" for t in full_df[\"Type\"]]\n", + "full_df[\"IC IR\"].plot.barh(ax=ax, color=colors)\n", + "ax.axvline(x=0, color=\"gray\", linestyle=\"--\")\n", + "ax.set_title(\"IC IR Comparison: Single vs. Composite Factors\")\n", + "ax.set_xlabel(\"IC IR\")\n", + "\n", + "# Add legend\n", + "from matplotlib.patches import Patch\n", + "ax.legend(handles=[\n", + " Patch(color=\"steelblue\", label=\"Single Factor\"),\n", + " Patch(color=\"coral\", label=\"Composite Factor\"),\n", + "])\n", + "\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 6. Backtesting: Single vs. Composite\n", + "\n", + "Let's see if combining factors actually improves backtest performance." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Backtest best individual factor and best composite\n", + "bt_results = {}\n", + "\n", + "for name, factor in all_factors.items():\n", + " signal = factor.cs_rank() if name in composites else factor\n", + " result = session.backtest(signal, neutralization=\"market\")\n", + " bt_results[name] = result\n", + "\n", + "# Collect metrics\n", + "metrics_rows = []\n", + "for name, result in bt_results.items():\n", + " m = result.metrics\n", + " metrics_rows.append({\n", + " \"Factor\": name,\n", + " \"Total Return\": m.get(\"total_return\", np.nan),\n", + " \"Sharpe\": m.get(\"sharpe_ratio\", np.nan),\n", + " \"Max DD\": m.get(\"max_drawdown\", np.nan),\n", + " \"Sortino\": m.get(\"sortino_ratio\", np.nan),\n", + " \"Type\": \"Composite\" if name in composites else \"Single\",\n", + " })\n", + "\n", + "bt_df = pd.DataFrame(metrics_rows).set_index(\"Factor\")\n", + "bt_df" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Plot equity curves for all factors\n", + "fig, ax = plt.subplots(figsize=(14, 7))\n", + "\n", + "for name, result in bt_results.items():\n", + " eq = result.equity_curve.to_pandas()\n", + " eq[\"ts\"] = pd.to_datetime(eq[\"start_time\"], unit=\"ms\")\n", + " style = \"--\" if name in factors else \"-\"\n", + " linewidth = 1.0 if name in factors else 2.0\n", + " ax.plot(eq[\"ts\"], eq[\"equity\"], style, label=name, linewidth=linewidth)\n", + "\n", + "ax.set_xlabel(\"Date\")\n", + "ax.set_ylabel(\"Equity\")\n", + "ax.set_title(\"Equity Curves — Single Factors (dashed) vs. Composite Factors (solid)\")\n", + "ax.legend(bbox_to_anchor=(1.05, 1), loc=\"upper left\")\n", + "ax.grid(True, alpha=0.3)\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 7. Factor Selection Workflow\n", + "\n", + "A practical approach to selecting which factors to include in a composite:\n", + "\n", + "1. **Filter by IC**: Keep factors with |Mean IC| above a threshold\n", + "2. **Filter by correlation**: Remove highly correlated factors (keep the one with better IC)\n", + "3. **Combine remaining factors**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Step 1: Filter by absolute Mean IC\n", + "IC_THRESHOLD = 0.005 # Adjust based on your domain\n", + "\n", + "selected = ic_df[ic_df[\"Mean IC\"].abs() >= IC_THRESHOLD]\n", + "print(f\"Step 1 — Factors with |Mean IC| >= {IC_THRESHOLD}:\")\n", + "print(selected)\n", + "print()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Step 2: Check correlation among selected factors\n", + "CORR_THRESHOLD = 0.7 # Max acceptable pairwise correlation\n", + "\n", + "selected_factors_list = [factors[name] for name in selected.index if name in factors]\n", + "selected_names = [f.name for f in selected_factors_list]\n", + "\n", + "print(f\"Selected factors for combination: {selected_names}\")\n", + "print(f\"Pairwise correlations (threshold = {CORR_THRESHOLD}):\")\n", + "\n", + "# Show relevant subset of correlation matrix\n", + "relevant_corr = corr_matrix.loc[\n", + " [n for n in selected.index if n in corr_matrix.index],\n", + " [n for n in selected.index if n in corr_matrix.columns],\n", + "]\n", + "relevant_corr" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Step 3: Combine selected factors\n", + "if len(selected_factors_list) > 1:\n", + " final_composite = CompositeFactor.from_equal_weights(\n", + " selected_factors_list, name=\"selected_composite\"\n", + " ).to_factor().cs_rank()\n", + " final_composite.name = \"selected_composite\"\n", + "\n", + " # Evaluate\n", + " final_report = session.quick_report(final_composite)\n", + " print(final_report)\n", + "else:\n", + " print(\"Only one factor selected — no combination needed.\")\n", + " final_report = session.quick_report(selected_factors_list[0])\n", + " print(final_report)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "In this notebook we:\n", + "- Built three individual factors: **momentum**, **inverse volatility**, and **volume ratio**\n", + "- Analyzed **factor correlations** using static correlation matrices and rolling `ts_corr()`\n", + "- **Orthogonalized** factors using `cs_neutralize()` to extract unique signals\n", + "- Combined factors using **`CompositeFactor`** (equal weight, custom weight, z-score)\n", + "- Compared **single-factor vs. composite-factor** performance via IC and backtesting\n", + "- Demonstrated a practical **factor selection workflow** (IC filter → correlation filter → combine)\n", + "\n", + "**Key takeaway:** Combining uncorrelated factors with positive IC tends to improve IC IR and reduce portfolio volatility, even if individual IC values are small." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} \ No newline at end of file diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..5cd600b --- /dev/null +++ b/examples/README.md @@ -0,0 +1,46 @@ +# Factorium Examples + +Interactive Jupyter notebooks demonstrating factor research workflows with Factorium. + +## Notebooks + +| Notebook | Description | Key Concepts | +|----------|-------------|-------------| +| [01 — Momentum Factor Research](01_momentum_factor_research.ipynb) | Complete workflow from data loading to backtest | Data loading, factor construction (code & expression), IC analysis, multi-horizon IC decay, quantile returns, backtesting, quick report | +| [02 — Mean Reversion Factor](02_mean_reversion_factor.ipynb) | Mean reversion with cross-sectional processing | Z-score distance, volatility normalization, `cs_rank`, `cs_zscore`, `cs_winsorize`, market-neutral vs. long-only backtest, advanced operators (`ts_autocorr`, `ts_kurtosis`, `ts_skewness`) | +| [03 — Data Loading & Exploration](03_data_loading_and_exploration.ipynb) | Deep dive into data handling | `BinanceDataLoader`, `AggBar` methods, time-bar intervals (1min/5min/1h), slicing, CSV/Parquet export, `ResearchSession` from files | +| [04 — Multi-Factor Combination](04_multi_factor_combination.ipynb) | Combine and select factors | Factor correlations, `ts_corr`, `cs_neutralize`, `CompositeFactor` (equal/custom/z-score), single vs. composite backtest, factor selection workflow | + +## Getting Started + +### Prerequisites + +```bash +pip install factorium +# or +uv add factorium +``` + +A Jupyter environment is required: + +```bash +pip install jupyterlab +``` + +### Running the Notebooks + +```bash +cd examples/ +jupyter lab +``` + +> **Note:** Notebooks download data directly from Binance Vision. The first run may take a few minutes depending on your internet connection. Subsequent runs are faster thanks to local caching. + +## Recommended Reading Order + +If you're new to Factorium, we recommend starting with: + +1. **Notebook 03** — Understand data loading and the `AggBar` container +2. **Notebook 01** — Walk through a full factor research workflow +3. **Notebook 02** — Learn about signal processing and cross-sectional transforms +4. **Notebook 04** — Combine multiple factors into a composite signal From 5abe8f7cf63f6fbfbe2b1ec60496861ab3a4cb61 Mon Sep 17 00:00:00 2001 From: novis10813 Date: Thu, 12 Feb 2026 20:48:28 +0800 Subject: [PATCH 02/25] fix: auto-prepare data in FactorAnalyzer methods - Add _ensure_data_prepared() helper to FactorAnalyzer that auto-calls prepare_data() when _clean_data is not yet set - Replace ValueError raises in calculate_ic(), calculate_turnover(), and calculate_quantile_returns() with auto-prepare calls - Fix notebook 01 to explicitly call prepare_data() before plotting quantile returns (best practice documentation) - Clear stale error outputs from notebook cells --- examples/01_momentum_factor_research.ipynb | 2112 +++++++++++++++----- src/factorium/factors/analyzer.py | 15 +- 2 files changed, 1639 insertions(+), 488 deletions(-) diff --git a/examples/01_momentum_factor_research.ipynb b/examples/01_momentum_factor_research.ipynb index 8a81281..8a3a633 100644 --- a/examples/01_momentum_factor_research.ipynb +++ b/examples/01_momentum_factor_research.ipynb @@ -1,485 +1,1633 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Momentum Factor Research\n", - "\n", - "This notebook walks through a **complete factor research workflow** using Factorium:\n", - "\n", - "1. Load data from Binance\n", - "2. Explore the AggBar data structure\n", - "3. Build momentum factors (code-based & expression-based)\n", - "4. Visualize factor behavior\n", - "5. Run IC (Information Coefficient) analysis\n", - "6. Analyze quantile returns\n", - "7. Run a vectorized backtest\n", - "8. Generate a quick report\n", - "\n", - "**Prerequisites**: `pip install factorium` (or `uv add factorium`)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 1. Setup & Data Loading\n", - "\n", - "We'll download 30 days of 1-minute futures data for 10 crypto symbols from Binance Vision." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from factorium import BinanceDataLoader, ResearchSession\n", - "from factorium.factors import FactorAnalyzer, CompositeFactor\n", - "\n", - "import matplotlib.pyplot as plt\n", - "import pandas as pd\n", - "import numpy as np\n", - "\n", - "%matplotlib inline\n", - "plt.style.use(\"seaborn-v0_8-whitegrid\")\n", - "plt.rcParams[\"figure.figsize\"] = (14, 6)\n", - "plt.rcParams[\"figure.dpi\"] = 100" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "SYMBOLS = [\n", - " \"BTCUSDT\", \"ETHUSDT\", \"BNBUSDT\", \"SOLUSDT\", \"XRPUSDT\",\n", - " \"DOGEUSDT\", \"ADAUSDT\", \"AVAXUSDT\", \"DOTUSDT\", \"CAKEUSDT\",\n", - "]\n", - "\n", - "loader = BinanceDataLoader()\n", - "\n", - "agg = loader.load_aggbar(\n", - " symbols=SYMBOLS,\n", - " data_type=\"aggTrades\",\n", - " market_type=\"futures\",\n", - " futures_type=\"um\",\n", - " days=30,\n", - " bar_type=\"time\",\n", - " interval=60_000, # 1-minute bars\n", - ")\n", - "\n", - "print(f\"Loaded {len(agg):,} bars\")\n", - "print(f\"Symbols: {agg.symbols}\")\n", - "print(f\"Columns: {agg.cols}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 2. Exploring the AggBar\n", - "\n", - "`AggBar` is a **multi-symbol OHLCV container** stored in long format. You can inspect it, convert to Polars/Pandas, and slice by time or symbols." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Summary info per symbol\n", - "agg.info()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# View raw data as Polars DataFrame\n", - "agg.to_polars().head(10)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Slice by symbols — creates a new AggBar\n", - "btc_eth = agg.slice(symbols=[\"BTCUSDT\", \"ETHUSDT\"])\n", - "print(f\"Sliced AggBar: {btc_eth.symbols}, {len(btc_eth):,} bars\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 3. Building Momentum Factors\n", - "\n", - "### 3.1 Code-Based Factor Construction\n", - "\n", - "Extract a column from `AggBar` with `agg[\"close\"]` to get a `Factor` object, then chain time-series and cross-sectional operators." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "close = agg[\"close\"]\n", - "volume = agg[\"volume\"]\n", - "\n", - "# Simple momentum: percentage change over 60 periods\n", - "momentum_60 = close.ts_delta(60) / close.ts_shift(60)\n", - "momentum_60.name = \"momentum_60\"\n", - "\n", - "# Short-term momentum (20 periods)\n", - "momentum_20 = close.ts_delta(20) / close.ts_shift(20)\n", - "momentum_20.name = \"momentum_20\"\n", - "\n", - "# Volatility-adjusted momentum\n", - "volatility = (close.ts_delta(1) / close.ts_shift(1)).ts_std(60)\n", - "vol_adj_momentum = momentum_60 / volatility\n", - "vol_adj_momentum.name = \"vol_adj_momentum\"\n", - "\n", - "# Cross-sectional rank (normalized to [0, 1])\n", - "momentum_rank = momentum_60.cs_rank()\n", - "momentum_rank.name = \"momentum_rank\"\n", - "\n", - "print(\"Factor shapes:\")\n", - "print(f\" momentum_60: {len(momentum_60):,} rows\")\n", - "print(f\" vol_adj_momentum: {len(vol_adj_momentum):,} rows\")\n", - "print(f\" momentum_rank: {len(momentum_rank):,} rows\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 3.2 Expression-Based Factor Construction\n", - "\n", - "You can also define factors using string expressions via `ResearchSession.create_factor()`. This is useful for rapid experimentation." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "session = ResearchSession(agg, default_frequency=\"1min\")\n", - "\n", - "# Same momentum factor, defined via expression\n", - "mom_expr = session.create_factor(\n", - " \"ts_delta(close, 60) / ts_shift(close, 60)\",\n", - " name=\"momentum_60_expr\",\n", - ")\n", - "\n", - "# A more complex expression: MA crossover signal\n", - "ma_cross = session.create_factor(\n", - " \"ts_mean(close, 10) - ts_mean(close, 30)\",\n", - " name=\"ma_crossover\",\n", - ")\n", - "\n", - "print(f\"Expression-based momentum: {len(mom_expr):,} rows\")\n", - "print(f\"MA crossover signal: {len(ma_cross):,} rows\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 4. Visualizing Factors\n", - "\n", - "Use the `.plot` accessor on Factor objects to produce time series, heatmaps, and distributions." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Time series of momentum for a few symbols\n", - "momentum_60.plot.plot_timeseries(symbols=[\"BTCUSDT\", \"ETHUSDT\", \"SOLUSDT\"])\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Distribution of momentum values across all symbols\n", - "momentum_60.plot.plot_distribution()\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Heatmap: momentum intensity across symbols and time\n", - "momentum_rank.plot.plot_heatmap()\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 5. IC (Information Coefficient) Analysis\n", - "\n", - "The **IC** measures the rank correlation between factor values and subsequent returns. A consistently positive (or negative) IC indicates predictive power.\n", - "\n", - "### 5.1 Single-Period IC" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Use ResearchSession for streamlined analysis\n", - "signal = momentum_rank # Use ranked momentum as our signal\n", - "\n", - "analysis = session.analyze(signal, periods=1)\n", - "\n", - "print(\"IC Summary:\")\n", - "print(analysis.ic_summary)\n", - "print()\n", - "print(f\"IC Series shape: {analysis.ic_series.shape}\")\n", - "print(analysis.ic_series.head())" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 5.2 Multi-Horizon IC Analysis\n", - "\n", - "To understand how quickly a factor's signal decays, we compute IC across multiple forward horizons." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Compute IC for multiple horizons\n", - "horizons = [1, 5, 10, 20, 60]\n", - "ic_results = {}\n", - "\n", - "for h in horizons:\n", - " result = session.analyze(signal, periods=h)\n", - " ic_results[h] = result.ic_summary.get(h, {})\n", - "\n", - "# Display IC decay table\n", - "ic_decay_df = pd.DataFrame(ic_results).T\n", - "ic_decay_df.index.name = \"horizon\"\n", - "print(\"IC Decay Analysis:\")\n", - "ic_decay_df" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Plot IC decay curve\n", - "fig, ax = plt.subplots(figsize=(10, 5))\n", - "ax.plot(ic_decay_df.index, ic_decay_df[\"mean_ic\"], \"o-\", linewidth=2, markersize=8)\n", - "ax.axhline(y=0, color=\"gray\", linestyle=\"--\", alpha=0.5)\n", - "ax.set_xlabel(\"Forward Horizon (periods)\")\n", - "ax.set_ylabel(\"Mean IC\")\n", - "ax.set_title(\"IC Decay Curve — Momentum Factor\")\n", - "ax.grid(True, alpha=0.3)\n", - "plt.tight_layout()\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 6. Quantile Analysis\n", - "\n", - "Split the cross-section into quantiles by factor value and examine the mean returns of each group. A monotonic pattern (Q1 < Q2 < ... < Q5) indicates strong linear predictive power." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Use FactorAnalyzer directly for more control\n", - "analyzer = FactorAnalyzer(signal, agg, quantiles=5)\n", - "\n", - "# Quantile returns\n", - "analyzer.plot_quantile_returns(quantiles=5, period=1)\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Cumulative returns by quantile (includes Long-Short portfolio)\n", - "analyzer.plot_cumulative_returns(quantiles=5, period=1, long_short=True)\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# IC time series plot\n", - "analyzer.plot_ic(period=1, method=\"rank\", plot_type=\"ts\")\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 7. Backtesting\n", - "\n", - "Run a **market-neutral vectorized backtest** using the momentum signal. The backtester:\n", - "- Converts signals to portfolio weights (cross-sectional normalization)\n", - "- Handles transaction costs\n", - "- Tracks equity, returns, and positions over time" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Market-neutral backtest\n", - "result = session.backtest(\n", - " signal,\n", - " neutralization=\"market\",\n", - " transaction_cost=0.0003,\n", - ")\n", - "\n", - "# Key performance metrics\n", - "print(\"Backtest Metrics:\")\n", - "for key, val in result.metrics.items():\n", - " if isinstance(val, float):\n", - " print(f\" {key:25s}: {val:>10.4f}\")\n", - " else:\n", - " print(f\" {key:25s}: {val}\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Plot equity curve\n", - "equity = result.equity_curve.to_pandas()\n", - "equity[\"timestamp\"] = pd.to_datetime(equity[\"start_time\"], unit=\"ms\")\n", - "\n", - "fig, ax = plt.subplots(figsize=(14, 6))\n", - "ax.plot(equity[\"timestamp\"], equity[\"equity\"], linewidth=1.5)\n", - "ax.set_xlabel(\"Date\")\n", - "ax.set_ylabel(\"Portfolio Equity\")\n", - "ax.set_title(\"Momentum Factor — Equity Curve (Market Neutral)\")\n", - "ax.grid(True, alpha=0.3)\n", - "plt.tight_layout()\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Plot returns distribution\n", - "returns = result.returns.to_pandas()\n", - "\n", - "fig, ax = plt.subplots(figsize=(10, 5))\n", - "ax.hist(returns[\"portfolio_return\"].dropna(), bins=100, alpha=0.75, edgecolor=\"black\", linewidth=0.5)\n", - "ax.axvline(x=0, color=\"red\", linestyle=\"--\", alpha=0.7)\n", - "ax.set_xlabel(\"Return\")\n", - "ax.set_ylabel(\"Frequency\")\n", - "ax.set_title(\"Distribution of Portfolio Returns\")\n", - "ax.grid(True, alpha=0.3)\n", - "plt.tight_layout()\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 8. Quick Report\n", - "\n", - "`ResearchSession.quick_report()` combines IC analysis and backtesting into a single text summary — great for rapid iteration." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "report = session.quick_report(signal)\n", - "print(report)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 9. Summary & Next Steps\n", - "\n", - "In this notebook we:\n", - "- Loaded multi-symbol 1-minute data from Binance using `BinanceDataLoader`\n", - "- Built momentum factors via both **code-based** and **expression-based** methods\n", - "- Visualized factor behavior with time series, distributions, and heatmaps\n", - "- Computed IC and analyzed its decay across multiple horizons\n", - "- Ran a market-neutral backtest and examined performance metrics\n", - "\n", - "**Next notebooks to explore:**\n", - "- `02_mean_reversion_factor.ipynb` — Mean reversion with volatility normalization\n", - "- `03_data_loading_and_exploration.ipynb` — Deep dive into AggBar and data loading\n", - "- `04_multi_factor_combination.ipynb` — Combine multiple factors and compare performance" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".venv", - "language": "python", - "name": "python3" - }, - "language_info": { - "name": "python", - "version": "3.13.2" - } + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Momentum Factor Research\n", + "\n", + "This notebook walks through a **complete factor research workflow** using Factorium:\n", + "\n", + "1. Load data from Binance\n", + "2. Explore the AggBar data structure\n", + "3. Build momentum factors (code-based & expression-based)\n", + "4. Visualize factor behavior\n", + "5. Run IC (Information Coefficient) analysis\n", + "6. Analyze quantile returns\n", + "7. Run a vectorized backtest\n", + "8. Generate a quick report\n", + "\n", + "**Prerequisites**: `pip install factorium` (or `uv add factorium`)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Setup & Data Loading\n", + "\n", + "We'll download 30 days of 1-minute futures data for 10 crypto symbols from Binance Vision." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from factorium import BinanceDataLoader, ResearchSession\n", + "from factorium.factors import FactorAnalyzer, CompositeFactor\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import pandas as pd\n", + "import numpy as np\n", + "\n", + "%matplotlib inline\n", + "plt.style.use(\"seaborn-v0_8-whitegrid\")\n", + "plt.rcParams[\"figure.figsize\"] = (14, 6)\n", + "plt.rcParams[\"figure.dpi\"] = 100" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2026-02-12 19:22:07,485 - INFO - Downloading 10 date ranges for 10 symbols...\n", + "2026-02-12 19:22:07,487 - ERROR - Exception in callback Task.__step()\n", + "handle: \n", + "Traceback (most recent call last):\n", + " File \"/home/novis/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/asyncio/events.py\", line 89, in _run\n", + " self._context.run(self._callback, *self._args)\n", + " ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + "RuntimeError: cannot enter context: <_contextvars.Context object at 0x727d2837eac0> is already entered\n", + "2026-02-12 19:22:07,498 - ERROR - Exception in callback Task.__step()\n", + "handle: \n", + "Traceback (most recent call last):\n", + " File \"/home/novis/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/asyncio/events.py\", line 89, in _run\n", + " self._context.run(self._callback, *self._args)\n", + " ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + "RuntimeError: cannot enter context: <_contextvars.Context object at 0x727d2837eac0> is already entered\n", + "2026-02-12 19:22:07,500 - INFO - Processing data for 2026-01-14\n", + "2026-02-12 19:22:07,502 - INFO - Processing data for 2026-01-15\n", + "2026-02-12 19:22:07,504 - INFO - Processing data for 2026-01-16\n", + "2026-02-12 19:22:07,506 - INFO - Processing data for 2026-01-17\n", + "2026-02-12 19:22:07,508 - INFO - Processing data for 2026-01-18\n", + "2026-02-12 19:22:07,509 - INFO - Processing data for 2026-01-14\n", + "2026-02-12 19:22:07,512 - INFO - Processing data for 2026-01-15\n", + "2026-02-12 19:22:07,513 - INFO - Processing data for 2026-01-16\n", + "2026-02-12 19:22:07,514 - INFO - Processing data for 2026-01-17\n", + "2026-02-12 19:22:07,516 - INFO - Processing data for 2026-01-18\n", + "2026-02-12 19:22:07,517 - INFO - Processing data for 2026-01-14\n", + "2026-02-12 19:22:07,519 - INFO - Processing data for 2026-01-15\n", + "2026-02-12 19:22:07,521 - INFO - Processing data for 2026-01-16\n", + "2026-02-12 19:22:07,522 - INFO - Processing data for 2026-01-17\n", + "2026-02-12 19:22:07,524 - INFO - Processing data for 2026-01-18\n", + "2026-02-12 19:22:07,525 - INFO - Processing data for 2026-01-14\n", + "2026-02-12 19:22:07,528 - INFO - Processing data for 2026-01-15\n", + "2026-02-12 19:22:07,529 - INFO - Processing data for 2026-01-16\n", + "2026-02-12 19:22:07,531 - INFO - Processing data for 2026-01-17\n", + "2026-02-12 19:22:07,532 - INFO - Processing data for 2026-01-18\n", + "2026-02-12 19:22:07,533 - INFO - Processing data for 2026-01-14\n", + "2026-02-12 19:22:07,535 - INFO - Processing data for 2026-01-15\n", + "2026-02-12 19:22:07,537 - INFO - Processing data for 2026-01-16\n", + "2026-02-12 19:22:07,538 - INFO - Processing data for 2026-01-17\n", + "2026-02-12 19:22:07,539 - INFO - Processing data for 2026-01-18\n", + "2026-02-12 19:22:07,541 - INFO - Processing data for 2026-01-14\n", + "2026-02-12 19:22:07,542 - INFO - Processing data for 2026-01-15\n", + "2026-02-12 19:22:07,544 - INFO - Processing data for 2026-01-16\n", + "2026-02-12 19:22:07,546 - INFO - Processing data for 2026-01-17\n", + "2026-02-12 19:22:07,548 - INFO - Processing data for 2026-01-18\n", + "2026-02-12 19:22:07,549 - INFO - Processing data for 2026-01-14\n", + "2026-02-12 19:22:07,551 - INFO - Processing data for 2026-01-15\n", + "2026-02-12 19:22:07,552 - INFO - Processing data for 2026-01-16\n", + "2026-02-12 19:22:07,554 - INFO - Processing data for 2026-01-17\n", + "2026-02-12 19:22:07,555 - INFO - Processing data for 2026-01-18\n", + "2026-02-12 19:22:07,557 - INFO - Processing data for 2026-01-14\n", + "2026-02-12 19:22:07,559 - INFO - Processing data for 2026-01-15\n", + "2026-02-12 19:22:07,560 - INFO - Processing data for 2026-01-16\n", + "2026-02-12 19:22:07,562 - INFO - Processing data for 2026-01-17\n", + "2026-02-12 19:22:07,564 - INFO - Processing data for 2026-01-18\n", + "2026-02-12 19:22:07,565 - INFO - Processing data for 2026-01-14\n", + "2026-02-12 19:22:07,567 - INFO - Processing data for 2026-01-15\n", + "2026-02-12 19:22:07,568 - INFO - Processing data for 2026-01-16\n", + "2026-02-12 19:22:07,570 - INFO - Processing data for 2026-01-17\n", + "2026-02-12 19:22:07,571 - INFO - Processing data for 2026-01-18\n", + "2026-02-12 19:22:07,578 - ERROR - Task was destroyed but it is pending!\n", + "task: cb=[Task.task_wakeup()]>\n", + "2026-02-12 19:22:07,580 - ERROR - Task was destroyed but it is pending!\n", + "task: .run_in_context() running at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/ipykernel/utils.py:60> wait_for= cb=[Task.__wakeup()]> cb=[ZMQStream._run_callback.._log_error() at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/zmq/eventloop/zmqstream.py:563]>\n", + "/home/novis/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/asyncio/futures.py:119: RuntimeWarning: coroutine 'Kernel.shell_main' was never awaited\n", + " def get_loop(self):\n", + "RuntimeWarning: Enable tracemalloc to get the object allocation traceback\n", + "2026-02-12 19:22:07,584 - ERROR - Task was destroyed but it is pending!\n", + "task: .run_in_context() running at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/ipykernel/utils.py:60> wait_for= cb=[Task.task_wakeup()]> cb=[ZMQStream._run_callback.._log_error() at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/zmq/eventloop/zmqstream.py:563]>\n", + "2026-02-12 19:22:07,585 - ERROR - Task was destroyed but it is pending!\n", + "task: cb=[Task.__wakeup()]>\n", + "2026-02-12 19:22:07,586 - INFO - Processing data for 2026-01-14\n", + "2026-02-12 19:22:07,587 - INFO - Processing data for 2026-01-15\n", + "2026-02-12 19:22:07,589 - INFO - Processing data for 2026-01-16\n", + "2026-02-12 19:22:07,590 - INFO - Processing data for 2026-01-17\n", + "2026-02-12 19:22:07,591 - INFO - Processing data for 2026-01-18\n", + "2026-02-12 19:22:07,592 - ERROR - Exception in callback Task.__step()\n", + "handle: \n", + "Traceback (most recent call last):\n", + " File \"/home/novis/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/asyncio/events.py\", line 89, in _run\n", + " self._context.run(self._callback, *self._args)\n", + " ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + "RuntimeError: cannot enter context: <_contextvars.Context object at 0x727d2837eac0> is already entered\n", + "2026-02-12 19:22:07,594 - ERROR - Exception in callback Task.__step()\n", + "handle: \n", + "Traceback (most recent call last):\n", + " File \"/home/novis/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/asyncio/events.py\", line 89, in _run\n", + " self._context.run(self._callback, *self._args)\n", + " ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + "RuntimeError: cannot enter context: <_contextvars.Context object at 0x727d2837eac0> is already entered\n", + "2026-02-12 19:22:07,596 - ERROR - Exception in callback Task.__step()\n", + "handle: \n", + "Traceback (most recent call last):\n", + " File \"/home/novis/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/asyncio/events.py\", line 89, in _run\n", + " self._context.run(self._callback, *self._args)\n", + " ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + "RuntimeError: cannot enter context: <_contextvars.Context object at 0x727d2837eac0> is already entered\n", + "2026-02-12 19:22:07,600 - ERROR - Task was destroyed but it is pending!\n", + "task: .run_in_context() done, defined at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/ipykernel/utils.py:57> wait_for= cb=[Task.__wakeup()]> cb=[ZMQStream._run_callback.._log_error() at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/zmq/eventloop/zmqstream.py:563]>\n", + "/home/novis/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/typing.py:426: RuntimeWarning: coroutine 'Kernel.shell_main' was never awaited\n", + " @functools.wraps(func)\n", + "RuntimeWarning: Enable tracemalloc to get the object allocation traceback\n", + "2026-02-12 19:22:07,604 - ERROR - Task was destroyed but it is pending!\n", + "task: cb=[Task.__wakeup()]>\n", + "2026-02-12 19:22:07,606 - ERROR - Exception in callback Task.__step()\n", + "handle: \n", + "Traceback (most recent call last):\n", + " File \"/home/novis/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/asyncio/events.py\", line 89, in _run\n", + " self._context.run(self._callback, *self._args)\n", + " ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + "RuntimeError: cannot enter context: <_contextvars.Context object at 0x727d2837eac0> is already entered\n", + "2026-02-12 19:22:07,613 - ERROR - Exception in callback Task.__step()\n", + "handle: \n", + "Traceback (most recent call last):\n", + " File \"/home/novis/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/asyncio/events.py\", line 89, in _run\n", + " self._context.run(self._callback, *self._args)\n", + " ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + "RuntimeError: cannot enter context: <_contextvars.Context object at 0x727d2837eac0> is already entered\n", + "2026-02-12 19:22:07,616 - ERROR - Exception in callback Task.__step()\n", + "handle: \n", + "Traceback (most recent call last):\n", + " File \"/home/novis/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/asyncio/events.py\", line 89, in _run\n", + " self._context.run(self._callback, *self._args)\n", + " ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + "RuntimeError: cannot enter context: <_contextvars.Context object at 0x727d2837eac0> is already entered\n", + "2026-02-12 19:22:07,617 - ERROR - Exception in callback Task.__step()\n", + "handle: \n", + "Traceback (most recent call last):\n", + " File \"/home/novis/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/asyncio/events.py\", line 89, in _run\n", + " self._context.run(self._callback, *self._args)\n", + " ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + "RuntimeError: cannot enter context: <_contextvars.Context object at 0x727d2837eac0> is already entered\n", + "2026-02-12 19:22:07,619 - ERROR - Exception in callback Task.__step()\n", + "handle: \n", + "Traceback (most recent call last):\n", + " File \"/home/novis/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/asyncio/events.py\", line 89, in _run\n", + " self._context.run(self._callback, *self._args)\n", + " ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + "RuntimeError: cannot enter context: <_contextvars.Context object at 0x727d2837eac0> is already entered\n", + "2026-02-12 19:22:07,621 - ERROR - Exception in callback Task.__step()\n", + "handle: \n", + "Traceback (most recent call last):\n", + " File \"/home/novis/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/asyncio/events.py\", line 89, in _run\n", + " self._context.run(self._callback, *self._args)\n", + " ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + "RuntimeError: cannot enter context: <_contextvars.Context object at 0x727d2837eac0> is already entered\n", + "2026-02-12 19:22:07,622 - ERROR - Exception in callback Task.__step()\n", + "handle: \n", + "Traceback (most recent call last):\n", + " File \"/home/novis/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/asyncio/events.py\", line 89, in _run\n", + " self._context.run(self._callback, *self._args)\n", + " ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + "RuntimeError: cannot enter context: <_contextvars.Context object at 0x727d2837eac0> is already entered\n", + "2026-02-12 19:22:07,629 - ERROR - Exception in callback Task.__step()\n", + "handle: \n", + "Traceback (most recent call last):\n", + " File \"/home/novis/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/asyncio/events.py\", line 89, in _run\n", + " self._context.run(self._callback, *self._args)\n", + " ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + "RuntimeError: cannot enter context: <_contextvars.Context object at 0x727d2837eac0> is already entered\n", + "2026-02-12 19:22:07,637 - ERROR - Exception in callback Task.__step()\n", + "handle: \n", + "Traceback (most recent call last):\n", + " File \"/home/novis/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/asyncio/events.py\", line 89, in _run\n", + " self._context.run(self._callback, *self._args)\n", + " ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + "RuntimeError: cannot enter context: <_contextvars.Context object at 0x727d2837eac0> is already entered\n", + "2026-02-12 19:22:07,651 - ERROR - Exception in callback Task.__step()\n", + "handle: \n", + "Traceback (most recent call last):\n", + " File \"/home/novis/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/asyncio/events.py\", line 89, in _run\n", + " self._context.run(self._callback, *self._args)\n", + " ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + "RuntimeError: cannot enter context: <_contextvars.Context object at 0x727d2837eac0> is already entered\n", + "2026-02-12 19:22:07,691 - ERROR - Exception in callback Task.__step()\n", + "handle: \n", + "Traceback (most recent call last):\n", + " File \"/home/novis/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/asyncio/events.py\", line 89, in _run\n", + " self._context.run(self._callback, *self._args)\n", + " ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + "RuntimeError: cannot enter context: <_contextvars.Context object at 0x727d2837eac0> is already entered\n", + "2026-02-12 19:22:07,721 - ERROR - Exception in callback Task.__step()\n", + "handle: \n", + "Traceback (most recent call last):\n", + " File \"/home/novis/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/asyncio/events.py\", line 89, in _run\n", + " self._context.run(self._callback, *self._args)\n", + " ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + "RuntimeError: cannot enter context: <_contextvars.Context object at 0x727d2837eac0> is already entered\n", + "2026-02-12 19:22:07,732 - ERROR - Exception in callback Task.__step()\n", + "handle: \n", + "Traceback (most recent call last):\n", + " File \"/home/novis/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/asyncio/events.py\", line 89, in _run\n", + " self._context.run(self._callback, *self._args)\n", + " ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + "RuntimeError: cannot enter context: <_contextvars.Context object at 0x727d2837eac0> is already entered\n", + "2026-02-12 19:22:08,793 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-01-17.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=01/day=17\n", + "2026-02-12 19:22:08,798 - INFO - Processing data for 2026-01-19\n", + "2026-02-12 19:22:09,010 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-01-16.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=01/day=16\n", + "2026-02-12 19:22:09,014 - INFO - Processing data for 2026-01-19\n", + "2026-02-12 19:22:09,332 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-01-15.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=01/day=15\n", + "2026-02-12 19:22:09,337 - INFO - Processing data for 2026-01-20\n", + "2026-02-12 19:22:09,456 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-01-17.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=01/day=17\n", + "2026-02-12 19:22:09,460 - INFO - Processing data for 2026-01-20\n", + "2026-02-12 19:22:09,587 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-01-14.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=01/day=14\n", + "2026-02-12 19:22:09,592 - INFO - Processing data for 2026-01-21\n", + "2026-02-12 19:22:09,684 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-01-18.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=01/day=18\n", + "2026-02-12 19:22:09,687 - INFO - Processing data for 2026-01-21\n", + "2026-02-12 19:22:09,789 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-01-16.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=01/day=16\n", + "2026-02-12 19:22:09,792 - INFO - Processing data for 2026-01-22\n", + "2026-02-12 19:22:09,920 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-01-19.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=01/day=19\n", + "2026-02-12 19:22:09,925 - INFO - Processing data for 2026-01-23\n", + "2026-02-12 19:22:10,030 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-01-15.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=01/day=15\n", + "2026-02-12 19:22:10,034 - INFO - Processing data for 2026-01-22\n", + "2026-02-12 19:22:10,461 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-01-19.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=01/day=19\n", + "2026-02-12 19:22:10,467 - INFO - Processing data for 2026-01-23\n", + "2026-02-12 19:22:10,788 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-01-16.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=01/day=16\n", + "2026-02-12 19:22:10,804 - INFO - Processing data for 2026-01-19\n", + "2026-02-12 19:22:11,041 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-01-18.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=01/day=18\n", + "2026-02-12 19:22:11,050 - INFO - Processing data for 2026-01-20\n", + "2026-02-12 19:22:11,081 - ERROR - Task was destroyed but it is pending!\n", + "task: .run_in_context() done, defined at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/ipykernel/utils.py:57> wait_for= cb=[Task.__wakeup()]> cb=[ZMQStream._run_callback.._log_error() at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/zmq/eventloop/zmqstream.py:563]>\n", + "/home/novis/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/pathlib/_local.py:277: RuntimeWarning: coroutine 'Kernel.shell_main' was never awaited\n", + " @property\n", + "RuntimeWarning: Enable tracemalloc to get the object allocation traceback\n", + "2026-02-12 19:22:11,087 - ERROR - Task was destroyed but it is pending!\n", + "task: cb=[Task.__wakeup()]>\n", + "2026-02-12 19:22:11,088 - ERROR - Task was destroyed but it is pending!\n", + "task: .run_in_context() done, defined at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/ipykernel/utils.py:57> wait_for= cb=[Task.__wakeup()]> cb=[ZMQStream._run_callback.._log_error() at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/zmq/eventloop/zmqstream.py:563]>\n", + "2026-02-12 19:22:11,089 - ERROR - Task was destroyed but it is pending!\n", + "task: cb=[Task.__wakeup()]>\n", + "2026-02-12 19:22:11,090 - ERROR - Task was destroyed but it is pending!\n", + "task: .run_in_context() done, defined at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/ipykernel/utils.py:57> wait_for= cb=[Task.__wakeup()]> cb=[ZMQStream._run_callback.._log_error() at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/zmq/eventloop/zmqstream.py:563]>\n", + "2026-02-12 19:22:11,091 - ERROR - Task was destroyed but it is pending!\n", + "task: cb=[Task.__wakeup()]>\n", + "2026-02-12 19:22:11,092 - ERROR - Task was destroyed but it is pending!\n", + "task: .run_in_context() done, defined at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/ipykernel/utils.py:57> wait_for= cb=[Task.__wakeup()]> cb=[ZMQStream._run_callback.._log_error() at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/zmq/eventloop/zmqstream.py:563]>\n", + "2026-02-12 19:22:11,093 - ERROR - Task was destroyed but it is pending!\n", + "task: cb=[Task.__wakeup()]>\n", + "2026-02-12 19:22:11,094 - ERROR - Task was destroyed but it is pending!\n", + "task: .run_in_context() done, defined at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/ipykernel/utils.py:57> wait_for= cb=[Task.__wakeup()]> cb=[ZMQStream._run_callback.._log_error() at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/zmq/eventloop/zmqstream.py:563]>\n", + "2026-02-12 19:22:11,095 - ERROR - Task was destroyed but it is pending!\n", + "task: cb=[Task.__wakeup()]>\n", + "2026-02-12 19:22:11,096 - ERROR - Task was destroyed but it is pending!\n", + "task: .run_in_context() done, defined at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/ipykernel/utils.py:57> wait_for= cb=[Task.__wakeup()]> cb=[ZMQStream._run_callback.._log_error() at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/zmq/eventloop/zmqstream.py:563]>\n", + "2026-02-12 19:22:11,097 - ERROR - Task was destroyed but it is pending!\n", + "task: cb=[Task.__wakeup()]>\n", + "2026-02-12 19:22:11,098 - ERROR - Task was destroyed but it is pending!\n", + "task: .run_in_context() done, defined at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/ipykernel/utils.py:57> wait_for= cb=[Task.__wakeup()]> cb=[ZMQStream._run_callback.._log_error() at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/zmq/eventloop/zmqstream.py:563]>\n", + "2026-02-12 19:22:11,099 - ERROR - Task was destroyed but it is pending!\n", + "task: cb=[Task.__wakeup()]>\n", + "2026-02-12 19:22:11,102 - ERROR - Task was destroyed but it is pending!\n", + "task: .run_in_context() done, defined at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/ipykernel/utils.py:57> wait_for= cb=[Task.__wakeup()]> cb=[ZMQStream._run_callback.._log_error() at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/zmq/eventloop/zmqstream.py:563]>\n", + "2026-02-12 19:22:11,103 - ERROR - Task was destroyed but it is pending!\n", + "task: cb=[Task.__wakeup()]>\n", + "2026-02-12 19:22:11,104 - ERROR - Task was destroyed but it is pending!\n", + "task: .run_in_context() done, defined at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/ipykernel/utils.py:57> wait_for= cb=[Task.__wakeup()]> cb=[ZMQStream._run_callback.._log_error() at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/zmq/eventloop/zmqstream.py:563]>\n", + "2026-02-12 19:22:11,106 - ERROR - Task was destroyed but it is pending!\n", + "task: cb=[Task.__wakeup()]>\n", + "2026-02-12 19:22:11,107 - ERROR - Task was destroyed but it is pending!\n", + "task: .run_in_context() done, defined at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/ipykernel/utils.py:57> wait_for= cb=[Task.__wakeup()]> cb=[ZMQStream._run_callback.._log_error() at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/zmq/eventloop/zmqstream.py:563]>\n", + "2026-02-12 19:22:11,108 - ERROR - Task was destroyed but it is pending!\n", + "task: cb=[Task.__wakeup()]>\n", + "2026-02-12 19:22:11,253 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-01-18.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=01/day=18\n", + "2026-02-12 19:22:11,402 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-01-14.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=01/day=14\n", + "2026-02-12 19:22:11,413 - INFO - Processing data for 2026-01-24\n", + "2026-02-12 19:22:11,416 - INFO - Processing data for 2026-01-24\n", + "2026-02-12 19:22:11,798 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-01-18.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=01/day=18\n", + "2026-02-12 19:22:11,825 - INFO - Processing data for 2026-01-19\n", + "2026-02-12 19:22:12,162 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-01-20.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=01/day=20\n", + "2026-02-12 19:22:12,338 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-01-21.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=01/day=21\n", + "2026-02-12 19:22:12,504 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-01-20.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=01/day=20\n", + "2026-02-12 19:22:12,587 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-01-23.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=01/day=23\n", + "2026-02-12 19:22:12,875 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-01-15.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=01/day=15\n", + "2026-02-12 19:22:13,292 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-01-18.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=01/day=18\n", + "2026-02-12 19:22:13,501 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-01-18.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=01/day=18\n", + "2026-02-12 19:22:13,513 - INFO - Processing data for 2026-01-25\n", + "2026-02-12 19:22:13,514 - INFO - Processing data for 2026-01-26\n", + "2026-02-12 19:22:13,516 - INFO - Processing data for 2026-01-25\n", + "2026-02-12 19:22:13,517 - INFO - Processing data for 2026-01-27\n", + "2026-02-12 19:22:13,519 - INFO - Processing data for 2026-01-19\n", + "2026-02-12 19:22:13,520 - INFO - Processing data for 2026-01-19\n", + "2026-02-12 19:22:13,522 - INFO - Processing data for 2026-01-20\n", + "2026-02-12 19:22:13,883 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-01-14.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=01/day=14\n", + "2026-02-12 19:22:13,910 - INFO - Processing data for 2026-01-21\n", + "2026-02-12 19:22:14,152 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-01-17.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=01/day=17\n", + "2026-02-12 19:22:14,291 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-01-17.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=01/day=17\n", + "2026-02-12 19:22:14,550 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-01-18.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=01/day=18\n", + "2026-02-12 19:22:14,755 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-01-15.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=01/day=15\n", + "2026-02-12 19:22:15,050 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-01-17.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=01/day=17\n", + "2026-02-12 19:22:15,352 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-01-18.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=01/day=18\n", + "2026-02-12 19:22:15,691 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-01-16.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=01/day=16\n", + "2026-02-12 19:22:15,710 - INFO - Processing data for 2026-01-19\n", + "2026-02-12 19:22:15,712 - INFO - Processing data for 2026-01-21\n", + "2026-02-12 19:22:15,714 - INFO - Processing data for 2026-01-20\n", + "2026-02-12 19:22:15,715 - INFO - Processing data for 2026-01-22\n", + "2026-02-12 19:22:15,716 - INFO - Processing data for 2026-01-20\n", + "2026-02-12 19:22:15,718 - INFO - Processing data for 2026-01-19\n", + "2026-02-12 19:22:15,719 - INFO - Processing data for 2026-01-21\n", + "2026-02-12 19:22:15,878 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-01-17.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=01/day=17\n", + "2026-02-12 19:22:15,891 - INFO - Processing data for 2026-01-22\n", + "2026-02-12 19:22:16,111 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-01-23.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=01/day=23\n", + "2026-02-12 19:22:16,235 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-01-21.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=01/day=21\n", + "2026-02-12 19:22:16,348 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-01-22.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=01/day=22\n", + "2026-02-12 19:22:16,639 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-01-17.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=01/day=17\n", + "2026-02-12 19:22:16,736 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-01-22.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=01/day=22\n", + "2026-02-12 19:22:17,023 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-01-16.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=01/day=16\n", + "2026-02-12 19:22:17,325 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-01-17.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=01/day=17\n", + "2026-02-12 19:22:17,568 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-01-16.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=01/day=16\n", + "2026-02-12 19:22:17,905 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-01-16.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=01/day=16\n", + "2026-02-12 19:22:17,923 - INFO - Processing data for 2026-01-26\n", + "2026-02-12 19:22:17,924 - INFO - Processing data for 2026-01-27\n", + "2026-02-12 19:22:17,926 - INFO - Processing data for 2026-01-28\n", + "2026-02-12 19:22:17,927 - INFO - Processing data for 2026-01-20\n", + "2026-02-12 19:22:17,929 - INFO - Processing data for 2026-01-28\n", + "2026-02-12 19:22:17,931 - INFO - Processing data for 2026-01-21\n", + "2026-02-12 19:22:17,932 - INFO - Processing data for 2026-01-20\n", + "2026-02-12 19:22:17,934 - INFO - Processing data for 2026-01-23\n", + "2026-02-12 19:22:17,935 - INFO - Processing data for 2026-01-21\n", + "2026-02-12 19:22:18,574 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-01-17.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=01/day=17\n", + "2026-02-12 19:22:18,610 - INFO - Processing data for 2026-01-19\n", + "2026-02-12 19:22:18,697 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-01-24.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=01/day=24\n", + "2026-02-12 19:22:19,008 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-01-19.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=01/day=19\n", + "2026-02-12 19:22:19,306 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-01-15.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=01/day=15\n", + "2026-02-12 19:22:19,695 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-01-14.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=01/day=14\n", + "2026-02-12 19:22:20,201 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-01-15.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=01/day=15\n", + "2026-02-12 19:22:20,574 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-01-15.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=01/day=15\n", + "2026-02-12 19:22:20,951 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-01-15.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=01/day=15\n", + "2026-02-12 19:22:21,001 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-01-24.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=01/day=24\n", + "2026-02-12 19:22:21,021 - INFO - Processing data for 2026-01-29\n", + "2026-02-12 19:22:21,025 - INFO - Processing data for 2026-01-23\n", + "2026-02-12 19:22:21,030 - INFO - Processing data for 2026-01-22\n", + "2026-02-12 19:22:21,035 - INFO - Processing data for 2026-01-23\n", + "2026-02-12 19:22:21,038 - INFO - Processing data for 2026-01-21\n", + "2026-02-12 19:22:21,040 - INFO - Processing data for 2026-01-22\n", + "2026-02-12 19:22:21,042 - INFO - Processing data for 2026-01-22\n", + "2026-02-12 19:22:21,043 - INFO - Processing data for 2026-01-29\n", + "2026-02-12 19:22:21,389 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-01-14.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=01/day=14\n", + "2026-02-12 19:22:21,741 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-01-20.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=01/day=20\n", + "2026-02-12 19:22:21,756 - INFO - Processing data for 2026-01-24\n", + "2026-02-12 19:22:21,758 - INFO - Processing data for 2026-01-25\n", + "2026-02-12 19:22:22,386 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-01-17.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=01/day=17\n", + "2026-02-12 19:22:22,413 - INFO - Processing data for 2026-01-19\n", + "2026-02-12 19:22:22,942 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-01-16.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=01/day=16\n", + "2026-02-12 19:22:23,560 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-01-18.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=01/day=18\n", + "2026-02-12 19:22:23,595 - INFO - Processing data for 2026-01-22\n", + "2026-02-12 19:22:23,597 - INFO - Processing data for 2026-01-20\n", + "2026-02-12 19:22:23,677 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-01-27.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=01/day=27\n", + "2026-02-12 19:22:24,114 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-01-14.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=01/day=14\n", + "2026-02-12 19:22:24,138 - INFO - Processing data for 2026-01-30\n", + "2026-02-12 19:22:24,140 - INFO - Processing data for 2026-01-23\n", + "2026-02-12 19:22:24,266 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-01-25.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=01/day=25\n", + "2026-02-12 19:22:24,279 - INFO - Processing data for 2026-01-31\n", + "2026-02-12 19:22:24,402 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-01-25.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=01/day=25\n", + "2026-02-12 19:22:24,416 - INFO - Processing data for 2026-01-30\n", + "2026-02-12 19:22:24,493 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-01-26.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=01/day=26\n", + "2026-02-12 19:22:24,891 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-01-14.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=01/day=14\n", + "2026-02-12 19:22:24,913 - INFO - Processing data for 2026-02-01\n", + "2026-02-12 19:22:24,916 - INFO - Processing data for 2026-01-23\n", + "2026-02-12 19:22:25,207 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-01-19.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=01/day=19\n", + "2026-02-12 19:22:25,557 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-01-20.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=01/day=20\n", + "2026-02-12 19:22:26,036 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-01-19.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=01/day=19\n", + "2026-02-12 19:22:26,060 - INFO - Processing data for 2026-01-24\n", + "2026-02-12 19:22:26,061 - INFO - Processing data for 2026-01-25\n", + "2026-02-12 19:22:26,063 - INFO - Processing data for 2026-01-24\n", + "2026-02-12 19:22:26,458 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-01-21.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=01/day=21\n", + "2026-02-12 19:22:26,678 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-01-22.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=01/day=22\n", + "2026-02-12 19:22:26,697 - INFO - Processing data for 2026-01-26\n", + "2026-02-12 19:22:26,699 - INFO - Processing data for 2026-01-26\n", + "2026-02-12 19:22:27,513 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-01-18.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=01/day=18\n", + "2026-02-12 19:22:27,541 - INFO - Processing data for 2026-01-20\n", + "2026-02-12 19:22:28,318 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-01-14.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=01/day=14\n", + "2026-02-12 19:22:28,351 - INFO - Processing data for 2026-01-23\n", + "2026-02-12 19:22:28,572 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-01-22.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=01/day=22\n", + "2026-02-12 19:22:28,904 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-01-21.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=01/day=21\n", + "2026-02-12 19:22:28,925 - INFO - Processing data for 2026-01-27\n", + "2026-02-12 19:22:28,928 - INFO - Processing data for 2026-01-27\n", + "2026-02-12 19:22:29,055 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-01-26.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=01/day=26\n", + "2026-02-12 19:22:29,128 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-01-28.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=01/day=28\n", + "2026-02-12 19:22:29,211 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-01-27.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=01/day=27\n", + "2026-02-12 19:22:29,298 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-01-28.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=01/day=28\n", + "2026-02-12 19:22:29,602 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-01-20.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=01/day=20\n", + "2026-02-12 19:22:30,032 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-01-19.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=01/day=19\n", + "2026-02-12 19:22:30,051 - INFO - Processing data for 2026-01-31\n", + "2026-02-12 19:22:30,054 - INFO - Processing data for 2026-02-02\n", + "2026-02-12 19:22:30,056 - INFO - Processing data for 2026-02-01\n", + "2026-02-12 19:22:30,059 - INFO - Processing data for 2026-02-02\n", + "2026-02-12 19:22:30,061 - INFO - Processing data for 2026-01-24\n", + "2026-02-12 19:22:30,062 - INFO - Processing data for 2026-01-24\n", + "2026-02-12 19:22:30,418 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-01-23.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=01/day=23\n", + "2026-02-12 19:22:30,895 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-01-19.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=01/day=19\n", + "2026-02-12 19:22:30,915 - INFO - Processing data for 2026-01-28\n", + "2026-02-12 19:22:30,917 - INFO - Processing data for 2026-01-25\n", + "2026-02-12 19:22:31,106 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-01-29.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=01/day=29\n", + "2026-02-12 19:22:31,247 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-01-29.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=01/day=29\n", + "2026-02-12 19:22:31,395 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-01-24.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=01/day=24\n", + "2026-02-12 19:22:31,410 - INFO - Processing data for 2026-02-03\n", + "2026-02-12 19:22:31,412 - INFO - Processing data for 2026-02-03\n", + "2026-02-12 19:22:31,415 - INFO - Processing data for 2026-01-28\n", + "2026-02-12 19:22:31,606 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-01-23.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=01/day=23\n", + "2026-02-12 19:22:32,068 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-01-20.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=01/day=20\n", + "2026-02-12 19:22:32,098 - INFO - Processing data for 2026-01-29\n", + "2026-02-12 19:22:32,100 - INFO - Processing data for 2026-01-25\n", + "2026-02-12 19:22:32,343 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-01-25.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=01/day=25\n", + "2026-02-12 19:22:32,828 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-01-19.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=01/day=19\n", + "2026-02-12 19:22:33,407 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-01-21.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=01/day=21\n", + "2026-02-12 19:22:33,437 - INFO - Processing data for 2026-01-30\n", + "2026-02-12 19:22:33,441 - INFO - Processing data for 2026-01-24\n", + "2026-02-12 19:22:33,446 - INFO - Processing data for 2026-01-26\n", + "2026-02-12 19:22:34,840 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-01-16.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=01/day=16\n", + "2026-02-12 19:22:35,133 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-01-22.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=01/day=22\n", + "2026-02-12 19:22:35,152 - INFO - Processing data for 2026-01-21\n", + "2026-02-12 19:22:35,156 - INFO - Processing data for 2026-01-26\n", + "2026-02-12 19:22:35,443 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-01-23.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=01/day=23\n", + "2026-02-12 19:22:35,463 - INFO - Processing data for 2026-01-27\n", + "2026-02-12 19:22:35,829 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-01-22.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=01/day=22\n", + "2026-02-12 19:22:36,270 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-01-22.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=01/day=22\n", + "2026-02-12 19:22:36,708 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-01-21.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=01/day=21\n", + "2026-02-12 19:22:37,168 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-01-20.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=01/day=20\n", + "2026-02-12 19:22:37,179 - INFO - Processing data for 2026-01-27\n", + "2026-02-12 19:22:38,247 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-01-16.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=01/day=16\n", + "2026-02-12 19:22:38,283 - INFO - Processing data for 2026-01-25\n", + "2026-02-12 19:22:38,285 - INFO - Processing data for 2026-01-28\n", + "2026-02-12 19:22:38,287 - INFO - Processing data for 2026-01-26\n", + "2026-02-12 19:22:38,289 - INFO - Processing data for 2026-01-21\n", + "2026-02-12 19:22:38,950 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-01-21.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=01/day=21\n", + "2026-02-12 19:22:39,528 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-01-20.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=01/day=20\n", + "2026-02-12 19:22:40,025 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-01-21.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=01/day=21\n", + "2026-02-12 19:22:40,164 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-01-30.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=01/day=30\n", + "2026-02-12 19:22:40,169 - INFO - Processing data for 2026-01-27\n", + "2026-02-12 19:22:40,405 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-01-30.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=01/day=30\n", + "2026-02-12 19:22:40,421 - INFO - Processing data for 2026-01-25\n", + "2026-02-12 19:22:40,423 - INFO - Processing data for 2026-01-26\n", + "2026-02-12 19:22:40,425 - INFO - Processing data for 2026-02-04\n", + "2026-02-12 19:22:40,431 - INFO - Processing data for 2026-02-04\n", + "2026-02-12 19:22:40,617 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-01-24.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=01/day=24\n", + "2026-02-12 19:22:40,860 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-02-01.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=02/day=01\n", + "2026-02-12 19:22:41,115 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-01-31.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=01/day=31\n", + "2026-02-12 19:22:41,278 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-01-24.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=01/day=24\n", + "2026-02-12 19:22:41,292 - INFO - Processing data for 2026-01-28\n", + "2026-02-12 19:22:41,297 - INFO - Processing data for 2026-02-05\n", + "2026-02-12 19:22:41,301 - INFO - Processing data for 2026-02-06\n", + "2026-02-12 19:22:41,303 - INFO - Processing data for 2026-01-29\n", + "2026-02-12 19:22:41,728 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-01-22.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=01/day=22\n", + "2026-02-12 19:22:41,769 - INFO - Processing data for 2026-01-27\n", + "2026-02-12 19:22:42,083 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-01-26.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=01/day=26\n", + "2026-02-12 19:22:42,329 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-01-26.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=01/day=26\n", + "2026-02-12 19:22:42,558 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-01-25.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=01/day=25\n", + "2026-02-12 19:22:42,933 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-01-23.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=01/day=23\n", + "2026-02-12 19:22:44,627 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-01-15.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=01/day=15\n", + "2026-02-12 19:22:44,684 - INFO - Processing data for 2026-01-31\n", + "2026-02-12 19:22:44,686 - INFO - Processing data for 2026-01-30\n", + "2026-02-12 19:22:44,688 - INFO - Processing data for 2026-01-31\n", + "2026-02-12 19:22:44,690 - INFO - Processing data for 2026-01-28\n", + "2026-02-12 19:22:44,695 - INFO - Processing data for 2026-01-22\n", + "2026-02-12 19:22:45,082 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-01-23.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=01/day=23\n", + "2026-02-12 19:22:45,108 - INFO - Processing data for 2026-01-29\n", + "2026-02-12 19:22:47,075 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-01-14.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=01/day=14\n", + "2026-02-12 19:22:47,323 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-01-27.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=01/day=27\n", + "2026-02-12 19:22:47,330 - INFO - Processing data for 2026-01-23\n", + "2026-02-12 19:22:47,342 - INFO - Processing data for 2026-02-01\n", + "2026-02-12 19:22:47,666 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-01-27.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=01/day=27\n", + "2026-02-12 19:22:47,796 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-02-01.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=02/day=01\n", + "2026-02-12 19:22:47,812 - INFO - Processing data for 2026-02-01\n", + "2026-02-12 19:22:47,814 - INFO - Processing data for 2026-02-05\n", + "2026-02-12 19:22:47,968 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-02-02.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=02/day=02\n", + "2026-02-12 19:22:48,101 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-01-28.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=01/day=28\n", + "2026-02-12 19:22:48,287 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-02-03.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=02/day=03\n", + "2026-02-12 19:22:48,478 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-01-31.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=01/day=31\n", + "2026-02-12 19:22:48,667 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-01-24.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=01/day=24\n", + "2026-02-12 19:22:48,806 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-02-03.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=02/day=03\n", + "2026-02-12 19:22:48,978 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-01-28.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=01/day=28\n", + "2026-02-12 19:22:49,235 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-02-02.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=02/day=02\n", + "2026-02-12 19:22:49,252 - INFO - Processing data for 2026-02-06\n", + "2026-02-12 19:22:49,254 - INFO - Processing data for 2026-02-02\n", + "2026-02-12 19:22:49,256 - INFO - Processing data for 2026-02-07\n", + "2026-02-12 19:22:49,258 - INFO - Processing data for 2026-02-07\n", + "2026-02-12 19:22:49,261 - INFO - Processing data for 2026-01-29\n", + "2026-02-12 19:22:49,263 - INFO - Processing data for 2026-02-08\n", + "2026-02-12 19:22:49,265 - INFO - Processing data for 2026-02-02\n", + "2026-02-12 19:22:49,270 - INFO - Processing data for 2026-02-08\n", + "2026-02-12 19:22:49,590 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-01-24.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=01/day=24\n", + "2026-02-12 19:22:49,616 - INFO - Processing data for 2026-01-28\n", + "2026-02-12 19:22:49,913 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-01-25.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=01/day=25\n", + "2026-02-12 19:22:50,467 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-01-23.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=01/day=23\n", + "2026-02-12 19:22:50,478 - INFO - Processing data for 2026-01-30\n", + "2026-02-12 19:22:50,712 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-01-29.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=01/day=29\n", + "2026-02-12 19:22:50,739 - INFO - Processing data for 2026-01-29\n", + "2026-02-12 19:22:50,742 - INFO - Processing data for 2026-02-03\n", + "2026-02-12 19:22:51,026 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-01-30.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=01/day=30\n", + "2026-02-12 19:22:51,282 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-01-24.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=01/day=24\n", + "2026-02-12 19:22:51,304 - INFO - Processing data for 2026-02-04\n", + "2026-02-12 19:22:51,306 - INFO - Processing data for 2026-01-29\n", + "2026-02-12 19:22:51,797 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-01-25.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=01/day=25\n", + "2026-02-12 19:22:51,821 - INFO - Processing data for 2026-01-30\n", + "2026-02-12 19:22:52,253 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-01-26.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=01/day=26\n", + "2026-02-12 19:22:52,528 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-01-26.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=01/day=26\n", + "2026-02-12 19:22:52,553 - INFO - Processing data for 2026-01-31\n", + "2026-02-12 19:22:52,554 - INFO - Processing data for 2026-01-31\n", + "2026-02-12 19:22:52,876 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-01-27.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=01/day=27\n", + "2026-02-12 19:22:52,893 - INFO - Processing data for 2026-02-01\n", + "2026-02-12 19:22:54,346 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-01-19.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=01/day=19\n", + "2026-02-12 19:22:56,356 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-01-15.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=01/day=15\n", + "2026-02-12 19:22:56,417 - INFO - Processing data for 2026-01-24\n", + "2026-02-12 19:22:56,918 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-01-27.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=01/day=27\n", + "2026-02-12 19:22:57,052 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-02-04.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=02/day=04\n", + "2026-02-12 19:22:57,056 - INFO - Processing data for 2026-01-22\n", + "2026-02-12 19:22:57,074 - INFO - Processing data for 2026-02-01\n", + "2026-02-12 19:22:57,077 - INFO - Processing data for 2026-02-09\n", + "2026-02-12 19:22:57,370 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-02-04.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=02/day=04\n", + "2026-02-12 19:22:57,640 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-01-28.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=01/day=28\n", + "2026-02-12 19:22:57,658 - INFO - Processing data for 2026-02-09\n", + "2026-02-12 19:22:57,660 - INFO - Processing data for 2026-02-02\n", + "2026-02-12 19:22:57,944 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-01-29.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=01/day=29\n", + "2026-02-12 19:22:57,978 - INFO - Processing data for 2026-02-03\n", + "2026-02-12 19:23:00,209 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-01-14.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=01/day=14\n", + "2026-02-12 19:23:00,621 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-01-27.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=01/day=27\n", + "2026-02-12 19:23:00,640 - INFO - Processing data for 2026-01-23\n", + "2026-02-12 19:23:00,642 - INFO - Processing data for 2026-01-30\n", + "2026-02-12 19:23:00,993 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-02-06.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=02/day=06\n", + "2026-02-12 19:23:01,894 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-01-25.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=01/day=25\n", + "2026-02-12 19:23:02,545 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-01-28.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=01/day=28\n", + "2026-02-12 19:23:02,566 - INFO - Processing data for 2026-02-10\n", + "2026-02-12 19:23:02,567 - INFO - Processing data for 2026-01-31\n", + "2026-02-12 19:23:02,569 - INFO - Processing data for 2026-02-02\n", + "2026-02-12 19:23:02,864 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-01-30.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=01/day=30\n", + "2026-02-12 19:23:03,294 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-01-26.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=01/day=26\n", + "2026-02-12 19:23:03,311 - INFO - Processing data for 2026-02-04\n", + "2026-02-12 19:23:03,313 - INFO - Processing data for 2026-02-01\n", + "2026-02-12 19:23:03,757 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-01-31.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=01/day=31\n", + "2026-02-12 19:23:04,183 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-01-28.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=01/day=28\n", + "2026-02-12 19:23:04,523 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-01-31.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=01/day=31\n", + "2026-02-12 19:23:04,971 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-02-05.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=02/day=05\n", + "2026-02-12 19:23:05,065 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-02-08.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=02/day=08\n", + "2026-02-12 19:23:05,072 - INFO - Processing data for 2026-02-05\n", + "2026-02-12 19:23:05,460 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-01-27.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=01/day=27\n", + "2026-02-12 19:23:05,468 - INFO - Processing data for 2026-02-02\n", + "2026-02-12 19:23:05,470 - INFO - Processing data for 2026-02-05\n", + "2026-02-12 19:23:05,475 - INFO - Processing data for 2026-02-11\n", + "2026-02-12 19:23:05,488 - INFO - Processing data for 2026-02-10\n", + "2026-02-12 19:23:05,497 - INFO - Processing data for 2026-01-30\n", + "2026-02-12 19:23:05,632 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-02-07.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=02/day=07\n", + "2026-02-12 19:23:06,244 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-01-25.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=01/day=25\n", + "2026-02-12 19:23:06,711 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-01-26.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=01/day=26\n", + "2026-02-12 19:23:06,751 - INFO - Processing data for 2026-02-11\n", + "2026-02-12 19:23:06,940 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-02-06.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=02/day=06\n", + "2026-02-12 19:23:07,096 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-02-07.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=02/day=07\n", + "2026-02-12 19:23:07,101 - INFO - Processing data for 2026-01-31\n", + "2026-02-12 19:23:07,391 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-02-01.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=02/day=01\n", + "2026-02-12 19:23:07,494 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-02-08.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=02/day=08\n", + "2026-02-12 19:23:07,498 - INFO - Processing data for 2026-02-01\n", + "2026-02-12 19:23:07,715 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-02-05.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=02/day=05\n", + "2026-02-12 19:23:07,734 - INFO - Processing data for 2026-02-06\n", + "2026-02-12 19:23:08,067 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-02-01.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=02/day=01\n", + "2026-02-12 19:23:08,091 - INFO - Processing data for 2026-02-06\n", + "2026-02-12 19:23:08,392 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-02-02.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=02/day=02\n", + "2026-02-12 19:23:08,409 - INFO - Processing data for 2026-02-07\n", + "2026-02-12 19:23:08,959 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-01-29.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=01/day=29\n", + "2026-02-12 19:23:09,349 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-02-02.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=02/day=02\n", + "2026-02-12 19:23:09,368 - INFO - Processing data for 2026-02-03\n", + "2026-02-12 19:23:09,371 - INFO - Processing data for 2026-02-07\n", + "2026-02-12 19:23:10,964 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-01-19.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=01/day=19\n", + "2026-02-12 19:23:11,431 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-01-29.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=01/day=29\n", + "2026-02-12 19:23:11,705 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-02-03.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=02/day=03\n", + "2026-02-12 19:23:11,711 - INFO - Processing data for 2026-01-24\n", + "2026-02-12 19:23:11,729 - INFO - Processing data for 2026-02-03\n", + "2026-02-12 19:23:11,731 - INFO - Processing data for 2026-02-08\n", + "2026-02-12 19:23:12,047 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-02-04.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=02/day=04\n", + "2026-02-12 19:23:12,451 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-01-28.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=01/day=28\n", + "2026-02-12 19:23:12,476 - INFO - Processing data for 2026-02-09\n", + "2026-02-12 19:23:12,479 - INFO - Processing data for 2026-02-02\n", + "2026-02-12 19:23:12,643 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-02-09.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=02/day=09\n", + "2026-02-12 19:23:12,754 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-02-09.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=02/day=09\n", + "2026-02-12 19:23:13,223 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-01-30.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=01/day=30\n", + "2026-02-12 19:23:13,252 - INFO - Processing data for 2026-02-04\n", + "2026-02-12 19:23:13,958 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-01-29.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=01/day=29\n", + "2026-02-12 19:23:14,638 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-01-29.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=01/day=29\n", + "2026-02-12 19:23:14,659 - INFO - Processing data for 2026-02-03\n", + "2026-02-12 19:23:14,661 - INFO - Processing data for 2026-02-03\n", + "2026-02-12 19:23:16,732 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-01-20.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=01/day=20\n", + "2026-02-12 19:23:16,829 - INFO - Processing data for 2026-01-25\n", + "2026-02-12 19:23:16,948 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-02-10.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=02/day=10\n", + "2026-02-12 19:23:17,234 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-02-03.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=02/day=03\n", + "2026-02-12 19:23:17,638 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-01-24.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=01/day=24\n", + "2026-02-12 19:23:18,043 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-02-01.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=02/day=01\n", + "2026-02-12 19:23:18,070 - INFO - Processing data for 2026-02-08\n", + "2026-02-12 19:23:18,071 - INFO - Processing data for 2026-01-26\n", + "2026-02-12 19:23:18,074 - INFO - Processing data for 2026-02-05\n", + "2026-02-12 19:23:18,296 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-02-10.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=02/day=10\n", + "2026-02-12 19:23:18,381 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-02-11.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=02/day=11\n", + "2026-02-12 19:23:18,950 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-01-30.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=01/day=30\n", + "2026-02-12 19:23:19,505 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-01-31.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=01/day=31\n", + "2026-02-12 19:23:19,529 - INFO - Processing data for 2026-02-04\n", + "2026-02-12 19:23:19,530 - INFO - Processing data for 2026-02-06\n", + "2026-02-12 19:23:19,686 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-02-11.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=02/day=11\n", + "2026-02-12 19:23:19,953 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-02-04.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=02/day=04\n", + "2026-02-12 19:23:20,408 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-02-02.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=02/day=02\n", + "2026-02-12 19:23:20,427 - INFO - Processing data for 2026-02-09\n", + "2026-02-12 19:23:20,430 - INFO - Processing data for 2026-02-07\n", + "2026-02-12 19:23:21,292 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-01-31.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=01/day=31\n", + "2026-02-12 19:23:21,358 - INFO - Processing data for 2026-02-05\n", + "2026-02-12 19:23:21,828 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-02-05.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=02/day=05\n", + "2026-02-12 19:23:21,886 - INFO - Processing data for 2026-02-10\n", + "2026-02-12 19:23:22,188 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-02-07.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=02/day=07\n", + "2026-02-12 19:23:22,977 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-02-01.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=02/day=01\n", + "2026-02-12 19:23:23,012 - INFO - Processing data for 2026-02-11\n", + "2026-02-12 19:23:23,014 - INFO - Processing data for 2026-02-06\n", + "2026-02-12 19:23:23,199 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-02-08.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=02/day=08\n", + "2026-02-12 19:23:23,494 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-02-07.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=02/day=07\n", + "2026-02-12 19:23:23,962 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-02-05.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=02/day=05\n", + "2026-02-12 19:23:24,327 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-02-06.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=02/day=06\n", + "2026-02-12 19:23:24,352 - INFO - Processing data for 2026-02-10\n", + "2026-02-12 19:23:24,355 - INFO - Processing data for 2026-02-11\n", + "2026-02-12 19:23:24,604 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-02-09.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=02/day=09\n", + "2026-02-12 19:23:25,986 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-01-22.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=01/day=22\n", + "2026-02-12 19:23:26,676 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-01-30.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=01/day=30\n", + "2026-02-12 19:23:26,692 - INFO - Processing data for 2026-01-27\n", + "2026-02-12 19:23:27,308 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-02-01.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=02/day=01\n", + "2026-02-12 19:23:27,348 - INFO - Processing data for 2026-02-04\n", + "2026-02-12 19:23:27,350 - INFO - Processing data for 2026-02-05\n", + "2026-02-12 19:23:27,868 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-02-06.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=02/day=06\n", + "2026-02-12 19:23:28,707 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-01-31.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=01/day=31\n", + "2026-02-12 19:23:28,735 - INFO - Processing data for 2026-02-06\n", + "2026-02-12 19:23:29,481 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-02-03.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=02/day=03\n", + "2026-02-12 19:23:30,424 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-02-02.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=02/day=02\n", + "2026-02-12 19:23:31,625 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-01-30.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=01/day=30\n", + "2026-02-12 19:23:32,705 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-02-02.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=02/day=02\n", + "2026-02-12 19:23:33,364 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-02-01.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=02/day=01\n", + "2026-02-12 19:23:34,869 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-01-23.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=01/day=23\n", + "2026-02-12 19:23:34,929 - INFO - Processing data for 2026-02-08\n", + "2026-02-12 19:23:34,932 - INFO - Processing data for 2026-02-07\n", + "2026-02-12 19:23:34,934 - INFO - Processing data for 2026-02-04\n", + "2026-02-12 19:23:34,937 - INFO - Processing data for 2026-02-07\n", + "2026-02-12 19:23:34,939 - INFO - Processing data for 2026-02-05\n", + "2026-02-12 19:23:34,942 - INFO - Processing data for 2026-01-28\n", + "2026-02-12 19:23:35,444 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-01-24.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=01/day=24\n", + "2026-02-12 19:23:35,464 - INFO - Processing data for 2026-01-25\n", + "2026-02-12 19:23:35,905 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-02-04.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=02/day=04\n", + "2026-02-12 19:23:38,265 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-01-20.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=01/day=20\n", + "2026-02-12 19:23:38,455 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-02-08.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=02/day=08\n", + "2026-02-12 19:23:38,469 - INFO - Processing data for 2026-02-09\n", + "2026-02-12 19:23:38,470 - INFO - Processing data for 2026-01-26\n", + "2026-02-12 19:23:39,243 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-02-03.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=02/day=03\n", + "2026-02-12 19:23:39,270 - INFO - Processing data for 2026-02-08\n", + "2026-02-12 19:23:39,617 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-02-09.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=02/day=09\n", + "2026-02-12 19:23:40,314 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-02-02.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=02/day=02\n", + "2026-02-12 19:23:40,811 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-02-03.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=02/day=03\n", + "2026-02-12 19:23:40,957 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-02-10.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=02/day=10\n", + "2026-02-12 19:23:41,764 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-01-31.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=01/day=31\n", + "2026-02-12 19:23:41,793 - INFO - Processing data for 2026-02-06\n", + "2026-02-12 19:23:41,795 - INFO - Processing data for 2026-02-08\n", + "2026-02-12 19:23:41,796 - INFO - Processing data for 2026-02-07\n", + "2026-02-12 19:23:42,023 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-02-11.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=02/day=11\n", + "2026-02-12 19:23:42,845 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-02-03.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=02/day=03\n", + "2026-02-12 19:23:43,236 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-02-07.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=02/day=07\n", + "2026-02-12 19:23:43,421 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-02-10.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=02/day=10\n", + "2026-02-12 19:23:43,438 - INFO - Processing data for 2026-02-08\n", + "2026-02-12 19:23:43,440 - INFO - Processing data for 2026-02-10\n", + "2026-02-12 19:23:46,196 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-01-21.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=01/day=21\n", + "2026-02-12 19:23:46,261 - INFO - Processing data for 2026-01-29\n", + "2026-02-12 19:23:46,791 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-02-11.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=02/day=11\n", + "2026-02-12 19:23:48,225 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-02-05.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=02/day=05\n", + "2026-02-12 19:23:48,950 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-02-06.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=02/day=06\n", + "2026-02-12 19:23:48,980 - INFO - Processing data for 2026-02-11\n", + "2026-02-12 19:23:49,919 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-02-04.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=02/day=04\n", + "2026-02-12 19:23:49,952 - INFO - Processing data for 2026-02-09\n", + "2026-02-12 19:23:50,435 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-02-08.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=02/day=08\n", + "2026-02-12 19:23:53,574 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-01-21.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=01/day=21\n", + "2026-02-12 19:23:55,460 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-01-22.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=01/day=22\n", + "2026-02-12 19:23:55,549 - INFO - Processing data for 2026-01-27\n", + "2026-02-12 19:23:55,551 - INFO - Processing data for 2026-01-28\n", + "2026-02-12 19:23:55,986 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-02-09.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=02/day=09\n", + "2026-02-12 19:23:56,637 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-02-04.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=02/day=04\n", + "2026-02-12 19:23:57,967 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-01-25.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=01/day=25\n", + "2026-02-12 19:23:58,012 - INFO - Processing data for 2026-02-09\n", + "2026-02-12 19:23:59,919 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-01-23.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=01/day=23\n", + "2026-02-12 19:23:59,965 - INFO - Processing data for 2026-01-30\n", + "2026-02-12 19:23:59,981 - INFO - Processing data for 2026-01-29\n", + "2026-02-12 19:24:00,604 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-02-07.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=02/day=07\n", + "2026-02-12 19:24:00,627 - INFO - Processing data for 2026-02-10\n", + "2026-02-12 19:24:01,077 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-02-10.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=02/day=10\n", + "2026-02-12 19:24:01,527 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-02-08.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=02/day=08\n", + "2026-02-12 19:24:01,543 - INFO - Processing data for 2026-02-11\n", + "2026-02-12 19:24:02,515 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-02-07.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=02/day=07\n", + "2026-02-12 19:24:02,547 - INFO - Processing data for 2026-02-10\n", + "2026-02-12 19:24:04,128 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-01-26.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=01/day=26\n", + "2026-02-12 19:24:04,691 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-02-08.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=02/day=08\n", + "2026-02-12 19:24:04,705 - INFO - Processing data for 2026-01-31\n", + "2026-02-12 19:24:05,180 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-02-08.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=02/day=08\n", + "2026-02-12 19:24:05,208 - INFO - Processing data for 2026-02-11\n", + "2026-02-12 19:24:05,213 - INFO - Processing data for 2026-02-09\n", + "2026-02-12 19:24:06,839 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-02-06.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=02/day=06\n", + "2026-02-12 19:24:07,562 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-02-07.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=02/day=07\n", + "2026-02-12 19:24:07,926 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-02-11.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=02/day=11\n", + "2026-02-12 19:24:07,935 - INFO - Processing data for 2026-02-10\n", + "2026-02-12 19:24:08,659 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-02-04.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=02/day=04\n", + "2026-02-12 19:24:08,685 - INFO - Processing data for 2026-02-11\n", + "2026-02-12 19:24:10,037 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-01-27.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=01/day=27\n", + "2026-02-12 19:24:10,077 - INFO - Processing data for 2026-02-01\n", + "2026-02-12 19:24:11,309 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-02-05.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=02/day=05\n", + "2026-02-12 19:24:12,838 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-02-05.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=02/day=05\n", + "2026-02-12 19:24:13,481 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-02-09.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=02/day=09\n", + "2026-02-12 19:24:15,150 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-02-05.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=02/day=05\n", + "2026-02-12 19:24:16,930 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-02-06.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=02/day=06\n", + "2026-02-12 19:24:17,630 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-02-09.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=02/day=09\n", + "2026-02-12 19:24:18,697 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-02-06.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=02/day=06\n", + "2026-02-12 19:24:19,195 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-02-10.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=02/day=10\n", + "2026-02-12 19:24:20,992 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-01-25.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=01/day=25\n", + "2026-02-12 19:24:21,033 - INFO - Processing data for 2026-01-30\n", + "2026-02-12 19:24:21,509 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-02-10.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=02/day=10\n", + "2026-02-12 19:24:23,076 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-01-28.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=01/day=28\n", + "2026-02-12 19:24:23,115 - INFO - Processing data for 2026-02-02\n", + "2026-02-12 19:24:23,868 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-02-11.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=02/day=11\n", + "2026-02-12 19:24:24,384 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-02-11.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=02/day=11\n", + "2026-02-12 19:24:24,898 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-02-09.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=02/day=09\n", + "2026-02-12 19:24:25,359 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-02-10.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=02/day=10\n", + "2026-02-12 19:24:26,229 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-02-11.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=02/day=11\n", + "2026-02-12 19:24:28,515 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-01-26.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=01/day=26\n", + "2026-02-12 19:24:28,589 - INFO - Processing data for 2026-01-31\n", + "2026-02-12 19:24:30,315 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-01-28.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=01/day=28\n", + "2026-02-12 19:24:30,356 - INFO - Processing data for 2026-02-01\n", + "2026-02-12 19:24:32,527 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-01-29.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=01/day=29\n", + "2026-02-12 19:24:32,582 - INFO - Processing data for 2026-02-03\n", + "2026-02-12 19:24:34,467 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-01-27.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=01/day=27\n", + "2026-02-12 19:24:34,530 - INFO - Processing data for 2026-02-02\n", + "2026-02-12 19:24:37,668 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-01-29.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=01/day=29\n", + "2026-02-12 19:24:37,736 - INFO - Processing data for 2026-02-03\n", + "2026-02-12 19:24:41,734 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-01-30.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=01/day=30\n", + "2026-02-12 19:24:41,805 - INFO - Processing data for 2026-02-04\n", + "2026-02-12 19:24:45,114 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-02-01.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=02/day=01\n", + "2026-02-12 19:24:45,189 - INFO - Processing data for 2026-02-05\n", + "2026-02-12 19:24:49,059 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-01-31.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=01/day=31\n", + "2026-02-12 19:24:49,164 - INFO - Processing data for 2026-02-06\n", + "2026-02-12 19:24:57,912 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-01-30.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=01/day=30\n", + "2026-02-12 19:24:58,029 - INFO - Processing data for 2026-02-04\n", + "2026-02-12 19:25:04,390 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-02-01.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=02/day=01\n", + "2026-02-12 19:25:04,518 - INFO - Processing data for 2026-02-05\n", + "2026-02-12 19:25:08,908 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-02-04.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=02/day=04\n", + "2026-02-12 19:25:09,014 - INFO - Processing data for 2026-02-07\n", + "2026-02-12 19:25:13,862 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-02-03.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=02/day=03\n", + "2026-02-12 19:25:13,975 - INFO - Processing data for 2026-02-08\n", + "2026-02-12 19:25:19,822 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-02-02.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=02/day=02\n", + "2026-02-12 19:25:19,950 - INFO - Processing data for 2026-02-09\n", + "2026-02-12 19:25:25,728 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-01-31.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=01/day=31\n", + "2026-02-12 19:25:25,887 - INFO - Processing data for 2026-02-06\n", + "2026-02-12 19:25:33,790 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-02-03.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=02/day=03\n", + "2026-02-12 19:25:33,904 - INFO - Processing data for 2026-02-07\n", + "2026-02-12 19:25:43,716 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-02-06.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=02/day=06\n", + "2026-02-12 19:25:43,912 - INFO - Processing data for 2026-02-10\n", + "2026-02-12 19:25:46,978 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-02-08.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=02/day=08\n", + "2026-02-12 19:25:47,041 - INFO - Processing data for 2026-02-11\n", + "2026-02-12 19:25:52,879 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-02-07.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=02/day=07\n", + "2026-02-12 19:26:00,082 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-02-02.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=02/day=02\n", + "2026-02-12 19:26:00,261 - INFO - Processing data for 2026-02-08\n", + "2026-02-12 19:26:05,940 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-02-10.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=02/day=10\n", + "2026-02-12 19:26:12,079 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-02-04.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=02/day=04\n", + "2026-02-12 19:26:12,207 - INFO - Processing data for 2026-02-09\n", + "2026-02-12 19:26:22,562 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-02-05.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=02/day=05\n", + "2026-02-12 19:26:26,761 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-02-09.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=02/day=09\n", + "2026-02-12 19:26:32,990 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-02-07.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=02/day=07\n", + "2026-02-12 19:26:33,187 - INFO - Processing data for 2026-02-10\n", + "2026-02-12 19:26:41,338 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-02-06.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=02/day=06\n", + "2026-02-12 19:26:41,561 - INFO - Processing data for 2026-02-11\n", + "2026-02-12 19:26:43,832 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-02-08.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=02/day=08\n", + "2026-02-12 19:26:47,792 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-02-11.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=02/day=11\n", + "2026-02-12 19:27:00,270 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-02-05.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=02/day=05\n", + "2026-02-12 19:27:04,009 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-02-11.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=02/day=11\n", + "2026-02-12 19:27:07,094 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-02-10.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=02/day=10\n", + "2026-02-12 19:27:10,158 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-02-09.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=02/day=09\n", + "2026-02-12 19:27:59,836 - INFO - Loaded 411051 bars for 10 symbols\n" + ] }, - "nbformat": 4, - "nbformat_minor": 4 + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Loaded 411,051 bars\n", + "Symbols: ['ADAUSDT', 'AVAXUSDT', 'BNBUSDT', 'BTCUSDT', 'CAKEUSDT', 'DOGEUSDT', 'DOTUSDT', 'ETHUSDT', 'SOLUSDT', 'XRPUSDT']\n", + "Columns: ['symbol', 'start_time', 'end_time', 'open', 'high', 'low', 'close', 'volume', 'vwap', 'num_buyer', 'num_seller', 'num_buyer_volume', 'num_seller_volume']\n" + ] + } + ], + "source": [ + "SYMBOLS = [\n", + " \"BTCUSDT\", \"ETHUSDT\", \"BNBUSDT\", \"SOLUSDT\", \"XRPUSDT\",\n", + " \"DOGEUSDT\", \"ADAUSDT\", \"AVAXUSDT\", \"DOTUSDT\", \"CAKEUSDT\",\n", + "]\n", + "\n", + "loader = BinanceDataLoader()\n", + "\n", + "agg = loader.load_aggbar(\n", + " symbols=SYMBOLS,\n", + " data_type=\"aggTrades\",\n", + " market_type=\"futures\",\n", + " futures_type=\"um\",\n", + " days=30,\n", + " bar_type=\"time\",\n", + " interval=60_000, # 1-minute bars\n", + ")\n", + "\n", + "print(f\"Loaded {len(agg):,} bars\")\n", + "print(f\"Symbols: {agg.symbols}\")\n", + "print(f\"Columns: {agg.cols}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Exploring the AggBar\n", + "\n", + "`AggBar` is a **multi-symbol OHLCV container** stored in long format. You can inspect it, convert to Polars/Pandas, and slice by time or symbols." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "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", + " \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", + "
num_kbarstart_timeend_timenum_nan
symbol
ADAUSDT411062026-01-14 11:22:002026-02-120
AVAXUSDT411062026-01-14 11:22:002026-02-120
BNBUSDT411062026-01-14 11:22:002026-02-120
BTCUSDT411062026-01-14 11:22:002026-02-120
CAKEUSDT410972026-01-14 11:22:002026-02-120
DOGEUSDT411062026-01-14 11:22:002026-02-120
DOTUSDT411062026-01-14 11:22:002026-02-120
ETHUSDT411062026-01-14 11:22:002026-02-120
SOLUSDT411062026-01-14 11:22:002026-02-120
XRPUSDT411062026-01-14 11:22:002026-02-120
\n", + "
" + ], + "text/plain": [ + " num_kbar start_time end_time num_nan\n", + "symbol \n", + "ADAUSDT 41106 2026-01-14 11:22:00 2026-02-12 0\n", + "AVAXUSDT 41106 2026-01-14 11:22:00 2026-02-12 0\n", + "BNBUSDT 41106 2026-01-14 11:22:00 2026-02-12 0\n", + "BTCUSDT 41106 2026-01-14 11:22:00 2026-02-12 0\n", + "CAKEUSDT 41097 2026-01-14 11:22:00 2026-02-12 0\n", + "DOGEUSDT 41106 2026-01-14 11:22:00 2026-02-12 0\n", + "DOTUSDT 41106 2026-01-14 11:22:00 2026-02-12 0\n", + "ETHUSDT 41106 2026-01-14 11:22:00 2026-02-12 0\n", + "SOLUSDT 41106 2026-01-14 11:22:00 2026-02-12 0\n", + "XRPUSDT 41106 2026-01-14 11:22:00 2026-02-12 0" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Summary info per symbol\n", + "agg.info()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "shape: (10, 13)
symbolstart_timeend_timeopenhighlowclosevolumevwapnum_buyernum_sellernum_buyer_volumenum_seller_volume
stri64i64f64f64f64f64f64f64decimal[38,0]decimal[38,0]f64f64
"ADAUSDT"176838972000017683897800000.41630.41630.4160.4162422468.00.4160893450247651.0174817.0
"AVAXUSDT"1768389720000176838978000014.56714.56714.55414.5578092.014.55769420344859.03233.0
"BNBUSDT"17683897200001768389780000931.8931.8931.28931.56231.68931.5168789298141.7489.94
"BTCUSDT"1768389720000176838978000094951.594951.694892.694910.053.57894925.47540630556325.43828.14
"CAKEUSDT"176838972000017683897800002.03912.03912.03792.03814596.02.03859130283832.0764.0
"DOGEUSDT"176838972000017683897800000.146850.146850.146730.146792.758362e60.14678262431.479317e61.279045e6
"DOTUSDT"176838972000017683897800002.2572.2572.2522.25561027.12.255454262120337.840689.3
"ETHUSDT"176838972000017683897800003295.73296.03294.423295.612396.2643295.5500975285471155.6971240.567
"SOLUSDT"17683897200001768389780000144.03144.03143.82143.8828761.51143.91300614615517777.7410983.77
"XRPUSDT"176838972000017683897800002.12782.12792.12532.1261371037.52.12683196110193592.5177445.0
" + ], + "text/plain": [ + "shape: (10, 13)\n", + "\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n", + "\u2502 symbol \u2506 start_time \u2506 end_time \u2506 open \u2506 \u2026 \u2506 num_buyer \u2506 num_selle \u2506 num_buyer \u2506 num_selle \u2502\n", + "\u2502 --- \u2506 --- \u2506 --- \u2506 --- \u2506 \u2506 --- \u2506 r \u2506 _volume \u2506 r_volume \u2502\n", + "\u2502 str \u2506 i64 \u2506 i64 \u2506 f64 \u2506 \u2506 decimal[3 \u2506 --- \u2506 --- \u2506 --- \u2502\n", + "\u2502 \u2506 \u2506 \u2506 \u2506 \u2506 8,0] \u2506 decimal[3 \u2506 f64 \u2506 f64 \u2502\n", + "\u2502 \u2506 \u2506 \u2506 \u2506 \u2506 \u2506 8,0] \u2506 \u2506 \u2502\n", + "\u255e\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u256a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u256a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u256a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u256a\u2550\u2550\u2550\u256a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u256a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u256a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u256a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2561\n", + "\u2502 ADAUSDT \u2506 1768389720 \u2506 1768389780 \u2506 0.4163 \u2506 \u2026 \u2506 34 \u2506 50 \u2506 247651.0 \u2506 174817.0 \u2502\n", + "\u2502 \u2506 000 \u2506 000 \u2506 \u2506 \u2506 \u2506 \u2506 \u2506 \u2502\n", + "\u2502 AVAXUSDT \u2506 1768389720 \u2506 1768389780 \u2506 14.567 \u2506 \u2026 \u2506 20 \u2506 34 \u2506 4859.0 \u2506 3233.0 \u2502\n", + "\u2502 \u2506 000 \u2506 000 \u2506 \u2506 \u2506 \u2506 \u2506 \u2506 \u2502\n", + "\u2502 BNBUSDT \u2506 1768389720 \u2506 1768389780 \u2506 931.8 \u2506 \u2026 \u2506 92 \u2506 98 \u2506 141.74 \u2506 89.94 \u2502\n", + "\u2502 \u2506 000 \u2506 000 \u2506 \u2506 \u2506 \u2506 \u2506 \u2506 \u2502\n", + "\u2502 BTCUSDT \u2506 1768389720 \u2506 1768389780 \u2506 94951.5 \u2506 \u2026 \u2506 305 \u2506 563 \u2506 25.438 \u2506 28.14 \u2502\n", + "\u2502 \u2506 000 \u2506 000 \u2506 \u2506 \u2506 \u2506 \u2506 \u2506 \u2502\n", + "\u2502 CAKEUSDT \u2506 1768389720 \u2506 1768389780 \u2506 2.0391 \u2506 \u2026 \u2506 30 \u2506 28 \u2506 3832.0 \u2506 764.0 \u2502\n", + "\u2502 \u2506 000 \u2506 000 \u2506 \u2506 \u2506 \u2506 \u2506 \u2506 \u2502\n", + "\u2502 DOGEUSDT \u2506 1768389720 \u2506 1768389780 \u2506 0.14685 \u2506 \u2026 \u2506 62 \u2506 43 \u2506 1.479317e \u2506 1.279045e \u2502\n", + "\u2502 \u2506 000 \u2506 000 \u2506 \u2506 \u2506 \u2506 \u2506 6 \u2506 6 \u2502\n", + "\u2502 DOTUSDT \u2506 1768389720 \u2506 1768389780 \u2506 2.257 \u2506 \u2026 \u2506 26 \u2506 21 \u2506 20337.8 \u2506 40689.3 \u2502\n", + "\u2502 \u2506 000 \u2506 000 \u2506 \u2506 \u2506 \u2506 \u2506 \u2506 \u2502\n", + "\u2502 ETHUSDT \u2506 1768389720 \u2506 1768389780 \u2506 3295.7 \u2506 \u2026 \u2506 528 \u2506 547 \u2506 1155.697 \u2506 1240.567 \u2502\n", + "\u2502 \u2506 000 \u2506 000 \u2506 \u2506 \u2506 \u2506 \u2506 \u2506 \u2502\n", + "\u2502 SOLUSDT \u2506 1768389720 \u2506 1768389780 \u2506 144.03 \u2506 \u2026 \u2506 146 \u2506 155 \u2506 17777.74 \u2506 10983.77 \u2502\n", + "\u2502 \u2506 000 \u2506 000 \u2506 \u2506 \u2506 \u2506 \u2506 \u2506 \u2502\n", + "\u2502 XRPUSDT \u2506 1768389720 \u2506 1768389780 \u2506 2.1278 \u2506 \u2026 \u2506 96 \u2506 110 \u2506 193592.5 \u2506 177445.0 \u2502\n", + "\u2502 \u2506 000 \u2506 000 \u2506 \u2506 \u2506 \u2506 \u2506 \u2506 \u2502\n", + "\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# View raw data as Polars DataFrame\n", + "agg.to_polars().head(10)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sliced AggBar: ['BTCUSDT', 'ETHUSDT'], 82,212 bars\n" + ] + } + ], + "source": [ + "# Slice by symbols \u2014 creates a new AggBar\n", + "btc_eth = agg.slice(symbols=[\"BTCUSDT\", \"ETHUSDT\"])\n", + "print(f\"Sliced AggBar: {btc_eth.symbols}, {len(btc_eth):,} bars\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Building Momentum Factors\n", + "\n", + "### 3.1 Code-Based Factor Construction\n", + "\n", + "Extract a column from `AggBar` with `agg[\"close\"]` to get a `Factor` object, then chain time-series and cross-sectional operators." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Factor shapes:\n", + " momentum_60: 411,607 rows\n", + " vol_adj_momentum: 414,943 rows\n", + " momentum_rank: 411,607 rows\n" + ] + } + ], + "source": [ + "close = agg[\"close\"]\n", + "volume = agg[\"volume\"]\n", + "\n", + "# Simple momentum: percentage change over 60 periods\n", + "momentum_60 = close.ts_delta(60) / close.ts_shift(60)\n", + "momentum_60.name = \"momentum_60\"\n", + "\n", + "# Short-term momentum (20 periods)\n", + "momentum_20 = close.ts_delta(20) / close.ts_shift(20)\n", + "momentum_20.name = \"momentum_20\"\n", + "\n", + "# Volatility-adjusted momentum\n", + "volatility = (close.ts_delta(1) / close.ts_shift(1)).ts_std(60)\n", + "vol_adj_momentum = momentum_60 / volatility\n", + "vol_adj_momentum.name = \"vol_adj_momentum\"\n", + "\n", + "# Cross-sectional rank (normalized to [0, 1])\n", + "momentum_rank = momentum_60.cs_rank()\n", + "momentum_rank.name = \"momentum_rank\"\n", + "\n", + "print(\"Factor shapes:\")\n", + "print(f\" momentum_60: {len(momentum_60):,} rows\")\n", + "print(f\" vol_adj_momentum: {len(vol_adj_momentum):,} rows\")\n", + "print(f\" momentum_rank: {len(momentum_rank):,} rows\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 3.2 Expression-Based Factor Construction\n", + "\n", + "You can also define factors using string expressions via `ResearchSession.create_factor()`. This is useful for rapid experimentation." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Expression-based momentum: 411,607 rows\n", + "MA crossover signal: 411,607 rows\n" + ] + } + ], + "source": [ + "session = ResearchSession(agg, default_frequency=\"1min\")\n", + "\n", + "# Same momentum factor, defined via expression\n", + "mom_expr = session.create_factor(\n", + " \"ts_delta(close, 60) / ts_shift(close, 60)\",\n", + " name=\"momentum_60_expr\",\n", + ")\n", + "\n", + "# A more complex expression: MA crossover signal\n", + "ma_cross = session.create_factor(\n", + " \"ts_mean(close, 10) - ts_mean(close, 30)\",\n", + " name=\"ma_crossover\",\n", + ")\n", + "\n", + "print(f\"Expression-based momentum: {len(mom_expr):,} rows\")\n", + "print(f\"MA crossover signal: {len(ma_cross):,} rows\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Visualizing Factors\n", + "\n", + "Use the `.plot` accessor on Factor objects to produce time series, heatmaps, and distributions." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABKMAAAJOCAYAAABr8MR3AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzs3Xd4G1XWBvB3JLm3xOmkE0J6g5DQS4Cl7W4gLEvZXXpbFj76Upe29N4SOgESYEMgCSWhpZNOilPtFJe4d8uWrD4z3x+yNTPqtmVJjt/f8+SJrBmNrkajKWfOPVeQZVkGERERERERERFRFOhi3QAiIiIiIiIiIuo+GIwiIiIiIiIiIqKoYTCKiIiIiIiIiIiihsEoIiIiIiIiIiKKGgajiIiIiIiIiIgoahiMIiIiIiIiIiKiqGEwioiIiIiIiIiIoobBKCIiIiIiIiIiihoGo4iIiIiIiIiIKGoMsW4AERERRceDDz6IxYsXB51n2rRpuOSSS/DQQw9h2bJlGDFiRJRap5WTk4NPP/0UOTk5qKmpQUJCAkaMGIHLLrsMl19+eUTeY9SoUbjppptw3333RWR5FN/y8vLw/PPPIycnB8nJyTjjjDPw8MMPIysryzPP8uXLMWfOHBw8eBBpaWk455xz8OCDDyI9PT2GLSciIjryCLIsy7FuBBEREXU+k8kEm83m+fvxxx/H3r178fXXX3ueS0hIQHJyMkwmE7Kzs6HX66Pezs2bN+O6667DBRdcgGuuuQZ9+vRBXV0dFi9ejPnz5+PBBx/Edddd1+H3qampQWpqKtLS0iLQamr15ptvory8HM8//3ysm+JRUlKCWbNm4fzzz8cNN9yA6upqPPjggxg2bBg+/vhjAMCmTZtw3XXX4YYbbsBf//pXlJaW4sknn8TgwYPx4YcfxvgTEBERHVmYGUVERNRNZGRkICMjw/N3UlIS9Ho9+vTp4zNvcnJyNJum8eWXX6Jfv354+eWXIQgCAGDAgAEYP348bDYb9u7dG5H38fe5qeN27NiBfv36xboZGu+88w4GDRqEp556CoIgYNiwYXj77bdRUVEBWZYhCALmzJmDSZMmeTLlhgwZgkcffRQ33ngjduzYgSlTpsT4UxARER05WDOKiIiINBYtWoRRo0YhPz8fgLt73x//+EesWbMGF154ISZMmICLL74Yubm52LhxI2bOnIlJkybh0ksvRV5enmZZ3377LS677DIcd9xxmDZtGu6++25UVVUFfX+bzQZRFOF0On2mPfPMM3j55Zc9f8uyjE8++QQzZ87E5MmTcfLJJ+Oxxx5DU1OTZ54HH3wQM2fOxJdffolp06bhhRdeAODupqdeVk1NDf79739jxowZmDBhAi666CJN1hgA/Prrr7j00ktx3HHH4bjjjsMVV1yBDRs2BFx3/nRkfS5atAh/+tOfMGHCBBx//PG44YYbsGfPHp/337lzJ/7xj39g0qRJOPPMM/Hdd9+hvLwc119/PaZMmYKzzz4by5Yt0yx7586duOGGG3DyySdj8uTJ+Nvf/obt27d7pm/evBmjRo3C5s2bce+992Lq1KmYPn06HnjgAVgsFgDAjBkzsGHDBixevNgzb6B1MmPGDNx9990AgNLSUowaNQpLlizBAw88gKlTp3q+K7vdjsceewzTpk3DSSedhBdffDHguvVHlmUsX74cF154oSe4CQBjx47F2WefDUEQYLfbsXXrVpxxxhma15544olITEzE2rVr2/SeREREFByDUURERBRSQ0MD5s2bh1deeQXz589HfX09/v3vf2POnDl4+umnMW/ePNTU1OCZZ57xvObbb7/Fv//9b0yePBmLFi3CnDlzUFBQgGuvvRYOhyPge51++umoqqrC3/72N/z8888wmUwB533nnXfw/PPP46KLLsJ3332H559/HuvWrcPtt9/u0/7ly5dj3rx5uOWWW3yW43A4cM0112Dbtm144okn8P3332PmzJl49NFHsWTJEgBAYWEh7rrrLpx33nn49ttvsXDhQowfPx4333wzKioqAAAXXngh1q1bh2HDhkV8fX799dd46KGHcM4552DJkiX45JNP4HQ6cfXVV6OyslKz/BdeeAE333wzlixZguHDh+Oxxx7Dww8/jL///e9YtGiRJ+unubnZ89muueYaiKKIDz74AAsWLED//v1x/fXX+wSRnn/+eZx00klYvHgx7r33XixZsgTz58/3tDE7OxsXXHAB1q1b1+ZsonfffRdTpkzBokWLcNlll+Hjjz/Gtddei6OPPhoLFy7EpZdeio8++ghbtmwJe5llZWVobGxE37598fjjj+O0007DqaeeiieffNITRDt8+DBEUcSQIUM0r01ISMBRRx2FgoKCNn0OIiIiCo7BKCIiIgqptrYW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/NCnfo/agw8+iHHjxuHrr7/G0qVL0dDQgOTkZIwcORIPPPAALr/8cs+8t9xyC9LS0vD555/jxRdfRGJiIk444QR8/vnnbQp2JCYm4pNPPsHLL7+Me++9F42Njejduzcuuugi/N///R8A9yh/VqsVH374IZ566ikkJCTgmGOOwRtvvOGpObRs2TI89NBDWLZsGUaMGNHONeffxRdfDEmSMHfuXLz77rtITk7GtGnT8Mwzz7QpC8yfoUOHYt68eXjttddw9dVXw+l0YtiwYXjggQdw5ZVXtmlZt9xyC5555hlceeWVeO6553DBBRfgqaeewuzZs/HnP//Zs9w333yzQ21ui6lTp+K9997D66+/josuuggZGRmYNWuWZzQ9wJ0d9t577+HVV1/1jJB4wQUX4L777otaO4mIiLoLQW5LBUgiIiIiIiIiIqIOYDc9IiIiIiIiIiKKGnbTIyIiIqIu57HHHsP3338fcr4dO3ZEoTVERETUFuymR0RERERdTl1dHcxmc8j5hg4dGoXWEBERUVswGEVERERERERERFHDmlFERERERERERBQ1cR2MKisrw80334zp06fjrLPOwksvvQRJkvzO29zcjPvuuw+jRo1Cfn6+ZtqMGTMwfvx4TJgwwfPv1ltvjcZHICIiIiIiIiIilbguYH7HHXdg3LhxWL58Oerq6nDLLbegd+/euO666zTzVVVV4eqrr8bkyZMDLuujjz7C9OnTw3pfl8uFxsZGJCUlQaeL63gdEREREREREVGbSZIEu92OrKwsGAzRDQ/FbTBq9+7dyMvLw9y5c5GRkYGMjAxce+21+PTTT32CUQ0NDbj//vsxevRoLFmypMPv3djYiKKiog4vh4iIiIiIiIgong0bNgy9evWK6nvGbTBq7969GDhwILKysjzPjRs3DoWFhTCbzUhPT/c8P3r0aIwePRqlpaUBl/fZZ5/hkUceQV1dHU477TQ8/vjjAVd2UlISAPcXkpycHKFPREcCWZY9258gCLFuDsUxbisULm4rFC5uK/GN3w+Fi9sKhYPbCYWrI9uKzWZDUVGRJwYSTXEbjDIajcjMzNQ81xqYamho0ASjQhkzZgwmTpyIF198EU1NTXjggQdw5513Yv78+X7nb+2a53K54HQ62/kJ6EgkyzJEUYTT6eRBgYLitkLh4rZC4eK2Et/4/VC4uK1QOLidULg6sq24XC4AiEl5orgNRgHulRoJs2fP9jxOS0vD448/jgsvvBDFxcUYMmRIwNelp6cjNTU1Im2gI0PrNpmVlcWDAgXFbYXCxW2FwsVtJb7x+6FwcVuhcHA7oXB1ZFuxWCyd0aSwxG0wKjs7G0ajUfOc0WiEIAjIzs7u0LIHDhwIAKiurg4ajBIEgT988tG6XXDboFC4rVC4uK1QuLitxDd+PxQubisUDm4nFK72biux3Lbidqi48ePHo6KiAvX19Z7ndu/ejWOOOQZpaWlhL6esrAyPP/44HA6H57n8/HwAwODBgyPXYCIiIiIiIiIiCilug1Fjx47FhAkT8Morr8BsNiM/Px9z587FlVdeCQA4//zzsXXr1pDL6dWrF1auXInnn38eFosFVVVVeO6553DWWWehX79+nf0xiIiIiIiIiIhIJW6DUQDw5ptvorq6GqeccgquvvpqXHzxxbjqqqsAAIWFhZ7+jXPmzMGECRNw/vnnAwBmzpyJCRMmYM6cOUhOTsaHH36IwsJCnH766bjoooswePBgvPjiizH7XERERERERERE3ZUgR6pK+BHEYrEgNzcXY8aMYQFz0pBlGY2NjSwkSCFxW6FwcVuhcHFbiW/8fihc3FYoHNxOKFwd2VZiGfuI68woIiIiIiIiIiI6sjAYRUREREREREREUcNgFBERERERERERRQ2DUUREREREREREFDUMRhERERERERERUdQwGEVERERERERERFHDYBQREREREREREUUNg1FERERERERERBQ1DEYREREREREREVHUMBhFRERERERERERRw2AUERERERERERFFDYNRREREREREREQUNQxGERERERERERFR1DAYRURERERE1E2ZTLmoqV0R62YQUTdjiHUDiIiIiIiIKDa2/P5HAMC0E75DRsa4GLeGiLoLZkYRERERERF1c83Nh2LdBCLqRhiMIiIiIiIi6uZkWYx1E4ioG2EwioiIiIiIqNuTY90AIupGGIwiIiIiIiIiIqKoYTCKiIiIiIiIiIiihsEoIiIiIiIiIiKKGgajiIiIiIiIiIgoahiMIiIiIiIi6uZkFjAnoihiMIqIiIiIiIiIiKKGwSgiIiIiIiIiIooaBqOIiIiIiIiIiChqGIwiIiIiIiIiIqKoYTCKiIiIiIiou2P9ciKKIgajiIiIiIiIiIgoahiMIiIiIiIi6uZcdbZYN4GIuhEGo4iIiIiIiLo5qdkR6yYQUTfCYBQREREREVE3J8ssGkVE0cNgFBERERERERERRQ2DUUREREREREREFDUMRhERERERERERUdQwGEVERERERERERFHDYBQREREREREREUUNg1FERERERERERBQ1DEYREREREREREVHUMBhFRERERERERERRw2AUERERERFRtyfHugFE1I0wGEVERERERERERFHDYBQREREREREREUUNg1FERERERERERBQ1DEYREREREREREVHUMBhFRERERERERERRw2AUERERERFRt8fR9IgoehiMIiIiIiIiIiKiqGEwioiIiIiIiIiIoobBKCIiIiIiIiIiihoGo4iIiIiIiIiIKGoYjCIiIiIiIurmZBYwJ6IoYjCKiIiIiIiIiIiihsEoIiIiIiKibk6AEOsmEFE3wmAUERERERFRN8duekQUTQxGERERERERdXOSFOsWEFF3wmAUERERERFRN1dutMS6CUTUjTAYRURERERE1M1JMrvpEVH0MBhFRERERETU7bGAORFFD4NRRERERERE3R4zo4goehiMIiIiIiIiIiKiqGEwioiIiIiIiIiIoobBKCIiIiIiou6OvfSIKIoYjCIiIiIiIuruWL+ciKKIwSgiIiIiIqJuTpaZGkVE0cNgFBERERERUTcnu5yxbgIRdSMMRhEREREREXVzrubGWDeBiLqRuA5GlZWV4eabb8b06dNx1lln4aWXXoIkSX7nbW5uxn333YdRo0YhPz9fM81oNOKuu+7CySefjFNPPRWPPPIIbDZbND4CERERERFR/GMvPSKKorgORt1xxx3o168fli9fjrlz52L58uX49NNPfearqqrCrFmzoNfr/S7nP//5D6xWK3744Qd88803yM/Px8svv9zZzSciIiIiIuoSWL+ciKIpboNRu3fvRl5eHu677z5kZGRg2LBhuPbaa7FgwQKfeRsaGnD//ffjjjvu8JlWW1uL5cuX4+6770Z2djb69euH2267Dd988w2cTvaLJiIiIiIikpkaRURRFLfBqL1792LgwIHIysryPDdu3DgUFhbCbDZr5h09ejTOOeccv8vJzc2FXq/HqFGjNMuxWCwoKCjonMYTERERERF1AbswGd9jJoNRRBRVhlg3IBCj0YjMzEzNc62BqYaGBqSnp4e9nPT0dAiCkniqXk4wsixziFPSaN0muF1QKNxWKFzcVihc3FbiG78fCle8bSsvCP8BAAxKWo0/xEmbKP62E4pfHdlWYrl9xW0wCojcimnvcsxmM7vykYYsy7BYLACgCXASeeO2QuHitkLh4rYS3/j9ULjidVup1aWgsZEj6sWLeN1OKP50ZFux2+2d0aSwxG0wKjs7G0ajUfOc0WiEIAjIzs5u03LMZjNEUfQUOG9dbq9evYK+Nj09HampqW1qNx3ZWgObWVlZPChQUNxWKFzcVihc3FbiG78fCle8biuCIGhKpFBsxet2QvGnI9tKaxArFuI2GDV+/HhUVFSgvr7eE3zavXs3jjnmGKSlpYW9nDFjxkCWZeTl5WHcuHGe5WRmZmL48OFBXysIAn/45KN1u+C2QaFwW6FwcVuhcHFbiW/8fihc8bqtxFt7urt43U4o/rR3W4nlthW3BczHjh2LCRMm4JVXXoHZbEZ+fj7mzp2LK6+8EgBw/vnnY+vWrSGXk52djfPOOw+vv/466uvrUVlZidmzZ+Mvf/kLDIa4jcURERERERF1im8q63FXbjGcEusREVFsxG0wCgDefPNNVFdX45RTTsHVV1+Niy++GFdddRUAoLCw0JNSNmfOHEyYMAHnn38+AGDmzJmYMGEC5syZAwB46qmnkJGRgbPPPht//vOfMXHiRNx9992x+VBEREREREQx9K/cYvyvsh4LK+tj3RQi6qbiOjWof//++OCDD/xO279/v+fxbbfdhttuuy3gcjIyMvDqq69GvH1ERERERERdVZ3TpfzBnmBEFEVxnRlFREREREREUcAee0QURQxGERERERERERFR1DAYRURERERE1A1pkqFYzJyIoojBKCIiIiIiom5OFhmMIqLoYTCKiIiIiIiom9NL9lg3gYi6EQajiIiIiIiIuiFZkwzFzCgiih4Go4iIiIiIiLo5IdYNIKJuhcEoIiIiIiIiIiKKGgajiIiIiIiIujv20iOiKGIwioiIiIiIiIiIoobBKCIiIiIiom5I1qRDMTWKiKKHwSgiIiIiIiIiIooaBqOIiIiIiIiIiChqGIwiIiIiIiLqhtQd8xKQFLN2EFH3w2AUERERERFRN5eM5Fg3gYi6EQajiIiIiIiIiIgoahiMIiIiIiIi6obU3fScicZYNYOIuiEGo4iIiIiIiLo5W6+8WDeBiLoRBqOIiIiIiIiIiChqGIwiIiIiIiIiIqKoYTCKiIiIiIiIiIiihsEoIiIiIiKibkiWQ89DRNQZGIwiIiIiIiLqJhqcrlg3gYiIwSgiIiIiIqLu4mCzLdZNICJiMIqIiIiIiKi7kAM8JiKKJgajiIiIiIiIujkzMmLdBCLqRhiMIiIiIiIi6uZ+w5mxbgIRdSMMRhEREREREXUT2m56yl+nYk30G0NE3RaDUURERERERN2QOjB1SBoVs3YQUffDYBQREREREVE3JKuiUdv0J8SuIUTU7TAYRUREREREREREUcNgFBERERERUTckh56FiKhTMBhFRERERETUTagDUJLMcBQRxQaDUURERERERN2QXWIwiohig8EoIiIiIiKiboihKCKKFQajiIiIiIiIugl1zzwXu+kRUYwwGEVERERERNQNfVxWG+smEFE3xWAUERERERERERFFDYNRREREREREREQUNQxGERERERERdXPnGtfFuglE1I0wGEVERERERNRNyAHG0EuUnVFuCRF1ZwxGERERERERdROBxs8TBF4aElH0cI9DRERERETU7QUKUxERRR6DUURERERE4ShYA7wyGtj/U6xbQkRE1KUxGEVEREREFI7P/gyYKoAvL491S4gi4qTUROUPJkYRURQxGEVEREREFIZa9MBCXIgq9Ip1U4giIllQIlAOnRTDlhBRd2OIdQOIiIiIiLqC+ZgFI7JwAMPxSKwbQ9ROsioDSlI/FqLfFiLqvpgZRUREREQUBiOyAABOJIaYk6hrUOdCMRZFRNHEYBQRERERER0xTCYTPvzwQ+zYsSPWTYl7sipNysDUKCKKInbTIyIiIiKiI8bKlStRWlqK0tJSTJkyJdbNiTvqOuV5dtHzWGBuFBFFETOjiIiIiIjoiGG322PdhC5DDD0LEVGnYDCKiIiIiIiOGDodL3GCMYlKCErW5EkREUUP99RERERERHTEEAR2NwvmiUPlnsdyJ8WiSm0OVNudnbNwIjoisGYUEREREREdMRiMCq7E5vA87oxYlMklYurGfQCAyrMmd8I7ENGRgJlRRERERERE3Zw+tSkiyylVB7s6K/WKiLo8BqOIiIiIiIi6OWtCYkSW83JRpeexFJElEtGRiMEoIiIiIiKibknJXFounI9msePj6y2tafQ8lpgYRUQBMBhFRERERETUDXn3oiu2OvzP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UimCgoIwevRoTJ06FbNnz0ZSUpJJxyTLE0TOC0xERETkaATkncMJArNRRGRdTEY5IVEUcefOHezZswc9e/Y0apsTJ04gOzsbAwYMKLAuIiICM2bM0OsaZ4yQkBBs3LgRt2/f1i5TKBSIioqCSqUqctsBAwZAFEWcOHHCpGMSERERERHwRO6h8xeTUURkXRwzygJEUYSYlmbVYwquribfwZgzZw7mzZunHReqefPmGDJkiFHbxsXFISAgwOSEU1E+++wzTJw4EZ07d0ZwcDCaNGmCdu3aoV27dtoxogojk8kQFBSEuLg4i8VD5hF5J42IiIjI4aRIOfEMEdkOk1ElJIoibr00BGm//27V47o2aICq/7fRpITU9OnTMXjwYAA53e42bNiAqKgo7N27Fz4+PsVur9FozI7XkFq1auGHH37ApUuXcOrUKZw+fRrvvPMOQkNDsX79eri7uxe5vVqtLjZpRURERERExeHNRSKyLnbTswQHbBni5eWFt956C3K5HD/++GOx5YODg3H37l2kpqYWWU4ulwMA0tPTC6x79uwZlEplgeV16tTBqFGjsGrVKuzduxc3btzQDk5emJSUFMTGxqJatWrFxk5ERERERERE9oMto0pIEARU/b+NDtFNrzAZGRnFlmnRogVcXFywfv16vPHGG3rr/v77b4wfPx5btmyBn58fVCoV/vzzT4SHh2vLiKKIixcvonnz5tpttm3bhg8++AASSV5OtFq1aqhSpQrSink9v/nmG3h6emr3R7bDAcyJiIiIiIjIFExGWYAgCBDc3GwdhkkyMjKwadMmPH36FB07diy2vIeHBz744AN8+OGHEAQBQ4YMgUKhwKlTp/Dhhx+iZ8+e8PLyAgCMHDkSS5YsQZUqVdCoUSMkJSVh5cqVePjwIUaOHAkA8PPzw759+5Ceno4xY8agUqVKSElJwa5duxAbG4s2bdoYjOPp06fYtm0b1q5di0WLFsHFxcVyLwqZRSYp2NqNiIiIiByIA/b0ICLHxmSUE8kdwBwAlEolateujdWrVyMoKMio7fv164fy5ctj1apV+PrrryEIAoKDgzFhwgRERUVpy73++uuoUKECFixYgNjYWLi7u6NevXpYt24dfH19AQC+vr7YtGkTli9fjoEDByIhIQFKpRIRERFYu3Ytatasqd3fmjVrsG7dOm3cDRo0wLp16xAZGWmhV4ZKQiYwGUVERERERETGE0SRfWzyS01NRUxMDMLCwuDmYC2eqHSJoojExESoVCqLdZN0dG33nMNfXjkDyZ899AxVPm1t44jsA+sKGYt1hYzFumLf+P6QseylrlT87wXt49stq0Ch8LNZLFSQvdQTsn8lqSu2zH1wAHMiIiIiIiIiIrIaJqOIqEQEtq0kIiIicnBseUNE1sVkFBERERERERERWQ2TUURkMV6dq9o6BCIiIiIiIrJzTEYRkcVIfTizHhEREZEjqJH+SOcvdtMjIutiMoqILIYzfRARERE5BpU63dYhEJETYzKKiCyHuSgiIiIih8MbikRkbUxGEZHl8DyGiIiIyCGIotrWIRCRE2Myiogsh3fViIiIiByCRhRtHQIROTEmo4ioRKola/L+YC6KiIiIyAHxJI6IrIvJKCdz48YNTJw4ES1atEC9evXQoUMHzJkzBwkJCXrlTpw4gdDQUMycObPAPqZMmYIJEyboLTt37hwaNGiAQ4cOAQB27dqF0NBQ1K1bt8C/Hj16AADi4uIQGhqK69evFzjGwoULMXToUO3fycnJmDt3Ljp06ICIiAg0bNgQb7zxBv766y9tmaFDh6J27dra47Rq1Qrjxo3D77//ri0zffp07frw8PACMZ45c8b0F9XJTfwrHQNuZ2LdqRSON0BERETkKHjaRkQ2xGSUE4mJiUH//v1RsWJF7N27F+fPn8eKFSvw119/YfDgwUhPz5tRY/v27ejRowd++OEHZGRkFLnfv/76C2+++SamTZuGrl27apf7+fkhOjq6wL8ffvjB5NgnTZqEa9eu4bvvvsMff/yBw4cPo1KlShg+fDiSk5O15UaMGIHo6GhcuHABmzZtQnh4OIYPH47du3cDAObMmaONY/bs2QVibNy4scmxOTuvTBGTYzIQnqThSQ0RERGRQ+JJHBFZF5NRTmTWrFlo1aoV3nvvPfj5+UEqlSIsLAxfffUV6tevj3/++QcA8PTpUxw7dgzjxo2Dj48PDh8+XOg+79y5g9deew1jxoxBv379Si32kydPYsCAAQgKCoIgCPD19cXUqVMxZcoUqNUFB1+USqUICgrC6NGjMXXqVMyePRtJSUmlFp8zE5E33kDG9UQbRkJERERERESOgMkoJ/H48WOcP38eL7/8coF1Hh4e+OSTTxAUFAQA2LNnD8LCwhAcHIxevXphx44dBvf56NEjvPbaa3jxxRfxyiuvlGb4CAkJwcaNG3H79m3tMoVCgaioKKhUqiK3HTBgAERRxIkTJ0o1RmclIm/MqIybTEYRERERERFR0WS2DqAsEEUR2Zma4gtakEwhMWl8njt37gDISeoUZ8eOHRg8eDAAoE+fPlixYgXi4uJQpUoVbZlnz55h5MiRSElJwahRowzu59GjR6hbt26B5RMnTjQ5efXZZ59h4sSJ6Ny5M4KDg9GkSRO0a9cO7dq1g1QqLXJbmUyGoKAgxMXFmXRMMpbOTCxSNvEmIiIicjw8hyMi62IyqoREUcSuBefx4IZ1W4RUqq5C30kNjE5I5ZbTaIpOml24cAGxsbHo3r07ACAwMBD169fHrl27MG7cOG25EydOYMyYMfjpp58wbdo0LFq0qMC+/Pz8cPLkSWOfUpFq1aqFH374AZcuXcKpU6dw+vRpvPPOOwgNDcX69evh7u5e5PZqtbrYpBWZRxRF7fmL1Etp22CIiIiIiIjI7rGbngU4wgRiuV3wrl69WmS57du3Izs7Gx07dkRkZCQiIyMRHR2N3bt36yWyunTpgnHjxmHZsmU4efIkvv76a5NjksvlAKA3cHquZ8+eQaksmNioU6cORo0ahVWrVmHv3r24ceOGdnDywqSkpCA2NhbVqlUzOUYqnm5C1LV2ORtGQkRERETm4IzIRGRtdtsyKiEhAR9//DFOnz4NiUSCtm3bYsaMGXBxcTFYPisrC4sWLcLatWvxzTffoE2bNtp1GRkZmDt3Ln766SdkZGSgadOmmDlzJnx8fEocpyAI6Dupgd130/Px8UGTJk2wdu1atGrVSm9dWloahgwZgilTpuDAgQOYOXMmmjVrpre+f//+OHXqFFq2bAkA2lZGAQEBWLRoEUaPHo2aNWuiffv2Rsfk5+cHlUqFP//8E+Hh4drloiji4sWLaN68OQDg77//xrZt2/DBBx9AIsnLn1arVg1VqlRBWlpakcf55ptv4Onpqd0fWZZUkOc99mHLKCIiIiIiIiqa3baMmjFjBtLS0rB//37s3LkT169fx8KFCw2WTU1NxUsvvYSEhIScLkP5LF68GJcvX8bWrVtx6NAhiKKIqVOnWixWQRAgV0qt+s+cuxfTpk3DhQsX8O677+LBgwfQaDSIiYnByJEj4eLighs3bkCpVKJv376oWrWq9l+tWrXQoUOHQgcyb9myJcaNG4dJkybh+vXrRscjlUoxcuRILFmyBKdOnUJWVhYeP36MefPm4eHDhxg5ciSAnKTVvn378OGHH+LevXsQRRHJyclYv349YmNj9RKPup4+fYqvv/4aa9euxaxZswpNZJIFFfz4EREREREREemxy5ZRjx49wpEjR/D999/D19cXADBmzBi88847mDx5srZ7V67U1FT069cPgwYNwq5du/TWZWdnY8eOHZg/fz4qVaoEABg/fjx69OiB+Ph4+Pv7W+dJ2YFatWph27ZtWLZsGfr27YvU1FRUrFgRPXv2xKhRo/DKK6+gV69eUCgUBbbt168f3n77bSQkJBjc9+uvv45Lly5hzJgx2L59O4DCBzAHgAMHDiAwMBCvv/46KlSogAULFiA2Nhbu7u6oV68e1q1bp33vfX19sWnTJixfvhwDBw5EQkIClEolIiIisHbtWtSsWVO73zVr1mDdunUAAKVSiQYNGmDdunWIjIwsyUtHRdCIakiEf8fjMpAMJiIiIiIiItIliIaaEtnY8ePH8eabbyI6OlrbAujx48do0aIF9u7di9DQ0EK3DQ0NxapVq7StZW7cuIHu3bvj559/RsWKFbXl6tevj0WLFqFDhw4F9pGamoqYmBiEhYXBzc3Nws+OHJkoikhMTIRKpWLf+n/dev8YpJKcBLHfiDpwqVny7q9lAesKGYt1hYzFumLf+P6QseylrnTbtxsXPIIBAHGtq0Mm87RZLFSQvdQTsn8lqSu2zH3YZcuohIQEeHh46L2QKpUKQE7XK1P3BQBeXl56y728vIrdlyiKBrv9kfPKrROsF3lEnb55fG3ysK6QsVhXyFisK/aN7w8Zy37qiu45HOwgHtJlP/WE7F1J6oot65fNklF79uzB+++/b3DdhAkTLP6imLO/5ORkZGVlWTQOcmyiKCI1NRUAZx3Jk/fZSklLRUai3Q5FZ1WsK2Qs1hUyFuuKfeP7Q8ayx7qSlJQEqVRt6zBIhz3WE7JPJakrGRkZpRGSUWyWjOrTpw/69OljcN3JkyeRnJwMtVqtnbUtt4VTuXKmTR2fO+5QQkIC3N3dtcsTExOL3ZeHhwe76ZGe3KQmm8vm0W1f6O7uDpd/WzE6O9YVMhbrChmLdcW+8f0hY9lLXRGVqdrHXl4qyGTuRZQma7OXekL2ryR1JTeJZQt22U0vLCwMoijiypUrCA8PBwBER0fDy8sLISEhJu0rMDAQKpUKly9fRkBAAADg77//RmZmJurUqVPktoIg8INPBeTWC9aNXHktowTwzo0u1hUyFusKGYt1xb7x/SFj2Udd0TmHs3ksZIh91BNyBObWFVvWLbvsT+Pr64uuXbviiy++wJMnT/DgwQOsWLEC/fv3h0yWkz8bPnw4Dhw4UOy+pFIpXnzxRaxcuRL379/H06dPsWjRInTu3Bl+fn6l/VSIyrxsMdPWIRARERGRiWqob9o6BCJyYnaZjAKAWbNmwdPTEx07dkTv3r0RERGBCRMmaNffuXMHiYmJAIDdu3ejbt26qFu3LgBgzJgxqFu3LqZPnw4AGDduHOrVq4c+ffqgY8eOcHd3x9y5c63/pIjKoEx1ivaxILXbrxQiIiIi0tFb/B5DxO/wmTgOoshxconIuuyymx4AeHp6YtGiRYWuP3bsmPZxVFQUoqKiCi2rUCjw0Ucf4aOPPrJkiESUH1sQExERETkEObLxPPYBAARBauNoiMjZsBkDERERERGRs1ErtA+lUk7aRETWxWQUERERERGRkxH1WrSzeTsRWReTUURkOTyPISIiInIQos5jnsQRkXXZ7ZhRZFkdOnRAfHw8JJKc/KOfnx+aNm2KkSNHokaNGtpyly5dwsqVK3H27FmkpaWhfPny6NKlC9544w14eXnp7fPUqVP49ttvcfHiRWRkZKBcuXJo164d3nrrLZQrVw4A8Ntvv2HYsGFQKBTITyaT4ffffwcAhIaGYtWqVWjTpo1emc2bN2PVqlXaMcKysrLw1Vdf4YcffkB8fDwEQUCdOnXwzjvvoFGjRgCAKVOmYM+ePZDJZBBFER4eHoiIiMCQIUPQtm1bAMCXX36Jr776CgAgiiKysrL0Ypw9e3aR45AREREREZUVtpzenYicE1tGOZHp06cjOjoa58+fx+rVq+Hj44N+/frh1KlTAICTJ0/i5ZdfRkREBA4ePIgLFy7g66+/xrVr1zB48GAkJydr97V9+3a89dZbeP755/HTTz/h3LlzWLZsGa5evYoXX3xRrywAnD17FtHR0Xr/chNRpvj0009x7NgxLF26FOfOncPx48fRokULjBgxAnfu3NGW69atm/Y4u3btQvv27fHuu+9i5cqVAHJmXMxd/+233xaIkYkoIiIiIiIiotLBZJQTksvlqF69OiZPnoyhQ4di+vTpUKvV+OijjzBkyBC8/vrr8Pb2hiAIqF69OpYvX460tDR8/fXXAICkpCTMmzcP77//Pl544QW4ublBJpMhPDwcX331Ffr374/U1NRSif3kyZPo0aMHQkNDIZVK4eHhgTfffBNz5swx2PpKEARUrlwZgwcPxuLFi7F06VLExsaWSmxERERERA5DEIsvQ0RUSpiMsgBRFJGVnm7Vf6JomR+PV155BXFxcbh8+TLu3LmDYcOGFSijUCgwaNAgHDp0CABw4sQJiKKIfv36FSibmxyqUKGCReLLLyQkBN9//z1iYmL0lvfu3Rv+/v5FbtumTRsEBwfj8OHDpRIbERERERERERWPY0aVkCiK2PLh+7j3d0zxhS2ocmhtDJo5v8T9u/38/ODl5YW4uDi4uroWmtCpVq0a4uLiIIoi4uLiULlyZcjlcqOPkzuek67Bgwfjgw8+MCneGTNm4N1330VUVBQCAgLQsGFDtG3bFl26dDHYMiq/kJAQxMXFmXRMKhrvqRERERE5Ip7FEZHtMBllCQ4+4F92djYAQK1WQxRFgwmu/MvVarXe+vwDgvfu3Rvz5s3Trj979iyUSmWJY61cuTK2bNmCa9eu4ZdffsGZM2cwffp0LFmyBBs3biy2dZRarYZUKi1xHJQnXeZu6xCIiIiIyFTspkdENsRkVAkJgoBBM+cjOyPDqseVKZUWmfXi1q1bSE1NRYUKFZCZmYk7d+4gKCioQLmbN28iODgYgiCgWrVquHv3LtLT0+Hi4gIgZ0DwMWPGAMiZzU6j0ZgUh1wuR3p6eoHlz549M5jEqlGjBmrUqIFhw4bh4cOHGDBgANatW4f333+/0GNoNBpcuXIFrVq1Mik2Klq63BP4NzepCPQqujARERERERE5PY4ZZQGCIEDu4mLVf5aafnXZsmWoWbMmGjZsiODgYKxfv75AmezsbGzbtg3du3cHALRo0QLu7u7YsGGDwX2amogCcrrPXb58ucDyCxcuoGbNmgCABw8e4OOPPy4wU1/58uVRq1YtpKWlFXmMnTt34vHjx+jcubPJ8ZFxBKljtxIkIiIichZiRiVbh0BETozJKCcVHx+PTz75BEePHsXcuXMhCAI+/vhjbNu2DQsXLsSTJ08giiKuX7+OV199FZ6ennjttdcAAG5ubvjoo4+wZMkSfPnll0hMTNSOJbV48WIcOHAAdevWNSme0aNHY/369Th06BAyMjKQlJSEb775BqdOncL48eMBAL6+vvjll1/w3nvv4caNG9BoNEhLS8P+/ftx6tQpdOjQweC+k5OTsXXrVsybNw9TpkwptisfEREREVGZl80W7URkO+ym50TmzJmDefPmQRRFuLu7o3nz5ti+fTtq1KgBAGjevDn+7//+DytWrED37t2RlpYGf39/dOvWDaNHj4arq6t2X88//zwqVKiAr7/+Gt999x3S09Ph4+ODRo0aYcOGDYiMjNQ7tqEBzAFg1apVaNasGXr27AlPT0+sXLkS06dPh1wuR1hYGNauXYuQkBAAObP6bdiwAcuWLcNrr72GJ0+eQCKRICwsDJ9//jlat26t3e/Bgwdx5MgRAIBMJkOdOnXwxRdfoG3bthZ9TYmIiIiIHBPHjCIi2xFEUeS3UD6pqamIiYlBWFgY3NzcbB0O2RFRFJGYmAiVSmWxrpKO7vi0/yFEnfNaVPm0dTGlnQfrChmLdYWMxbpi3/j+kLHspa4c2f4ihHLnAAAdO1y3WRxkmL3UE7J/Jakrtsx9sJseERERERERERFZDZNRRFQilb1dbB0CEREREZmMHWSIyHaYjCKiEvFwkds6BCIiIiIiInIgTEYRUclw2DkiIiIixyPwHI6IbIfJKCIiIiIiIiIishomo4iIiIiIiIiIyGqYjCKikuFUs0REREQOiN30iMh2mIwiIiIiIiJyOkxGEZHtMBlFRERERERERERWw2QUERERERERERFZjczWAZB1dOjQAfHx8ZBIcvKPfn5+aNq0KUaOHIkaNWpoy126dAkrV67E2bNnkZaWhvLly6NLly5444034OXlBQCoW7eutnxWVhYkEgmkUikAoHLlyjh06BCmTJmCjIwMLF68WC+OjIwMREREYP369WjatCkA4Oeff8aqVavw999/IzU1FZUqVcKAAQMwatQoCIKA3377DcOGDYNCoQAASKVSBAcHo1u3bnjllVfg4uKCu3fvolu3btrjZGZmQiaTaZ9v48aNsWbNGku/rAQAIpt4ExERETkaV4UE6bYOgoicFpNRTmT69OkYPHgwsrKycPv2bezYsQP9+vXDypUr0bx5c5w8eRJvvfUWxowZgzlz5kClUuHGjRuYP38+Bg8ejK1bt8LDwwPR0dHafQ4dOhT16tXDpEmTzIrp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FYMHDwYAVK1aFePGjcO0adOQlJRkcJuBAwciJCQEc+bMMelY4eHhaNWqFSZPnozr169DrVbj+vXrePfdd9GsWTPtWFVubm5YuHAhNmzYgMTERAA541R99tlnCAgIQK1atQzu//bt25g4cSJiY2Mxbdo0k2IjIiIiIiIiIttiNz0ncODAARw7dgz79u3Ta1E1fPhw/Pjjj5g7d67BllaCIGDmzJno168f/vvf/6J9+/ZGH3P58uVYvXo1xowZg/j4eJQvXx5t2rTB+PHjtcdq0aIFVqxYgTVr1mDFihVITk6Gt7c3mjdvjo0bN0KhUGj3N2bMGAiCAFEU4e3tjbZt22Lnzp2oVKlSCV4ZIiIiIiIiIrI2QRRF0dZB2JvU1FTExMQgLCwMbm5utg6H7IgoikhMTIRKpTK5qyQ5F9YVMhbrChmLdcW+8f0hY7GukDFYT8hYJakrtsx9sJseERERERERERFZDZNRRERERERERERkNUxGERERERERERGR1TAZRUREREREREREVsNkFBERERERERERWQ2TUUREREREREREZDVMRhERERERERERkdUwGUVERERERERERFbDZBQREREREREREVkNk1FERERERERERGQ1TEYREREREREREZHVMBlFRERERERERERWw2QUERERERERERFZDZNRRERERERERERkNTJbB2CPNBoNACAtLc3GkZC9EUURGRkZSE1NhSAItg6H7BjrChmLdYWMxbpi3/j+kLFYV8gYrCdkrJLUldycR24OxJqYjDIgIyMDABAbG2vbQIiIiIiIiIiISlFGRgY8PDysekxBFEXRqkd0ANnZ2UhMTIRSqYREwp6MRERERERERFS2aDQaZGRkQKVSQSazblslJqOIiIiIiIiIiMhq2OyHiIiIiIiIiIishskoIiIiIiIiIiKyGiajyGHcvXsXb731Fpo2bYoWLVpgypQpSEpKAgDExMTg5ZdfRsOGDdGlSxesWbNGb9v//Oc/6N27NyIjI9G1a1ds27ZNb/3169cxdOhQ1KtXD23btsV3331XZCzFHS8rKwvz589HrVq18L///c+o57d7925ERkZi4cKFestHjBiBunXr6v0LCwvD8uXLjdqvMyrLdeXKlSt45ZVX0KhRI7Rp0wZz585FZmamdr0oivj2229Rp04dbN68udj9OTtnrSu//fYbQkNDC3y3/Pjjj8Xu11k5a10BgP3796NXr16oX78+evTogRMnThS7T2tzpPenuOPll5CQgPHjx6NFixZo1aoVpk2bhvT0dL0yhZ1DkD5nrSdxcXEGv/O//fZbY142p+SsdQUATp48iQEDBiAyMhIdO3bE7t27i3m1nFtZrisAEB0djc6dO+PFF18ssO7HH3/Unh+0b98eCxYsQHZ2drH71BKJHETPnj3FKVOmiMnJyeL9+/fFF154Qfzggw/EtLQ0sXXr1uKyZcvElJQU8dKlS2KTJk3EQ4cOiaIoin/88YdYt25d8fDhw2JWVpb4008/ieHh4eKZM2dEURTFtLQ0sV27duKqVavE1NRU8Y8//hB79OghXrt2zWAcxR0vJSVF7N+/vzhlyhSxZs2a4s8//1zsc/v444/Ffv36ic8//7y4YMGCIssmJiaKLVu2FK9cuWLKy+dUympdSU5OFlu2bCkuWrRIzMjIEK9duya2b99eXLFihbbMqFGjxJEjR4rNmzcXN23aZImXs0xz1rry66+/iu3bt7fUy+gUnLWunD59Wqxdu7b4n//8R8zIyBCPHDkiNmjQQLx7966lXlqLcJT3p7jjGfL222+Lr7/+uvj48WPxwYMH4sCBA8XZs2dr15tyDuHsnLWe3LlzR6xZs6YlX8oyz1nrys2bN8U6deqIGzduFDMyMsQzZ86ITZo0ES9cuGDJl7dMKct1Zc+ePWLbtm3F1157TRwwYIDeuujoaDEiIkL86aefRLVaLf71119i8+bNxe+++87o147JKHIIiYmJ4pQpU8SHDx9ql23YsEHs0qWL+OOPP4rNmjUTs7OztesWLFggjhgxQhRFUfz555/F5cuX6+2vb9++4ldffSWKoiju2rVL7Nmzp9GxFHe8hw8fips3bxZFUTQ6GfX111+LGRkZ4ssvv1zsieTMmTPFjz/+2Oh4nU1Zriu3bt0Sp0yZImZlZWmXffrpp+Krr76q/XvFihWiRqMR27dvz2RUMZy5rjAZZRpnriuffvqpOHz4cL1txo0bJ65cudLomEubI70/xR0vv4cPH4q1atUSY2JitMt+/vlnsX79+mJmZqYoiqadQzgzZ64nTEaZxpnrysaNG8VOnTrpbTN//nxxxowZRsfsTMpyXRFFUdy2bZv44MEDcenSpQWSUdeuXRMPHz6st+ytt94Sp02bZnTM7KZHDsHLywuffPIJ/Pz8tMvu37+PChUq4PLlywgNDYVUKtWuq127Ni5dugQAaNOmDd566y3tuuzsbDx8+BD+/v4AgHPnzqFmzZqYOnUqGjVqhG7dumHv3r2FxlLc8fz8/DBo0CCTnt/rr78OhUJRbLlbt25h9+7dGDt2rEn7dyZlua4EBQXhk08+0Zt29f79+9r4AGDMmDEQBMHofTozZ68rKSkp2mblrVu3xtq1ayFygl2DnL2u5P9OUalUiImJMfoYpc2R3p/ijpdfTEwMpFIpQkNDtcvCw8ORmpqKGzduADD+HMLZOXs9AYD3338frVq1QrNmzfD5558jKyur6BfNSTl7XbH373x7UpbrCgAMGDCg0PXVq1dHp06dAABqtRqnTp3C2bNn0aVLl0L3lx+TUeSQoqOjsXHjRrz55ptISEiAl5eX3npvb28kJCRAo9EU2HbhwoVwc3PD888/DwB48OABjh49ihYtWuD48eMYPXo0Jk+ejD///NPgsU09niV988036NevH3x9fUv1OGVJWa4rR48exX//+1+MGDGixPsi56orHh4eqFmzJoYPH47jx4/jk08+wfLly7Fz584SH8sZOFNdad++PX777TccOXIEmZmZOHPmDI4dO4bExMQSH6u0ONL7k/94hvbn4eGhd3GoUqkAAE+fPi3iVaDiOFM9USgUiIyMROfOnfHf//4X33zzDfbu3Ysvv/yyiFeIcjlTXWnVqhXu3buHTZs2ITMzE1euXMGePXvs+jvfnpSlumKs3bt3o27duhgzZgwmTJiANm3aGL0tk1HkcM6dO4fXXnsNEydORIsWLQotlz+rL4oiFixYgP379+Orr76CUqnULg8PD0evXr3g6uqKvn37IiIiAgcPHjQprtJujZKQkIA9e/Zg2LBhpXqcsqQs15X//Oc/mDRpEj777DM899xzJd6fs3O2uhIeHo4NGzagSZMmUCgUaNWqFQYNGoRdu3aV+HhlnbPVlSZNmuDDDz/EggUL0Lx5c2zcuBFRUVF6d17tiaO8P4UdzxC2WLQ8Z6snFSpUwJYtW9C5c2fI5XJERERg9OjR/M43grPVlapVq+KLL77A5s2b0axZMyxYsAB9+/a12+98e1IW64oxoqKicPHiRaxatQpffvkltmzZYvS2suKLENmPY8eO4b333sOMGTMQFRUFAPD19UVsbKxeuYSEBHh7e0Miycm3ajQaTJ06FRcvXsTmzZsRGBioLVu+fHkkJCTobR8QEICHDx/i7t276Natm3b5mjVrjDpeYQztr3HjxkY996NHjyIkJEQvdipcWa4rW7duxcKFC7Fs2TK0atXKmJeDisC6khffoUOHiizj7Jy1rgwaNEiv69/s2bOLbNZvK47y/hR1vK5du+LevXsAgDfffBP16tVDcnIy1Gq19mIwN55y5cqZ9To5O9aTvPgePXoEURTZvb8QzlpXOnXqpO1+lRuHPX7n25OyWFfGjBlj9POXyWRo1KgRXnrpJWzcuNHo4QKYjCKHcf78eUyePBlLlizRO1HOncI+OztbO+ZFdHQ06tWrpy0zb948XL16FZs3b4a3t7fefqtXr47Nmzfr/RjfvXsXrVu3RkBAAKKjo/XKJyQkFHu8whjan7GOHj2Kli1bmrWtsynLdeXgwYNYvHgx1q9fj7CwMONeECqUs9aVH3/8EU+fPsVLL72kXXbjxg0mu4vgrHXlwYMHOHv2LHr27KlddvLkSYwcObLY41mTI70/RR0vf0L4yZMnEEURV65cQXh4uHZ/Xl5eCAkJMeOVcm7OWk9OnTqFCxcu4M0339Ruc+PGDQQEBDARVQhnrSuJiYk4cuQIXnjhBW18J0+eRGRkpKkvodMoq3WlOF9//TWuXr2KhQsXapcJgqA3BmWxjB7qnMiGsrKyxO7du4tbtmwpsC4jI0Ns3769uHTpUjE1NVW8cOGC2KhRI/G///2vKIqiePbsWbFx48Z6sxzoevDggVi/fn3xyy+/FNPS0sR9+/aJ4eHh4q1btwyWL+54uoydTS9XUTPhtGvXTtywYYPR+3JWZbmuJCUliU2bNhX/97//Ff0iiCJn0zOCM9eVw4cPixEREeLx48fFzMxM8cSJE2L9+vW10/+SPmeuK7GxsWLt2rXFo0ePillZWeKXX34ptmnTRkxJSSlyv9bkSO9PccczZPz48eLIkSPFx48fi/fv3xf79esnfvrppwXKcTa9ojlzPYmOjhbDw8PF3bt3i5mZmeLFixfFli1bimvWrDF6/87EmevKs2fPxMjISHHjxo1idna2+P3334uRkZHigwcPjN6/MynrdSWXodn0zp07J4aHh4s//vijmJWVJf79999i+/btxS+++MLo/QqiyI7oZP/Onj2LIUOGGJwt5uDBg0hJScFHH32ES5cuwc/PD6NGjdLe8f/ggw/w/fffF8jSNm7cGGvWrAEAnD59GnPnzsWNGzdQuXJlfPDBB2jbtm2h8fz999+FHm/37t2YMWMGACAzMxNyuRyCIKBPnz6YM2dOgX3pNrPMysqCRCKBVCpF5cqV9TLUderUwcKFC/WaZFJBZbmu7N69G5MnTzb43KKjo3HmzBntoMOZmZmQyWSQSCR68VMeZ64rQE63rDVr1uD+/fvw8/PDm2++iQEDBhjz0jkdZ68ru3fvxtKlS/H48WOEh4dj5syZdjVWnSO9P8YcL79nz57ho48+wn//+1/I5XL07NkTU6ZMgUKhMPocgpy7ngDA4cOHsXz5csTGxsLT0xNDhw7FqFGjiu3e64ycva7873//wyeffIK7d++iWrVqmD59Oho1amTkq+dcynpdye26p1arodFoIJfLtc8tICAA//nPf7B48WLExcXBz88PPXr0wLhx44ye4ZXJKCIiIiIiIiIishqmwomIiIiIiIiIyGqYjCIiIiIiIiIiIqthMoqIiIiIiIiIiKyGySgiIiIiIiIiIrIaJqOIiIiIiIiIiMhqmIwiIiIiIiIiIiKrYTKKiIiIiIiIiIishskoIiIiIiIiIiKyGiajiIiIiKzg7t27qFu3Lm7evGnrUIiIiIhsSmbrAIiIiIjKihEjRuDMmTMAALVaDY1GA7lcrl1/8OBBBAQE2Co8IiIiIrsgiKIo2joIIiIiorJm2bJlOH78OLZt22brUIiIiIjsCrvpEREREVlBXFwcQkNDcf36dQBAhw4dsHnzZgwdOhT16tXDoEGDcP/+fUycOBGRkZHo2rUrLl26pN3+1KlTGDhwICIjI9G6dWusWLHCVk+FiIiIqESYjCIiIiKykU2bNmHWrFk4evQo4uLiMGTIELzwwgv49ddfERgYiOXLlwMAHjx4gDFjxmDw4ME4e/YsVq9ejS1btmDfvn02fgZEREREpmMyioiIiMhG2rVrh5CQEPj5+SEiIgKBgYFo2bIllEolWrVqhdjYWADA/v378dxzzyEqKgpSqRShoaEYNGgQ9uzZY9snQERERGQGDmBOREREZCMVK1bUPlYqlfDw8ND7OzMzEwBw+/ZtREdHo27dutr1oigiJCTEesESERERWQiTUUREREQ2IpFIivw7l4uLC9q2bYuVK1daIywiIiKiUsVuekRERER2LigoCH///Td0J0F++PChtuUUERERkSNhMoqIiIjIzvXo0QMJCQn48ssvkZ6ejjt37mDEiBFYt26drUMjIiIiMhmTUURERER2zsfHB19++SWOHj2Kxo0b4+WXX0b79u0xYsQIW4dGREREZDJB1G3vTUREREREREREVIrYMoqIiIiIiIiIiKyGySgiIiIiIiIiIrIaJqOIiIiIiIiIiMhqmIwiIiIiIiIiIiKrYTKKiIiIiIiIiIishskoIiIiIiIiIiKyGiajiIiIiIiIiIjIapiMIiIiIiIiIiIiq2EyioiIiIiIiIiIrIbJKCIiIiIiIiIishomo4iIiIiIiIiIyGqYjCIiIiIiIiIiIqv5f1MWz2L3djEXAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Time series of momentum for a few symbols\n", + "momentum_60.plot()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. IC (Information Coefficient) Analysis\n", + "\n", + "The **IC** measures the rank correlation between factor values and subsequent returns. A consistently positive (or negative) IC indicates predictive power.\n", + "\n", + "### 5.1 Single-Period IC" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2026-02-12 20:40:58,547 - INFO - prepare_data: 411607 rows -> 412109 rows (100.1% retained)\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "IC Summary:\n", + "{1: {'mean_ic': -0.026684615700049246, 'ic_std': 0.3761825466053544, 'ic_ir': -0.07093528378934481, 't-stat': -14.36627769408405}}\n", + "\n", + "IC Series shape: (41017, 1)\n", + " period_1\n", + "start_time \n", + "1768393320000 0.369697\n", + "1768393380000 0.345455\n", + "1768393440000 -0.406061\n", + "1768393500000 0.260606\n", + "1768393560000 0.527273\n" + ] + } + ], + "source": [ + "# Use ResearchSession for streamlined analysis\n", + "signal = momentum_rank # Use ranked momentum as our signal\n", + "\n", + "analysis = session.analyze(signal, periods=1)\n", + "\n", + "print(\"IC Summary:\")\n", + "print(analysis.ic_summary)\n", + "print()\n", + "print(f\"IC Series shape: {analysis.ic_series.shape}\")\n", + "print(analysis.ic_series.head())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 5.2 Multi-Horizon IC Analysis\n", + "\n", + "To understand how quickly a factor's signal decays, we compute IC across multiple forward horizons." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2026-02-12 20:41:17,067 - INFO - prepare_data: 411607 rows -> 412109 rows (100.1% retained)\n", + "2026-02-12 20:41:18,547 - INFO - prepare_data: 411607 rows -> 412069 rows (100.1% retained)\n", + "2026-02-12 20:41:20,086 - INFO - prepare_data: 411607 rows -> 412019 rows (100.1% retained)\n", + "2026-02-12 20:41:21,566 - INFO - prepare_data: 411607 rows -> 411919 rows (100.1% retained)\n", + "2026-02-12 20:41:23,129 - INFO - prepare_data: 411607 rows -> 411519 rows (100.0% retained)\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "IC Decay Analysis:\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", + "
mean_icic_stdic_irt-stat
horizon
1-0.0266850.376183-0.070935-14.366278
5-0.0363140.383139-0.094780-19.194464
10-0.0407830.385369-0.105829-21.430799
20-0.0486770.386069-0.126085-25.529622
60-0.0415490.384516-0.108055-21.868208
\n", + "
" + ], + "text/plain": [ + " mean_ic ic_std ic_ir t-stat\n", + "horizon \n", + "1 -0.026685 0.376183 -0.070935 -14.366278\n", + "5 -0.036314 0.383139 -0.094780 -19.194464\n", + "10 -0.040783 0.385369 -0.105829 -21.430799\n", + "20 -0.048677 0.386069 -0.126085 -25.529622\n", + "60 -0.041549 0.384516 -0.108055 -21.868208" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Compute IC for multiple horizons\n", + "horizons = [1, 5, 10, 20, 60]\n", + "ic_results = {}\n", + "\n", + "for h in horizons:\n", + " result = session.analyze(signal, periods=h)\n", + " ic_results[h] = result.ic_summary.get(h, {})\n", + "\n", + "# Display IC decay table\n", + "ic_decay_df = pd.DataFrame(ic_results).T\n", + "ic_decay_df.index.name = \"horizon\"\n", + "print(\"IC Decay Analysis:\")\n", + "ic_decay_df" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA90AAAHqCAYAAAAZLi26AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAc2BJREFUeJzt3XdclXX/x/H3YasMBTcOXJCIAxeKJrnTcmSOzJHW3S5HmdowG7buzIZaav3S0ixzj7g1R5Y7zQWKZiqKJrgARTac3x/GySOgHOV4DvB6Ph4W57q+nPO54MMF73Nd3+syGI1GowAAAAAAQKFzsHUBAAAAAAAUV4RuAAAAAACshNANAAAAAICVELoBAAAAALASQjcAAAAAAFZC6AYAAAAAwEoI3QAAAAAAWAmhGwAAAAAAKyF0AwAAAABgJU62LgAAYFtTp07VtGnTtH//frm6upqWp6amau7cuQoPD1d0dLQkqWrVqurYsaOGDRsmb2/vfJ9zyZIlevnll02PHRwc5OXlpfr16+v+++9X79695ejoaLVtspZjx47pyy+/1LZt23T+/Hl5enqqTp066tu3r3r16mXr8mwiICBAkvTtt98qJCQk1/qLFy+qXbt2ysjI0Pr161WtWrU7XWKRlvP1zc+vv/6qypUr36FqAAC3gtANAMglPj5ew4cPV3x8vJ577jm1aNFCWVlZ2r17t6ZNm6aVK1fq22+/VfXq1W/4PN99951q1qyp7OxsxcXF6bffftO7776rpUuXaubMmSpTpswd2qLb98svv2jkyJFq3bq13n33XdWsWVPnz5/XqlWrNH78eG3evFkffvihrcu0idKlS2vJkiV5hu6VK1fK2dlZGRkZNqjs9sTExKhTp046fPiwTesYPHiwnnrqqTzX+fj4FOprtW/fXu+//36e30sAwK0hdAMAcnnzzTd15swZLVu2TFWqVDEtr1Onjtq0aaNevXpp2rRp+uCDD274POXKlVOFChUkSZUqVVKjRo3UrVs3DRgwQG+++ab++9//WnU7Csu5c+c0ZswYtW/fXp988okMBoMkydfXV40bN1aNGjU0adIk9enTR61bt7ZxtXdeSEiI1qxZowkTJsjd3d1s3bJly9SiRQv9+uuvNqru1u3Zs8fWJUiSSpUqZfo5sqa4uDj9/fffVn8dAChpmNMNADBz+vRprV69WsOHDzcL3DmqVq2qpUuX6r333rul569Tp44effRRrVixQmfOnDEt/+233zR48GC1bNlSTZs21eOPP66jR4+afe6xY8f01FNPqWnTpgoJCdEzzzxjOvVduhqOx48fr9atWysoKEgdOnTQ+++/r9TUVEnSBx98oODgYF25csXseffu3auAgIB8g+GPP/6o5ORkjR8/3hS4rzVo0CBt2LDBFLjHjx+vNm3amI05deqUAgIC9P3330u6egp+zmt27NhRDz74oF566SW1a9dORqPR7HN/+uknBQQE6ODBg5Kk48eP6/nnn1e7du3UqFEj9enTRxs2bMj3a25tbdq0UXZ2tv73v/+ZLT98+LAOHjyoDh065Pqco0eP6qmnnlLz5s0VFBSk7t27a+7cuWZjAgIC9OWXX+qDDz5Qq1atFBwcrHHjxiktLU0ff/yx2rRpoxYtWujll19Wenq66fOSkpL09ttvq2vXrmrYsKE6deqkWbNmmX1dO3TooHfeeUffffedOnbsqCZNmqhv377av3+/pKvTLl566SVTHePHjzd9PHnyZLM6p06dqoCAAKWlpUmShgwZoieffFLLli1Tx44d1ahRIw0cOFB///23wsPD1bVrVwUHB2vo0KGFFnJzeqJly5YKCgpS586d9cUXXyg7O9ts3NKlS9WjRw81atRInTp10qeffqrMzEzt2LFD7dq1kyQNHTrU7Hu2ZMkS9ejRQw0bNlSzZs302GOPKTIy0mz99b0MAPgXoRsAYGbnzp0yGo2655578h1TrVo1OTjc+q+Qjh07ymg0aseOHZKk33//XU8++aQqVqyo+fPn65tvvlF6eroGDx6sixcvSpISEhI0dOhQGY1GzZ07V998840uX76sRx99VCkpKZKkF198Ubt27dLnn3+utWvXauLEiVq8eLE++eQTSVL//v2VkpKiNWvWmNXz008/qUqVKrr77rvzrPf3339XQEBAnm9CSFfnrPv6+t7S12LmzJl69913NWPGDPXo0UNxcXG5jrCGh4erXr16CgwMVHx8vAYPHqyYmBhNmTJFS5cuVfPmzfXss89q+/btt1TD7SpTpozat2+vJUuWmC1ftmyZAgMDVatWLbPlFy5c0KBBg5SQkKBZs2Zp1apV6tWrl9555x19++23ZmMXLFggDw8PLViwQKNHj9ayZcv0yCOPKDMzU999951GjBih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5eDgoOrVq2vIkCEaOHBggY5GDho0yPRx6dKl5e/vr7Fjx6pfv35mp0E3b95cX331laZOnaoBAwYoOztbAQEB+vjjj01Hob29vTV37ly9//77GjBggFxcXNS0aVPNnj1bZcqUUZkyZfTOO+/os88+0/333y9/f3+9/vrrKleunHbu3KlBgwZp4cKFqlu3rqSrV1retGmT+vbtW6Cvyd13363ly5dr1qxZevPNN3Xu3DmVLVtW9evX18yZM033OM557r1792r+/Pn66quvFBwcrLfffjvPi4rlpU6dOmrQoIEOHDigUaNGma0rW7as5s+fr8mTJ+upp55ScnKyqlSpokceeUSPP/54gZ7fmnr27KmpU6fmedXyHDnfy//+978aPny40tLSVLt2bX3wwQfq3bv3bdcwdepUffzxx3rrrbd0/vx5eXl5qVOnTho9enSB51HnbMvKlSs1atQotW/fXtOmTdNrr72mCRMm6Omnn1apUqX04IMPaujQoQV6E8pamjZtqhdffFFz587VDz/8YLr6f3x8vJ577jk99NBDWrdunXr37q3MzEx9/fXXmj59uipWrKjBgwfr6aefliQ1bNhQHTt21OzZs7V48WJt2rRJvXv3VnZ2tmbPnq0ZM2bIzc1NLVu21DvvvGN2ZB0AkD+D0ZqXpwUAwE7lHPWcOXOmrUsBAADFGEe6AQAlRnp6us6dO6cFCxZo8+bNFp3yDQAAcCsI3QCAEmP//v0aMmSI/Pz8NH36dNOtowAAAKyF08sBAAAAALASrl4OAAAAAICVELoBAAAAALASQjcAAAAAAFbChdTykJmZqcTERLm6usrBgfclAAAAAADmsrOzlZaWJi8vLzk55R+tCd15SExMVHR0tK3LAAAAAADYOT8/P/n4+OS7ntCdB1dXV0lXv3ilSpWy6msZjUYlJSXJ3d1dBoPBqq+Foo9+gaXoGViCfoEl6BdYgn6BpYpCz6SkpCg6OtqUH/ND6M5DzinlpUqVUunSpa36WkajURkZGSpdurTdNhPsB/0CS9EzsAT9AkvQL7AE/QJLFaWeudmUZCYsAwAAAABgJYRuAAAAAACshNANAAAAAICVELoBAAAAALASQjcAAAAAAFZC6AYAAAAAwEoI3QAAAAAAWAmhGwAAAAAAKyF0AwAAAABgJYRuAAAAAACshNANAAAAAICVELoBAAAA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eXl5qVKlSnrooYe0ceNGSVePjN9zzz1q27atHB0d1bx5c3Xr1k3Lly83fa6vr6/at2+f53YtWrRITz75pKpVq2Z6I+LcuXPavXu3IiMjdfbsWT399NNydXVV48aNza4DcOnSJTk7O8vNzU0uLi565plnzC54V6dOHTk4OOjPP//M9+sJACi+CN0AgBJl0qRJuYLp448/Lunq6dV16tQxG1+zZk2dPn3a9NjX19f0cUhIiCIjI5WZmamdO3eqadOmaty4sf744w9lZGRo3759Cg0NlXT1yO2AAQMUHByshg0bKjw8XOnp6WavVbVqVdPHcXFxqlatmumxt7e3vLy8brhtjz76aK756p9//rlp/d9//y2j0Wi2jTVq1JAks228to68bN26VfPmzdPHH38sV1dXSQX72lWtWlUODv/+6VGqVCmlpqbm+RoJCQkqW7ZsruW1a9c2e76zZ89Kkk6ePKk1a9aYfV9XrFihuLg40/hrv3fXSkxM1OXLl82eu0yZMvLx8dHp06cVGxsrT09PeXh4mNb7+fmZPn744Yd1/PhxhYWFafz48Vq/fr3Z8zs4OMjLy0sXL17M8/UBAMWbk60LAADgTnrttdc0cODAPNddH4JzXHtk1NHR0fSxr6+vypcvr4MHD2rXrl2m8P5///d/OnjwoLy9vVWjRg0dPXpUI0eO1Lhx49S/f3+5ubnppZdeynXk2snp31/L6enpudbnzI++Vflt34228Xrnz5/XSy+9pAkTJpiF1IJ87a4N3AWR1xHprKysPMe4ublp4MCBZqd1Xy+/7brZ1yXnFPNrXfu9qFatmsLDw7Vjxw5t2LBBr7/+ulasWKHPPvvshtsCACgZONINAMA/atSooWPHjpktO3bsmKpXr57v54SEhGjnzp2KiIhQkyZN1KhRI0VERGjnzp2mU8ujoqLk4uKioUOHys3NTUaj0ewiXXmpWLGi2Rzgs2fP6tKlS7exdTJtx7XbmPNxzhHvGzEajRo7dqzatWun3r17m627la/djZQtW1bx8fG5lp88edL08enTp1WpUiXT6187916SYmNjc4XlvPj4+KhMmTJm9ScmJurChQuqUaOGKlasqKSkJF2+fNm0/trT4q9cuaKsrCyFhobqtdde08KFC7VmzRpT/dnZ2UpMTFS5cuUKuPUAgOKE0A0AwD969eqllStXau/evcrIyNCSJUt05MiRXBcUu1arVq20cOFC+fn5qXTp0nJ3d1eVKlW0dOlSU+j29fVVamqqoqKilJiYqA8//FAuLi46e/asjEZjns979913a9WqVYqOjlZSUpLZqdy3ysfHR23bttWnn36qhIQEJSYm6pNPPlFISIiqVKly08+fNWuW4uLi9Prrr+da161bN8XExGjBggXKzMzU/v37tXTpUj3wwAO3VGu9evV05MiRXMvnzJmjy5cvKzY2VgsWLFD79u0lSX379tXu3bu1ePFipaenKyoqSv369TObj58fBwcH3X///Zo1a5ZiY2OVnJysyZMnq3r16goODlbjxo3l5eWlr776Sunp6dq1a5d++eUX0+ePGDFCH3zwgZKSkpSdna09e/aobNmypukAx44dU1ZW1m3dLx0AUHQRugEA+Md9992nJ598UmPHjlVISIjmz5+vr7/+2mz+7vVCQkJ0/PhxNWvWzLSsadOmOnr0qCl0BwcHa9CgQRo8eLDuu+8++fr66pVXXtGff/6p0aNH5/m8w4YNU/v27dW/f3/de++9Cg4OVuXKlW97Gz/44AOVLl1a3bp1U/fu3eXu7m66AvjNLFiwQNHR0WrZsqXZ3OmdO3fK19dX06ZN04IFC9SiRQu99NJLGjlyZK4j4gXVqlUrbd++Pdfyjh07qnfv3urcubNq166tESNGSLp6sbKPPvpIX331lZo3b67nn39ejz32mLp3716g1xs/frzq16+vfv36qX379jp37pxmz54tR0dHubm5afr06Vq/fr1atGihadOm6dFHHzV97ttvv60TJ06oXbt2atGihebNm6fp06ebTqffsWOH/Pz8CuX7BwAoegzG/N5iBwAAsJELFy6oQ4cOmjt3rho1aqQdO3Zo6NCh2r9//20f8b/TevfurV69emn48OG2LgUAYAMc6QYAAHbHx8dHQ4cONbv6elG0bt06JSQkqH///rYuBQBgI4RuAABgl0aMGKELFy5o0aJFti7llsTHx+utt97S5MmTVaZMGVuXAwCwEU4vBwAAAADASjjSDQAAAACAlRC6AQAAAACwEkI3AAAAAABWQugGAAAAAMBKCN0AAAAAAFgJoRsAAAAAACshdAMAAAAAYCWEbgAAAAAArITQDQAAAACAlfw/p5zFhxPRM8sAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Plot IC decay curve\n", + "fig, ax = plt.subplots(figsize=(10, 5))\n", + "ax.plot(ic_decay_df.index, ic_decay_df[\"mean_ic\"], \"o-\", linewidth=2, markersize=8)\n", + "ax.axhline(y=0, color=\"gray\", linestyle=\"--\", alpha=0.5)\n", + "ax.set_xlabel(\"Forward Horizon (periods)\")\n", + "ax.set_ylabel(\"Mean IC\")\n", + "ax.set_title(\"IC Decay Curve \u2014 Momentum Factor\")\n", + "ax.grid(True, alpha=0.3)\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 6. Quantile Analysis\n", + "\n", + "Split the cross-section into quantiles by factor value and examine the mean returns of each group. A monotonic pattern (Q1 < Q2 < ... < Q5) indicates strong linear predictive power." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Use FactorAnalyzer directly for more control\n", + "analyzer = FactorAnalyzer(signal, agg, quantiles=5)\n", + "\n", + "# Prepare data: align factor values with forward returns\n", + "analyzer.prepare_data(price_col=\"close\", periods=[1])\n", + "\n", + "# Quantile returns\n", + "analyzer.plot_quantile_returns(quantiles=5, period=1)\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Cumulative returns by quantile (includes Long-Short portfolio)\n", + "analyzer.plot_cumulative_returns(quantiles=5, period=1, long_short=True)\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "ename": "ValueError", + "evalue": "Data not prepared. Call prepare_data() first.", + "output_type": "error", + "traceback": [ + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mValueError\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[19]\u001b[39m\u001b[32m, line 2\u001b[39m\n\u001b[32m 1\u001b[39m \u001b[38;5;66;03m# IC time series plot\u001b[39;00m\n\u001b[32m----> \u001b[39m\u001b[32m2\u001b[39m \u001b[43manalyzer\u001b[49m\u001b[43m.\u001b[49m\u001b[43mplot_ic\u001b[49m\u001b[43m(\u001b[49m\u001b[43mperiod\u001b[49m\u001b[43m=\u001b[49m\u001b[32;43m1\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmethod\u001b[49m\u001b[43m=\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mrank\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mplot_type\u001b[49m\u001b[43m=\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mts\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[32m 3\u001b[39m plt.show()\n", + "\u001b[36mFile \u001b[39m\u001b[32m/mnt/raid1/novis/factorium/src/factorium/factors/analyzer.py:544\u001b[39m, in \u001b[36mFactorAnalyzer.plot_ic\u001b[39m\u001b[34m(self, period, method, plot_type)\u001b[39m\n\u001b[32m 534\u001b[39m \u001b[38;5;250m\u001b[39m\u001b[33;03m\"\"\"\u001b[39;00m\n\u001b[32m 535\u001b[39m \u001b[33;03mPlot Information Coefficient (IC).\u001b[39;00m\n\u001b[32m 536\u001b[39m \n\u001b[32m (...)\u001b[39m\u001b[32m 540\u001b[39m \u001b[33;03m plot_type: 'ts' for time series, 'hist' for histogram.\u001b[39;00m\n\u001b[32m 541\u001b[39m \u001b[33;03m\"\"\"\u001b[39;00m\n\u001b[32m 542\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01m.\u001b[39;00m\u001b[34;01mplotting_analyzer\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m FactorAnalyzerPlotter\n\u001b[32m--> \u001b[39m\u001b[32m544\u001b[39m ic = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mcalculate_ic\u001b[49m\u001b[43m(\u001b[49m\u001b[43mmethod\u001b[49m\u001b[43m=\u001b[49m\u001b[43mmethod\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 545\u001b[39m col = \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mperiod_\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mperiod\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m\n\u001b[32m 546\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m col \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;129;01min\u001b[39;00m ic.columns:\n", + "\u001b[36mFile \u001b[39m\u001b[32m/mnt/raid1/novis/factorium/src/factorium/factors/analyzer.py:385\u001b[39m, in \u001b[36mFactorAnalyzer.calculate_ic\u001b[39m\u001b[34m(self, method)\u001b[39m\n\u001b[32m 375\u001b[39m \u001b[38;5;250m\u001b[39m\u001b[33;03m\"\"\"\u001b[39;00m\n\u001b[32m 376\u001b[39m \u001b[33;03mCalculate Information Coefficient (IC) for each period.\u001b[39;00m\n\u001b[32m 377\u001b[39m \n\u001b[32m (...)\u001b[39m\u001b[32m 382\u001b[39m \u001b[33;03m pd.DataFrame: IC values indexed by start_time.\u001b[39;00m\n\u001b[32m 383\u001b[39m \u001b[33;03m\"\"\"\u001b[39;00m\n\u001b[32m 384\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28mhasattr\u001b[39m(\u001b[38;5;28mself\u001b[39m, \u001b[33m\"\u001b[39m\u001b[33m_clean_data\u001b[39m\u001b[33m\"\u001b[39m):\n\u001b[32m--> \u001b[39m\u001b[32m385\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[33m\"\u001b[39m\u001b[33mData not prepared. Call prepare_data() first.\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m 387\u001b[39m period_cols = [c \u001b[38;5;28;01mfor\u001b[39;00m c \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m._clean_data.columns \u001b[38;5;28;01mif\u001b[39;00m c.startswith(\u001b[33m\"\u001b[39m\u001b[33mperiod_\u001b[39m\u001b[33m\"\u001b[39m)]\n\u001b[32m 388\u001b[39m corr_method = \u001b[33m\"\u001b[39m\u001b[33mspearman\u001b[39m\u001b[33m\"\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m method == \u001b[33m\"\u001b[39m\u001b[33mrank\u001b[39m\u001b[33m\"\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m \u001b[33m\"\u001b[39m\u001b[33mpearson\u001b[39m\u001b[33m\"\u001b[39m\n", + "\u001b[31mValueError\u001b[39m: Data not prepared. Call prepare_data() first." + ] + } + ], + "source": [ + "# IC time series plot\n", + "analyzer.plot_ic(period=1, method=\"rank\", plot_type=\"ts\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 7. Backtesting\n", + "\n", + "Run a **market-neutral vectorized backtest** using the momentum signal. The backtester:\n", + "- Converts signals to portfolio weights (cross-sectional normalization)\n", + "- Handles transaction costs\n", + "- Tracks equity, returns, and positions over time" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Market-neutral backtest\n", + "result = session.backtest(\n", + " signal,\n", + " neutralization=\"market\",\n", + " transaction_cost=0.0003,\n", + ")\n", + "\n", + "# Key performance metrics\n", + "print(\"Backtest Metrics:\")\n", + "for key, val in result.metrics.items():\n", + " if isinstance(val, float):\n", + " print(f\" {key:25s}: {val:>10.4f}\")\n", + " else:\n", + " print(f\" {key:25s}: {val}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Plot equity curve\n", + "equity = result.equity_curve.to_pandas()\n", + "equity[\"timestamp\"] = pd.to_datetime(equity[\"start_time\"], unit=\"ms\")\n", + "\n", + "fig, ax = plt.subplots(figsize=(14, 6))\n", + "ax.plot(equity[\"timestamp\"], equity[\"equity\"], linewidth=1.5)\n", + "ax.set_xlabel(\"Date\")\n", + "ax.set_ylabel(\"Portfolio Equity\")\n", + "ax.set_title(\"Momentum Factor \u2014 Equity Curve (Market Neutral)\")\n", + "ax.grid(True, alpha=0.3)\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Plot returns distribution\n", + "returns = result.returns.to_pandas()\n", + "\n", + "fig, ax = plt.subplots(figsize=(10, 5))\n", + "ax.hist(returns[\"portfolio_return\"].dropna(), bins=100, alpha=0.75, edgecolor=\"black\", linewidth=0.5)\n", + "ax.axvline(x=0, color=\"red\", linestyle=\"--\", alpha=0.7)\n", + "ax.set_xlabel(\"Return\")\n", + "ax.set_ylabel(\"Frequency\")\n", + "ax.set_title(\"Distribution of Portfolio Returns\")\n", + "ax.grid(True, alpha=0.3)\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 8. Quick Report\n", + "\n", + "`ResearchSession.quick_report()` combines IC analysis and backtesting into a single text summary \u2014 great for rapid iteration." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "report = session.quick_report(signal)\n", + "print(report)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 9. Summary & Next Steps\n", + "\n", + "In this notebook we:\n", + "- Loaded multi-symbol 1-minute data from Binance using `BinanceDataLoader`\n", + "- Built momentum factors via both **code-based** and **expression-based** methods\n", + "- Visualized factor behavior with time series, distributions, and heatmaps\n", + "- Computed IC and analyzed its decay across multiple horizons\n", + "- Ran a market-neutral backtest and examined performance metrics\n", + "\n", + "**Next notebooks to explore:**\n", + "- `02_mean_reversion_factor.ipynb` \u2014 Mean reversion with volatility normalization\n", + "- `03_data_loading_and_exploration.ipynb` \u2014 Deep dive into AggBar and data loading\n", + "- `04_multi_factor_combination.ipynb` \u2014 Combine multiple factors and compare performance" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.2" + } + }, + "nbformat": 4, + "nbformat_minor": 4 } diff --git a/src/factorium/factors/analyzer.py b/src/factorium/factors/analyzer.py index 549c7e7..7c6c152 100644 --- a/src/factorium/factors/analyzer.py +++ b/src/factorium/factors/analyzer.py @@ -240,6 +240,12 @@ def __init__(self, factor: Factor, prices: AggBar | Factor, quantiles: int = 5): else: self.prices = prices + def _ensure_data_prepared(self, periods: list[int] | None = None, price_col: str | None = None) -> None: + """Ensure data is prepared. Auto-calls prepare_data() if needed.""" + if not hasattr(self, "_clean_data"): + logger.info("Data not prepared. Auto-calling prepare_data()...") + self.prepare_data(periods=periods, price_col=price_col) + def analyze(self, price_col: str = "close", periods: int | list[int] = 1) -> FactorAnalysisResult: """ Run full factor analysis. @@ -381,8 +387,7 @@ def calculate_ic(self, method: str = "rank") -> pd.DataFrame: Returns: pd.DataFrame: IC values indexed by start_time. """ - if not hasattr(self, "_clean_data"): - raise ValueError("Data not prepared. Call prepare_data() first.") + self._ensure_data_prepared() period_cols = [c for c in self._clean_data.columns if c.startswith("period_")] corr_method = "spearman" if method == "rank" else "pearson" @@ -438,8 +443,7 @@ def calculate_turnover(self) -> pd.Series: Returns: pd.Series: Turnover time series indexed by start_time """ - if not hasattr(self, "_clean_data"): - raise ValueError("Data not prepared. Call prepare_data() first.") + self._ensure_data_prepared() # Ensure data is sorted by symbol and time for correct shift operation sorted_data = self._clean_data.sort(["symbol", "start_time"]) @@ -474,8 +478,7 @@ def calculate_quantile_returns(self, quantiles: int = 5, period: int = 1) -> pd. Returns: pd.DataFrame: Mean returns and counts per (start_time, quantile). """ - if not hasattr(self, "_clean_data"): - raise ValueError("Data not prepared. Call prepare_data() first.") + self._ensure_data_prepared(periods=[period]) col = f"period_{period}" if col not in self._clean_data.columns: From 2fb6c6d7c63b034590148a3c7923b271f085d5f1 Mon Sep 17 00:00:00 2001 From: novis10813 Date: Fri, 13 Feb 2026 11:16:25 +0800 Subject: [PATCH 03/25] fix: correct notebook column references and API usage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix equity_curve column names: start_time → end_time, equity → total_value - Fix returns column name: portfolio_return → return - Fix AggBarMetadata usage: meta.num_symbols → len(meta.symbols) - Fix Factor.plot API: .plot.plot_timeseries() → .plot() - Add jupyterlab to dev dependencies --- examples/01_momentum_factor_research.ipynb | 2101 ++++------------- examples/02_mean_reversion_factor.ipynb | 14 +- .../03_data_loading_and_exploration.ipynb | 8 +- examples/04_multi_factor_combination.ipynb | 10 +- pyproject.toml | 1 + uv.lock | 1205 +++++++++- 6 files changed, 1692 insertions(+), 1647 deletions(-) diff --git a/examples/01_momentum_factor_research.ipynb b/examples/01_momentum_factor_research.ipynb index 8a3a633..d07d074 100644 --- a/examples/01_momentum_factor_research.ipynb +++ b/examples/01_momentum_factor_research.ipynb @@ -1,1633 +1,474 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Momentum Factor Research\n", - "\n", - "This notebook walks through a **complete factor research workflow** using Factorium:\n", - "\n", - "1. Load data from Binance\n", - "2. Explore the AggBar data structure\n", - "3. Build momentum factors (code-based & expression-based)\n", - "4. Visualize factor behavior\n", - "5. Run IC (Information Coefficient) analysis\n", - "6. Analyze quantile returns\n", - "7. Run a vectorized backtest\n", - "8. Generate a quick report\n", - "\n", - "**Prerequisites**: `pip install factorium` (or `uv add factorium`)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 1. Setup & Data Loading\n", - "\n", - "We'll download 30 days of 1-minute futures data for 10 crypto symbols from Binance Vision." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "from factorium import BinanceDataLoader, ResearchSession\n", - "from factorium.factors import FactorAnalyzer, CompositeFactor\n", - "\n", - "import matplotlib.pyplot as plt\n", - "import pandas as pd\n", - "import numpy as np\n", - "\n", - "%matplotlib inline\n", - "plt.style.use(\"seaborn-v0_8-whitegrid\")\n", - "plt.rcParams[\"figure.figsize\"] = (14, 6)\n", - "plt.rcParams[\"figure.dpi\"] = 100" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2026-02-12 19:22:07,485 - INFO - Downloading 10 date ranges for 10 symbols...\n", - "2026-02-12 19:22:07,487 - ERROR - Exception in callback Task.__step()\n", - "handle: \n", - "Traceback (most recent call last):\n", - " File \"/home/novis/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/asyncio/events.py\", line 89, in _run\n", - " self._context.run(self._callback, *self._args)\n", - " ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", - "RuntimeError: cannot enter context: <_contextvars.Context object at 0x727d2837eac0> is already entered\n", - "2026-02-12 19:22:07,498 - ERROR - Exception in callback Task.__step()\n", - "handle: \n", - "Traceback (most recent call last):\n", - " File \"/home/novis/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/asyncio/events.py\", line 89, in _run\n", - " self._context.run(self._callback, *self._args)\n", - " ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", - "RuntimeError: cannot enter context: <_contextvars.Context object at 0x727d2837eac0> is already entered\n", - "2026-02-12 19:22:07,500 - INFO - Processing data for 2026-01-14\n", - "2026-02-12 19:22:07,502 - INFO - Processing data for 2026-01-15\n", - "2026-02-12 19:22:07,504 - INFO - Processing data for 2026-01-16\n", - "2026-02-12 19:22:07,506 - INFO - Processing data for 2026-01-17\n", - "2026-02-12 19:22:07,508 - INFO - Processing data for 2026-01-18\n", - "2026-02-12 19:22:07,509 - INFO - Processing data for 2026-01-14\n", - "2026-02-12 19:22:07,512 - INFO - Processing data for 2026-01-15\n", - "2026-02-12 19:22:07,513 - INFO - Processing data for 2026-01-16\n", - "2026-02-12 19:22:07,514 - INFO - Processing data for 2026-01-17\n", - "2026-02-12 19:22:07,516 - INFO - Processing data for 2026-01-18\n", - "2026-02-12 19:22:07,517 - INFO - Processing data for 2026-01-14\n", - "2026-02-12 19:22:07,519 - INFO - Processing data for 2026-01-15\n", - "2026-02-12 19:22:07,521 - INFO - Processing data for 2026-01-16\n", - "2026-02-12 19:22:07,522 - INFO - Processing data for 2026-01-17\n", - "2026-02-12 19:22:07,524 - INFO - Processing data for 2026-01-18\n", - "2026-02-12 19:22:07,525 - INFO - Processing data for 2026-01-14\n", - "2026-02-12 19:22:07,528 - INFO - Processing data for 2026-01-15\n", - "2026-02-12 19:22:07,529 - INFO - Processing data for 2026-01-16\n", - "2026-02-12 19:22:07,531 - INFO - Processing data for 2026-01-17\n", - "2026-02-12 19:22:07,532 - INFO - Processing data for 2026-01-18\n", - "2026-02-12 19:22:07,533 - INFO - Processing data for 2026-01-14\n", - "2026-02-12 19:22:07,535 - INFO - Processing data for 2026-01-15\n", - "2026-02-12 19:22:07,537 - INFO - Processing data for 2026-01-16\n", - "2026-02-12 19:22:07,538 - INFO - Processing data for 2026-01-17\n", - "2026-02-12 19:22:07,539 - INFO - Processing data for 2026-01-18\n", - "2026-02-12 19:22:07,541 - INFO - Processing data for 2026-01-14\n", - "2026-02-12 19:22:07,542 - INFO - Processing data for 2026-01-15\n", - "2026-02-12 19:22:07,544 - INFO - Processing data for 2026-01-16\n", - "2026-02-12 19:22:07,546 - INFO - Processing data for 2026-01-17\n", - "2026-02-12 19:22:07,548 - INFO - Processing data for 2026-01-18\n", - "2026-02-12 19:22:07,549 - INFO - Processing data for 2026-01-14\n", - "2026-02-12 19:22:07,551 - INFO - Processing data for 2026-01-15\n", - "2026-02-12 19:22:07,552 - INFO - Processing data for 2026-01-16\n", - "2026-02-12 19:22:07,554 - INFO - Processing data for 2026-01-17\n", - "2026-02-12 19:22:07,555 - INFO - Processing data for 2026-01-18\n", - "2026-02-12 19:22:07,557 - INFO - Processing data for 2026-01-14\n", - "2026-02-12 19:22:07,559 - INFO - Processing data for 2026-01-15\n", - "2026-02-12 19:22:07,560 - INFO - Processing data for 2026-01-16\n", - "2026-02-12 19:22:07,562 - INFO - Processing data for 2026-01-17\n", - "2026-02-12 19:22:07,564 - INFO - Processing data for 2026-01-18\n", - "2026-02-12 19:22:07,565 - INFO - Processing data for 2026-01-14\n", - "2026-02-12 19:22:07,567 - INFO - Processing data for 2026-01-15\n", - "2026-02-12 19:22:07,568 - INFO - Processing data for 2026-01-16\n", - "2026-02-12 19:22:07,570 - INFO - Processing data for 2026-01-17\n", - "2026-02-12 19:22:07,571 - INFO - Processing data for 2026-01-18\n", - "2026-02-12 19:22:07,578 - ERROR - Task was destroyed but it is pending!\n", - "task: cb=[Task.task_wakeup()]>\n", - "2026-02-12 19:22:07,580 - ERROR - Task was destroyed but it is pending!\n", - "task: .run_in_context() running at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/ipykernel/utils.py:60> wait_for= cb=[Task.__wakeup()]> cb=[ZMQStream._run_callback.._log_error() at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/zmq/eventloop/zmqstream.py:563]>\n", - "/home/novis/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/asyncio/futures.py:119: RuntimeWarning: coroutine 'Kernel.shell_main' was never awaited\n", - " def get_loop(self):\n", - "RuntimeWarning: Enable tracemalloc to get the object allocation traceback\n", - "2026-02-12 19:22:07,584 - ERROR - Task was destroyed but it is pending!\n", - "task: .run_in_context() running at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/ipykernel/utils.py:60> wait_for= cb=[Task.task_wakeup()]> cb=[ZMQStream._run_callback.._log_error() at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/zmq/eventloop/zmqstream.py:563]>\n", - "2026-02-12 19:22:07,585 - ERROR - Task was destroyed but it is pending!\n", - "task: cb=[Task.__wakeup()]>\n", - "2026-02-12 19:22:07,586 - INFO - Processing data for 2026-01-14\n", - "2026-02-12 19:22:07,587 - INFO - Processing data for 2026-01-15\n", - "2026-02-12 19:22:07,589 - INFO - Processing data for 2026-01-16\n", - "2026-02-12 19:22:07,590 - INFO - Processing data for 2026-01-17\n", - "2026-02-12 19:22:07,591 - INFO - Processing data for 2026-01-18\n", - "2026-02-12 19:22:07,592 - ERROR - Exception in callback Task.__step()\n", - "handle: \n", - "Traceback (most recent call last):\n", - " File \"/home/novis/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/asyncio/events.py\", line 89, in _run\n", - " self._context.run(self._callback, *self._args)\n", - " ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", - "RuntimeError: cannot enter context: <_contextvars.Context object at 0x727d2837eac0> is already entered\n", - "2026-02-12 19:22:07,594 - ERROR - Exception in callback Task.__step()\n", - "handle: \n", - "Traceback (most recent call last):\n", - " File \"/home/novis/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/asyncio/events.py\", line 89, in _run\n", - " self._context.run(self._callback, *self._args)\n", - " ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", - "RuntimeError: cannot enter context: <_contextvars.Context object at 0x727d2837eac0> is already entered\n", - "2026-02-12 19:22:07,596 - ERROR - Exception in callback Task.__step()\n", - "handle: \n", - "Traceback (most recent call last):\n", - " File \"/home/novis/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/asyncio/events.py\", line 89, in _run\n", - " self._context.run(self._callback, *self._args)\n", - " ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", - "RuntimeError: cannot enter context: <_contextvars.Context object at 0x727d2837eac0> is already entered\n", - "2026-02-12 19:22:07,600 - ERROR - Task was destroyed but it is pending!\n", - "task: .run_in_context() done, defined at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/ipykernel/utils.py:57> wait_for= cb=[Task.__wakeup()]> cb=[ZMQStream._run_callback.._log_error() at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/zmq/eventloop/zmqstream.py:563]>\n", - "/home/novis/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/typing.py:426: RuntimeWarning: coroutine 'Kernel.shell_main' was never awaited\n", - " @functools.wraps(func)\n", - "RuntimeWarning: Enable tracemalloc to get the object allocation traceback\n", - "2026-02-12 19:22:07,604 - ERROR - Task was destroyed but it is pending!\n", - "task: cb=[Task.__wakeup()]>\n", - "2026-02-12 19:22:07,606 - ERROR - Exception in callback Task.__step()\n", - "handle: \n", - "Traceback (most recent call last):\n", - " File \"/home/novis/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/asyncio/events.py\", line 89, in _run\n", - " self._context.run(self._callback, *self._args)\n", - " ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", - "RuntimeError: cannot enter context: <_contextvars.Context object at 0x727d2837eac0> is already entered\n", - "2026-02-12 19:22:07,613 - ERROR - Exception in callback Task.__step()\n", - "handle: \n", - "Traceback (most recent call last):\n", - " File \"/home/novis/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/asyncio/events.py\", line 89, in _run\n", - " self._context.run(self._callback, *self._args)\n", - " ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", - "RuntimeError: cannot enter context: <_contextvars.Context object at 0x727d2837eac0> is already entered\n", - "2026-02-12 19:22:07,616 - ERROR - Exception in callback Task.__step()\n", - "handle: \n", - "Traceback (most recent call last):\n", - " File \"/home/novis/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/asyncio/events.py\", line 89, in _run\n", - " self._context.run(self._callback, *self._args)\n", - " ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", - "RuntimeError: cannot enter context: <_contextvars.Context object at 0x727d2837eac0> is already entered\n", - "2026-02-12 19:22:07,617 - ERROR - Exception in callback Task.__step()\n", - "handle: \n", - "Traceback (most recent call last):\n", - " File \"/home/novis/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/asyncio/events.py\", line 89, in _run\n", - " self._context.run(self._callback, *self._args)\n", - " ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", - "RuntimeError: cannot enter context: <_contextvars.Context object at 0x727d2837eac0> is already entered\n", - "2026-02-12 19:22:07,619 - ERROR - Exception in callback Task.__step()\n", - "handle: \n", - "Traceback (most recent call last):\n", - " File \"/home/novis/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/asyncio/events.py\", line 89, in _run\n", - " self._context.run(self._callback, *self._args)\n", - " ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", - "RuntimeError: cannot enter context: <_contextvars.Context object at 0x727d2837eac0> is already entered\n", - "2026-02-12 19:22:07,621 - ERROR - Exception in callback Task.__step()\n", - "handle: \n", - "Traceback (most recent call last):\n", - " File \"/home/novis/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/asyncio/events.py\", line 89, in _run\n", - " self._context.run(self._callback, *self._args)\n", - " ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", - "RuntimeError: cannot enter context: <_contextvars.Context object at 0x727d2837eac0> is already entered\n", - "2026-02-12 19:22:07,622 - ERROR - Exception in callback Task.__step()\n", - "handle: \n", - "Traceback (most recent call last):\n", - " File \"/home/novis/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/asyncio/events.py\", line 89, in _run\n", - " self._context.run(self._callback, *self._args)\n", - " ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", - "RuntimeError: cannot enter context: <_contextvars.Context object at 0x727d2837eac0> is already entered\n", - "2026-02-12 19:22:07,629 - ERROR - Exception in callback Task.__step()\n", - "handle: \n", - "Traceback (most recent call last):\n", - " File \"/home/novis/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/asyncio/events.py\", line 89, in _run\n", - " self._context.run(self._callback, *self._args)\n", - " ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", - "RuntimeError: cannot enter context: <_contextvars.Context object at 0x727d2837eac0> is already entered\n", - "2026-02-12 19:22:07,637 - ERROR - Exception in callback Task.__step()\n", - "handle: \n", - "Traceback (most recent call last):\n", - " File \"/home/novis/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/asyncio/events.py\", line 89, in _run\n", - " self._context.run(self._callback, *self._args)\n", - " ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", - "RuntimeError: cannot enter context: <_contextvars.Context object at 0x727d2837eac0> is already entered\n", - "2026-02-12 19:22:07,651 - ERROR - Exception in callback Task.__step()\n", - "handle: \n", - "Traceback (most recent call last):\n", - " File \"/home/novis/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/asyncio/events.py\", line 89, in _run\n", - " self._context.run(self._callback, *self._args)\n", - " ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", - "RuntimeError: cannot enter context: <_contextvars.Context object at 0x727d2837eac0> is already entered\n", - "2026-02-12 19:22:07,691 - ERROR - Exception in callback Task.__step()\n", - "handle: \n", - "Traceback (most recent call last):\n", - " File \"/home/novis/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/asyncio/events.py\", line 89, in _run\n", - " self._context.run(self._callback, *self._args)\n", - " ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", - "RuntimeError: cannot enter context: <_contextvars.Context object at 0x727d2837eac0> is already entered\n", - "2026-02-12 19:22:07,721 - ERROR - Exception in callback Task.__step()\n", - "handle: \n", - "Traceback (most recent call last):\n", - " File \"/home/novis/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/asyncio/events.py\", line 89, in _run\n", - " self._context.run(self._callback, *self._args)\n", - " ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", - "RuntimeError: cannot enter context: <_contextvars.Context object at 0x727d2837eac0> is already entered\n", - "2026-02-12 19:22:07,732 - ERROR - Exception in callback Task.__step()\n", - "handle: \n", - "Traceback (most recent call last):\n", - " File \"/home/novis/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/asyncio/events.py\", line 89, in _run\n", - " self._context.run(self._callback, *self._args)\n", - " ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", - "RuntimeError: cannot enter context: <_contextvars.Context object at 0x727d2837eac0> is already entered\n", - "2026-02-12 19:22:08,793 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-01-17.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=01/day=17\n", - "2026-02-12 19:22:08,798 - INFO - Processing data for 2026-01-19\n", - "2026-02-12 19:22:09,010 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-01-16.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=01/day=16\n", - "2026-02-12 19:22:09,014 - INFO - Processing data for 2026-01-19\n", - "2026-02-12 19:22:09,332 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-01-15.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=01/day=15\n", - "2026-02-12 19:22:09,337 - INFO - Processing data for 2026-01-20\n", - "2026-02-12 19:22:09,456 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-01-17.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=01/day=17\n", - "2026-02-12 19:22:09,460 - INFO - Processing data for 2026-01-20\n", - "2026-02-12 19:22:09,587 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-01-14.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=01/day=14\n", - "2026-02-12 19:22:09,592 - INFO - Processing data for 2026-01-21\n", - "2026-02-12 19:22:09,684 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-01-18.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=01/day=18\n", - "2026-02-12 19:22:09,687 - INFO - Processing data for 2026-01-21\n", - "2026-02-12 19:22:09,789 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-01-16.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=01/day=16\n", - "2026-02-12 19:22:09,792 - INFO - Processing data for 2026-01-22\n", - "2026-02-12 19:22:09,920 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-01-19.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=01/day=19\n", - "2026-02-12 19:22:09,925 - INFO - Processing data for 2026-01-23\n", - "2026-02-12 19:22:10,030 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-01-15.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=01/day=15\n", - "2026-02-12 19:22:10,034 - INFO - Processing data for 2026-01-22\n", - "2026-02-12 19:22:10,461 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-01-19.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=01/day=19\n", - "2026-02-12 19:22:10,467 - INFO - Processing data for 2026-01-23\n", - "2026-02-12 19:22:10,788 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-01-16.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=01/day=16\n", - "2026-02-12 19:22:10,804 - INFO - Processing data for 2026-01-19\n", - "2026-02-12 19:22:11,041 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-01-18.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=01/day=18\n", - "2026-02-12 19:22:11,050 - INFO - Processing data for 2026-01-20\n", - "2026-02-12 19:22:11,081 - ERROR - Task was destroyed but it is pending!\n", - "task: .run_in_context() done, defined at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/ipykernel/utils.py:57> wait_for= cb=[Task.__wakeup()]> cb=[ZMQStream._run_callback.._log_error() at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/zmq/eventloop/zmqstream.py:563]>\n", - "/home/novis/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/pathlib/_local.py:277: RuntimeWarning: coroutine 'Kernel.shell_main' was never awaited\n", - " @property\n", - "RuntimeWarning: Enable tracemalloc to get the object allocation traceback\n", - "2026-02-12 19:22:11,087 - ERROR - Task was destroyed but it is pending!\n", - "task: cb=[Task.__wakeup()]>\n", - "2026-02-12 19:22:11,088 - ERROR - Task was destroyed but it is pending!\n", - "task: .run_in_context() done, defined at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/ipykernel/utils.py:57> wait_for= cb=[Task.__wakeup()]> cb=[ZMQStream._run_callback.._log_error() at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/zmq/eventloop/zmqstream.py:563]>\n", - "2026-02-12 19:22:11,089 - ERROR - Task was destroyed but it is pending!\n", - "task: cb=[Task.__wakeup()]>\n", - "2026-02-12 19:22:11,090 - ERROR - Task was destroyed but it is pending!\n", - "task: .run_in_context() done, defined at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/ipykernel/utils.py:57> wait_for= cb=[Task.__wakeup()]> cb=[ZMQStream._run_callback.._log_error() at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/zmq/eventloop/zmqstream.py:563]>\n", - "2026-02-12 19:22:11,091 - ERROR - Task was destroyed but it is pending!\n", - "task: cb=[Task.__wakeup()]>\n", - "2026-02-12 19:22:11,092 - ERROR - Task was destroyed but it is pending!\n", - "task: .run_in_context() done, defined at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/ipykernel/utils.py:57> wait_for= cb=[Task.__wakeup()]> cb=[ZMQStream._run_callback.._log_error() at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/zmq/eventloop/zmqstream.py:563]>\n", - "2026-02-12 19:22:11,093 - ERROR - Task was destroyed but it is pending!\n", - "task: cb=[Task.__wakeup()]>\n", - "2026-02-12 19:22:11,094 - ERROR - Task was destroyed but it is pending!\n", - "task: .run_in_context() done, defined at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/ipykernel/utils.py:57> wait_for= cb=[Task.__wakeup()]> cb=[ZMQStream._run_callback.._log_error() at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/zmq/eventloop/zmqstream.py:563]>\n", - "2026-02-12 19:22:11,095 - ERROR - Task was destroyed but it is pending!\n", - "task: cb=[Task.__wakeup()]>\n", - "2026-02-12 19:22:11,096 - ERROR - Task was destroyed but it is pending!\n", - "task: .run_in_context() done, defined at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/ipykernel/utils.py:57> wait_for= cb=[Task.__wakeup()]> cb=[ZMQStream._run_callback.._log_error() at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/zmq/eventloop/zmqstream.py:563]>\n", - "2026-02-12 19:22:11,097 - ERROR - Task was destroyed but it is pending!\n", - "task: cb=[Task.__wakeup()]>\n", - "2026-02-12 19:22:11,098 - ERROR - Task was destroyed but it is pending!\n", - "task: .run_in_context() done, defined at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/ipykernel/utils.py:57> wait_for= cb=[Task.__wakeup()]> cb=[ZMQStream._run_callback.._log_error() at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/zmq/eventloop/zmqstream.py:563]>\n", - "2026-02-12 19:22:11,099 - ERROR - Task was destroyed but it is pending!\n", - "task: cb=[Task.__wakeup()]>\n", - "2026-02-12 19:22:11,102 - ERROR - Task was destroyed but it is pending!\n", - "task: .run_in_context() done, defined at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/ipykernel/utils.py:57> wait_for= cb=[Task.__wakeup()]> cb=[ZMQStream._run_callback.._log_error() at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/zmq/eventloop/zmqstream.py:563]>\n", - "2026-02-12 19:22:11,103 - ERROR - Task was destroyed but it is pending!\n", - "task: cb=[Task.__wakeup()]>\n", - "2026-02-12 19:22:11,104 - ERROR - Task was destroyed but it is pending!\n", - "task: .run_in_context() done, defined at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/ipykernel/utils.py:57> wait_for= cb=[Task.__wakeup()]> cb=[ZMQStream._run_callback.._log_error() at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/zmq/eventloop/zmqstream.py:563]>\n", - "2026-02-12 19:22:11,106 - ERROR - Task was destroyed but it is pending!\n", - "task: cb=[Task.__wakeup()]>\n", - "2026-02-12 19:22:11,107 - ERROR - Task was destroyed but it is pending!\n", - "task: .run_in_context() done, defined at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/ipykernel/utils.py:57> wait_for= cb=[Task.__wakeup()]> cb=[ZMQStream._run_callback.._log_error() at /mnt/raid1/novis/factorium/.venv/lib/python3.13/site-packages/zmq/eventloop/zmqstream.py:563]>\n", - "2026-02-12 19:22:11,108 - ERROR - Task was destroyed but it is pending!\n", - "task: cb=[Task.__wakeup()]>\n", - "2026-02-12 19:22:11,253 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-01-18.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=01/day=18\n", - "2026-02-12 19:22:11,402 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-01-14.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=01/day=14\n", - "2026-02-12 19:22:11,413 - INFO - Processing data for 2026-01-24\n", - "2026-02-12 19:22:11,416 - INFO - Processing data for 2026-01-24\n", - "2026-02-12 19:22:11,798 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-01-18.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=01/day=18\n", - "2026-02-12 19:22:11,825 - INFO - Processing data for 2026-01-19\n", - "2026-02-12 19:22:12,162 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-01-20.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=01/day=20\n", - "2026-02-12 19:22:12,338 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-01-21.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=01/day=21\n", - "2026-02-12 19:22:12,504 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-01-20.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=01/day=20\n", - "2026-02-12 19:22:12,587 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-01-23.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=01/day=23\n", - "2026-02-12 19:22:12,875 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-01-15.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=01/day=15\n", - "2026-02-12 19:22:13,292 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-01-18.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=01/day=18\n", - "2026-02-12 19:22:13,501 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-01-18.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=01/day=18\n", - "2026-02-12 19:22:13,513 - INFO - Processing data for 2026-01-25\n", - "2026-02-12 19:22:13,514 - INFO - Processing data for 2026-01-26\n", - "2026-02-12 19:22:13,516 - INFO - Processing data for 2026-01-25\n", - "2026-02-12 19:22:13,517 - INFO - Processing data for 2026-01-27\n", - "2026-02-12 19:22:13,519 - INFO - Processing data for 2026-01-19\n", - "2026-02-12 19:22:13,520 - INFO - Processing data for 2026-01-19\n", - "2026-02-12 19:22:13,522 - INFO - Processing data for 2026-01-20\n", - "2026-02-12 19:22:13,883 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-01-14.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=01/day=14\n", - "2026-02-12 19:22:13,910 - INFO - Processing data for 2026-01-21\n", - "2026-02-12 19:22:14,152 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-01-17.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=01/day=17\n", - "2026-02-12 19:22:14,291 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-01-17.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=01/day=17\n", - "2026-02-12 19:22:14,550 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-01-18.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=01/day=18\n", - "2026-02-12 19:22:14,755 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-01-15.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=01/day=15\n", - "2026-02-12 19:22:15,050 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-01-17.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=01/day=17\n", - "2026-02-12 19:22:15,352 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-01-18.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=01/day=18\n", - "2026-02-12 19:22:15,691 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-01-16.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=01/day=16\n", - "2026-02-12 19:22:15,710 - INFO - Processing data for 2026-01-19\n", - "2026-02-12 19:22:15,712 - INFO - Processing data for 2026-01-21\n", - "2026-02-12 19:22:15,714 - INFO - Processing data for 2026-01-20\n", - "2026-02-12 19:22:15,715 - INFO - Processing data for 2026-01-22\n", - "2026-02-12 19:22:15,716 - INFO - Processing data for 2026-01-20\n", - "2026-02-12 19:22:15,718 - INFO - Processing data for 2026-01-19\n", - "2026-02-12 19:22:15,719 - INFO - Processing data for 2026-01-21\n", - "2026-02-12 19:22:15,878 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-01-17.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=01/day=17\n", - "2026-02-12 19:22:15,891 - INFO - Processing data for 2026-01-22\n", - "2026-02-12 19:22:16,111 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-01-23.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=01/day=23\n", - "2026-02-12 19:22:16,235 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-01-21.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=01/day=21\n", - "2026-02-12 19:22:16,348 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-01-22.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=01/day=22\n", - "2026-02-12 19:22:16,639 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-01-17.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=01/day=17\n", - "2026-02-12 19:22:16,736 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-01-22.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=01/day=22\n", - "2026-02-12 19:22:17,023 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-01-16.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=01/day=16\n", - "2026-02-12 19:22:17,325 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-01-17.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=01/day=17\n", - "2026-02-12 19:22:17,568 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-01-16.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=01/day=16\n", - "2026-02-12 19:22:17,905 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-01-16.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=01/day=16\n", - "2026-02-12 19:22:17,923 - INFO - Processing data for 2026-01-26\n", - "2026-02-12 19:22:17,924 - INFO - Processing data for 2026-01-27\n", - "2026-02-12 19:22:17,926 - INFO - Processing data for 2026-01-28\n", - "2026-02-12 19:22:17,927 - INFO - Processing data for 2026-01-20\n", - "2026-02-12 19:22:17,929 - INFO - Processing data for 2026-01-28\n", - "2026-02-12 19:22:17,931 - INFO - Processing data for 2026-01-21\n", - "2026-02-12 19:22:17,932 - INFO - Processing data for 2026-01-20\n", - "2026-02-12 19:22:17,934 - INFO - Processing data for 2026-01-23\n", - "2026-02-12 19:22:17,935 - INFO - Processing data for 2026-01-21\n", - "2026-02-12 19:22:18,574 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-01-17.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=01/day=17\n", - "2026-02-12 19:22:18,610 - INFO - Processing data for 2026-01-19\n", - "2026-02-12 19:22:18,697 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-01-24.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=01/day=24\n", - "2026-02-12 19:22:19,008 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-01-19.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=01/day=19\n", - "2026-02-12 19:22:19,306 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-01-15.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=01/day=15\n", - "2026-02-12 19:22:19,695 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-01-14.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=01/day=14\n", - "2026-02-12 19:22:20,201 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-01-15.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=01/day=15\n", - "2026-02-12 19:22:20,574 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-01-15.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=01/day=15\n", - "2026-02-12 19:22:20,951 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-01-15.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=01/day=15\n", - "2026-02-12 19:22:21,001 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-01-24.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=01/day=24\n", - "2026-02-12 19:22:21,021 - INFO - Processing data for 2026-01-29\n", - "2026-02-12 19:22:21,025 - INFO - Processing data for 2026-01-23\n", - "2026-02-12 19:22:21,030 - INFO - Processing data for 2026-01-22\n", - "2026-02-12 19:22:21,035 - INFO - Processing data for 2026-01-23\n", - "2026-02-12 19:22:21,038 - INFO - Processing data for 2026-01-21\n", - "2026-02-12 19:22:21,040 - INFO - Processing data for 2026-01-22\n", - "2026-02-12 19:22:21,042 - INFO - Processing data for 2026-01-22\n", - "2026-02-12 19:22:21,043 - INFO - Processing data for 2026-01-29\n", - "2026-02-12 19:22:21,389 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-01-14.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=01/day=14\n", - "2026-02-12 19:22:21,741 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-01-20.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=01/day=20\n", - "2026-02-12 19:22:21,756 - INFO - Processing data for 2026-01-24\n", - "2026-02-12 19:22:21,758 - INFO - Processing data for 2026-01-25\n", - "2026-02-12 19:22:22,386 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-01-17.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=01/day=17\n", - "2026-02-12 19:22:22,413 - INFO - Processing data for 2026-01-19\n", - "2026-02-12 19:22:22,942 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-01-16.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=01/day=16\n", - "2026-02-12 19:22:23,560 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-01-18.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=01/day=18\n", - "2026-02-12 19:22:23,595 - INFO - Processing data for 2026-01-22\n", - "2026-02-12 19:22:23,597 - INFO - Processing data for 2026-01-20\n", - "2026-02-12 19:22:23,677 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-01-27.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=01/day=27\n", - "2026-02-12 19:22:24,114 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-01-14.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=01/day=14\n", - "2026-02-12 19:22:24,138 - INFO - Processing data for 2026-01-30\n", - "2026-02-12 19:22:24,140 - INFO - Processing data for 2026-01-23\n", - "2026-02-12 19:22:24,266 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-01-25.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=01/day=25\n", - "2026-02-12 19:22:24,279 - INFO - Processing data for 2026-01-31\n", - "2026-02-12 19:22:24,402 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-01-25.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=01/day=25\n", - "2026-02-12 19:22:24,416 - INFO - Processing data for 2026-01-30\n", - "2026-02-12 19:22:24,493 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-01-26.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=01/day=26\n", - "2026-02-12 19:22:24,891 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-01-14.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=01/day=14\n", - "2026-02-12 19:22:24,913 - INFO - Processing data for 2026-02-01\n", - "2026-02-12 19:22:24,916 - INFO - Processing data for 2026-01-23\n", - "2026-02-12 19:22:25,207 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-01-19.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=01/day=19\n", - "2026-02-12 19:22:25,557 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-01-20.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=01/day=20\n", - "2026-02-12 19:22:26,036 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-01-19.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=01/day=19\n", - "2026-02-12 19:22:26,060 - INFO - Processing data for 2026-01-24\n", - "2026-02-12 19:22:26,061 - INFO - Processing data for 2026-01-25\n", - "2026-02-12 19:22:26,063 - INFO - Processing data for 2026-01-24\n", - "2026-02-12 19:22:26,458 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-01-21.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=01/day=21\n", - "2026-02-12 19:22:26,678 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-01-22.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=01/day=22\n", - "2026-02-12 19:22:26,697 - INFO - Processing data for 2026-01-26\n", - "2026-02-12 19:22:26,699 - INFO - Processing data for 2026-01-26\n", - "2026-02-12 19:22:27,513 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-01-18.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=01/day=18\n", - "2026-02-12 19:22:27,541 - INFO - Processing data for 2026-01-20\n", - "2026-02-12 19:22:28,318 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-01-14.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=01/day=14\n", - "2026-02-12 19:22:28,351 - INFO - Processing data for 2026-01-23\n", - "2026-02-12 19:22:28,572 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-01-22.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=01/day=22\n", - "2026-02-12 19:22:28,904 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-01-21.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=01/day=21\n", - "2026-02-12 19:22:28,925 - INFO - Processing data for 2026-01-27\n", - "2026-02-12 19:22:28,928 - INFO - Processing data for 2026-01-27\n", - "2026-02-12 19:22:29,055 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-01-26.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=01/day=26\n", - "2026-02-12 19:22:29,128 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-01-28.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=01/day=28\n", - "2026-02-12 19:22:29,211 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-01-27.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=01/day=27\n", - "2026-02-12 19:22:29,298 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-01-28.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=01/day=28\n", - "2026-02-12 19:22:29,602 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-01-20.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=01/day=20\n", - "2026-02-12 19:22:30,032 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-01-19.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=01/day=19\n", - "2026-02-12 19:22:30,051 - INFO - Processing data for 2026-01-31\n", - "2026-02-12 19:22:30,054 - INFO - Processing data for 2026-02-02\n", - "2026-02-12 19:22:30,056 - INFO - Processing data for 2026-02-01\n", - "2026-02-12 19:22:30,059 - INFO - Processing data for 2026-02-02\n", - "2026-02-12 19:22:30,061 - INFO - Processing data for 2026-01-24\n", - "2026-02-12 19:22:30,062 - INFO - Processing data for 2026-01-24\n", - "2026-02-12 19:22:30,418 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-01-23.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=01/day=23\n", - "2026-02-12 19:22:30,895 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-01-19.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=01/day=19\n", - "2026-02-12 19:22:30,915 - INFO - Processing data for 2026-01-28\n", - "2026-02-12 19:22:30,917 - INFO - Processing data for 2026-01-25\n", - "2026-02-12 19:22:31,106 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-01-29.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=01/day=29\n", - "2026-02-12 19:22:31,247 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-01-29.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=01/day=29\n", - "2026-02-12 19:22:31,395 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-01-24.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=01/day=24\n", - "2026-02-12 19:22:31,410 - INFO - Processing data for 2026-02-03\n", - "2026-02-12 19:22:31,412 - INFO - Processing data for 2026-02-03\n", - "2026-02-12 19:22:31,415 - INFO - Processing data for 2026-01-28\n", - "2026-02-12 19:22:31,606 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-01-23.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=01/day=23\n", - "2026-02-12 19:22:32,068 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-01-20.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=01/day=20\n", - "2026-02-12 19:22:32,098 - INFO - Processing data for 2026-01-29\n", - "2026-02-12 19:22:32,100 - INFO - Processing data for 2026-01-25\n", - "2026-02-12 19:22:32,343 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-01-25.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=01/day=25\n", - "2026-02-12 19:22:32,828 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-01-19.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=01/day=19\n", - "2026-02-12 19:22:33,407 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-01-21.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=01/day=21\n", - "2026-02-12 19:22:33,437 - INFO - Processing data for 2026-01-30\n", - "2026-02-12 19:22:33,441 - INFO - Processing data for 2026-01-24\n", - "2026-02-12 19:22:33,446 - INFO - Processing data for 2026-01-26\n", - "2026-02-12 19:22:34,840 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-01-16.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=01/day=16\n", - "2026-02-12 19:22:35,133 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-01-22.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=01/day=22\n", - "2026-02-12 19:22:35,152 - INFO - Processing data for 2026-01-21\n", - "2026-02-12 19:22:35,156 - INFO - Processing data for 2026-01-26\n", - "2026-02-12 19:22:35,443 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-01-23.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=01/day=23\n", - "2026-02-12 19:22:35,463 - INFO - Processing data for 2026-01-27\n", - "2026-02-12 19:22:35,829 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-01-22.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=01/day=22\n", - "2026-02-12 19:22:36,270 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-01-22.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=01/day=22\n", - "2026-02-12 19:22:36,708 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-01-21.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=01/day=21\n", - "2026-02-12 19:22:37,168 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-01-20.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=01/day=20\n", - "2026-02-12 19:22:37,179 - INFO - Processing data for 2026-01-27\n", - "2026-02-12 19:22:38,247 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-01-16.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=01/day=16\n", - "2026-02-12 19:22:38,283 - INFO - Processing data for 2026-01-25\n", - "2026-02-12 19:22:38,285 - INFO - Processing data for 2026-01-28\n", - "2026-02-12 19:22:38,287 - INFO - Processing data for 2026-01-26\n", - "2026-02-12 19:22:38,289 - INFO - Processing data for 2026-01-21\n", - "2026-02-12 19:22:38,950 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-01-21.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=01/day=21\n", - "2026-02-12 19:22:39,528 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-01-20.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=01/day=20\n", - "2026-02-12 19:22:40,025 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-01-21.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=01/day=21\n", - "2026-02-12 19:22:40,164 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-01-30.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=01/day=30\n", - "2026-02-12 19:22:40,169 - INFO - Processing data for 2026-01-27\n", - "2026-02-12 19:22:40,405 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-01-30.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=01/day=30\n", - "2026-02-12 19:22:40,421 - INFO - Processing data for 2026-01-25\n", - "2026-02-12 19:22:40,423 - INFO - Processing data for 2026-01-26\n", - "2026-02-12 19:22:40,425 - INFO - Processing data for 2026-02-04\n", - "2026-02-12 19:22:40,431 - INFO - Processing data for 2026-02-04\n", - "2026-02-12 19:22:40,617 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-01-24.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=01/day=24\n", - "2026-02-12 19:22:40,860 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-02-01.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=02/day=01\n", - "2026-02-12 19:22:41,115 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-01-31.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=01/day=31\n", - "2026-02-12 19:22:41,278 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-01-24.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=01/day=24\n", - "2026-02-12 19:22:41,292 - INFO - Processing data for 2026-01-28\n", - "2026-02-12 19:22:41,297 - INFO - Processing data for 2026-02-05\n", - "2026-02-12 19:22:41,301 - INFO - Processing data for 2026-02-06\n", - "2026-02-12 19:22:41,303 - INFO - Processing data for 2026-01-29\n", - "2026-02-12 19:22:41,728 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-01-22.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=01/day=22\n", - "2026-02-12 19:22:41,769 - INFO - Processing data for 2026-01-27\n", - "2026-02-12 19:22:42,083 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-01-26.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=01/day=26\n", - "2026-02-12 19:22:42,329 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-01-26.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=01/day=26\n", - "2026-02-12 19:22:42,558 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-01-25.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=01/day=25\n", - "2026-02-12 19:22:42,933 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-01-23.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=01/day=23\n", - "2026-02-12 19:22:44,627 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-01-15.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=01/day=15\n", - "2026-02-12 19:22:44,684 - INFO - Processing data for 2026-01-31\n", - "2026-02-12 19:22:44,686 - INFO - Processing data for 2026-01-30\n", - "2026-02-12 19:22:44,688 - INFO - Processing data for 2026-01-31\n", - "2026-02-12 19:22:44,690 - INFO - Processing data for 2026-01-28\n", - "2026-02-12 19:22:44,695 - INFO - Processing data for 2026-01-22\n", - "2026-02-12 19:22:45,082 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-01-23.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=01/day=23\n", - "2026-02-12 19:22:45,108 - INFO - Processing data for 2026-01-29\n", - "2026-02-12 19:22:47,075 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-01-14.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=01/day=14\n", - "2026-02-12 19:22:47,323 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-01-27.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=01/day=27\n", - "2026-02-12 19:22:47,330 - INFO - Processing data for 2026-01-23\n", - "2026-02-12 19:22:47,342 - INFO - Processing data for 2026-02-01\n", - "2026-02-12 19:22:47,666 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-01-27.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=01/day=27\n", - "2026-02-12 19:22:47,796 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-02-01.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=02/day=01\n", - "2026-02-12 19:22:47,812 - INFO - Processing data for 2026-02-01\n", - "2026-02-12 19:22:47,814 - INFO - Processing data for 2026-02-05\n", - "2026-02-12 19:22:47,968 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-02-02.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=02/day=02\n", - "2026-02-12 19:22:48,101 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-01-28.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=01/day=28\n", - "2026-02-12 19:22:48,287 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-02-03.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=02/day=03\n", - "2026-02-12 19:22:48,478 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-01-31.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=01/day=31\n", - "2026-02-12 19:22:48,667 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-01-24.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=01/day=24\n", - "2026-02-12 19:22:48,806 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-02-03.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=02/day=03\n", - "2026-02-12 19:22:48,978 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-01-28.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=01/day=28\n", - "2026-02-12 19:22:49,235 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-02-02.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=02/day=02\n", - "2026-02-12 19:22:49,252 - INFO - Processing data for 2026-02-06\n", - "2026-02-12 19:22:49,254 - INFO - Processing data for 2026-02-02\n", - "2026-02-12 19:22:49,256 - INFO - Processing data for 2026-02-07\n", - "2026-02-12 19:22:49,258 - INFO - Processing data for 2026-02-07\n", - "2026-02-12 19:22:49,261 - INFO - Processing data for 2026-01-29\n", - "2026-02-12 19:22:49,263 - INFO - Processing data for 2026-02-08\n", - "2026-02-12 19:22:49,265 - INFO - Processing data for 2026-02-02\n", - "2026-02-12 19:22:49,270 - INFO - Processing data for 2026-02-08\n", - "2026-02-12 19:22:49,590 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-01-24.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=01/day=24\n", - "2026-02-12 19:22:49,616 - INFO - Processing data for 2026-01-28\n", - "2026-02-12 19:22:49,913 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-01-25.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=01/day=25\n", - "2026-02-12 19:22:50,467 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-01-23.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=01/day=23\n", - "2026-02-12 19:22:50,478 - INFO - Processing data for 2026-01-30\n", - "2026-02-12 19:22:50,712 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-01-29.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=01/day=29\n", - "2026-02-12 19:22:50,739 - INFO - Processing data for 2026-01-29\n", - "2026-02-12 19:22:50,742 - INFO - Processing data for 2026-02-03\n", - "2026-02-12 19:22:51,026 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-01-30.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=01/day=30\n", - "2026-02-12 19:22:51,282 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-01-24.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=01/day=24\n", - "2026-02-12 19:22:51,304 - INFO - Processing data for 2026-02-04\n", - "2026-02-12 19:22:51,306 - INFO - Processing data for 2026-01-29\n", - "2026-02-12 19:22:51,797 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-01-25.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=01/day=25\n", - "2026-02-12 19:22:51,821 - INFO - Processing data for 2026-01-30\n", - "2026-02-12 19:22:52,253 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-01-26.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=01/day=26\n", - "2026-02-12 19:22:52,528 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-01-26.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=01/day=26\n", - "2026-02-12 19:22:52,553 - INFO - Processing data for 2026-01-31\n", - "2026-02-12 19:22:52,554 - INFO - Processing data for 2026-01-31\n", - "2026-02-12 19:22:52,876 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-01-27.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=01/day=27\n", - "2026-02-12 19:22:52,893 - INFO - Processing data for 2026-02-01\n", - "2026-02-12 19:22:54,346 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-01-19.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=01/day=19\n", - "2026-02-12 19:22:56,356 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-01-15.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=01/day=15\n", - "2026-02-12 19:22:56,417 - INFO - Processing data for 2026-01-24\n", - "2026-02-12 19:22:56,918 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-01-27.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=01/day=27\n", - "2026-02-12 19:22:57,052 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-02-04.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=02/day=04\n", - "2026-02-12 19:22:57,056 - INFO - Processing data for 2026-01-22\n", - "2026-02-12 19:22:57,074 - INFO - Processing data for 2026-02-01\n", - "2026-02-12 19:22:57,077 - INFO - Processing data for 2026-02-09\n", - "2026-02-12 19:22:57,370 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-02-04.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=02/day=04\n", - "2026-02-12 19:22:57,640 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-01-28.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=01/day=28\n", - "2026-02-12 19:22:57,658 - INFO - Processing data for 2026-02-09\n", - "2026-02-12 19:22:57,660 - INFO - Processing data for 2026-02-02\n", - "2026-02-12 19:22:57,944 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-01-29.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=01/day=29\n", - "2026-02-12 19:22:57,978 - INFO - Processing data for 2026-02-03\n", - "2026-02-12 19:23:00,209 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-01-14.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=01/day=14\n", - "2026-02-12 19:23:00,621 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-01-27.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=01/day=27\n", - "2026-02-12 19:23:00,640 - INFO - Processing data for 2026-01-23\n", - "2026-02-12 19:23:00,642 - INFO - Processing data for 2026-01-30\n", - "2026-02-12 19:23:00,993 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-02-06.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=02/day=06\n", - "2026-02-12 19:23:01,894 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-01-25.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=01/day=25\n", - "2026-02-12 19:23:02,545 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-01-28.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=01/day=28\n", - "2026-02-12 19:23:02,566 - INFO - Processing data for 2026-02-10\n", - "2026-02-12 19:23:02,567 - INFO - Processing data for 2026-01-31\n", - "2026-02-12 19:23:02,569 - INFO - Processing data for 2026-02-02\n", - "2026-02-12 19:23:02,864 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-01-30.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=01/day=30\n", - "2026-02-12 19:23:03,294 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-01-26.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=01/day=26\n", - "2026-02-12 19:23:03,311 - INFO - Processing data for 2026-02-04\n", - "2026-02-12 19:23:03,313 - INFO - Processing data for 2026-02-01\n", - "2026-02-12 19:23:03,757 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-01-31.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=01/day=31\n", - "2026-02-12 19:23:04,183 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-01-28.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=01/day=28\n", - "2026-02-12 19:23:04,523 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-01-31.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=01/day=31\n", - "2026-02-12 19:23:04,971 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-02-05.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=02/day=05\n", - "2026-02-12 19:23:05,065 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-02-08.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=02/day=08\n", - "2026-02-12 19:23:05,072 - INFO - Processing data for 2026-02-05\n", - "2026-02-12 19:23:05,460 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-01-27.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=01/day=27\n", - "2026-02-12 19:23:05,468 - INFO - Processing data for 2026-02-02\n", - "2026-02-12 19:23:05,470 - INFO - Processing data for 2026-02-05\n", - "2026-02-12 19:23:05,475 - INFO - Processing data for 2026-02-11\n", - "2026-02-12 19:23:05,488 - INFO - Processing data for 2026-02-10\n", - "2026-02-12 19:23:05,497 - INFO - Processing data for 2026-01-30\n", - "2026-02-12 19:23:05,632 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-02-07.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=02/day=07\n", - "2026-02-12 19:23:06,244 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-01-25.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=01/day=25\n", - "2026-02-12 19:23:06,711 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-01-26.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=01/day=26\n", - "2026-02-12 19:23:06,751 - INFO - Processing data for 2026-02-11\n", - "2026-02-12 19:23:06,940 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-02-06.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=02/day=06\n", - "2026-02-12 19:23:07,096 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-02-07.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=02/day=07\n", - "2026-02-12 19:23:07,101 - INFO - Processing data for 2026-01-31\n", - "2026-02-12 19:23:07,391 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-02-01.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=02/day=01\n", - "2026-02-12 19:23:07,494 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-02-08.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=02/day=08\n", - "2026-02-12 19:23:07,498 - INFO - Processing data for 2026-02-01\n", - "2026-02-12 19:23:07,715 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-02-05.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=02/day=05\n", - "2026-02-12 19:23:07,734 - INFO - Processing data for 2026-02-06\n", - "2026-02-12 19:23:08,067 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-02-01.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=02/day=01\n", - "2026-02-12 19:23:08,091 - INFO - Processing data for 2026-02-06\n", - "2026-02-12 19:23:08,392 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-02-02.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=02/day=02\n", - "2026-02-12 19:23:08,409 - INFO - Processing data for 2026-02-07\n", - "2026-02-12 19:23:08,959 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-01-29.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=01/day=29\n", - "2026-02-12 19:23:09,349 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-02-02.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=02/day=02\n", - "2026-02-12 19:23:09,368 - INFO - Processing data for 2026-02-03\n", - "2026-02-12 19:23:09,371 - INFO - Processing data for 2026-02-07\n", - "2026-02-12 19:23:10,964 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-01-19.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=01/day=19\n", - "2026-02-12 19:23:11,431 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-01-29.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=01/day=29\n", - "2026-02-12 19:23:11,705 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-02-03.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=02/day=03\n", - "2026-02-12 19:23:11,711 - INFO - Processing data for 2026-01-24\n", - "2026-02-12 19:23:11,729 - INFO - Processing data for 2026-02-03\n", - "2026-02-12 19:23:11,731 - INFO - Processing data for 2026-02-08\n", - "2026-02-12 19:23:12,047 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-02-04.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=02/day=04\n", - "2026-02-12 19:23:12,451 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-01-28.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=01/day=28\n", - "2026-02-12 19:23:12,476 - INFO - Processing data for 2026-02-09\n", - "2026-02-12 19:23:12,479 - INFO - Processing data for 2026-02-02\n", - "2026-02-12 19:23:12,643 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-02-09.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=02/day=09\n", - "2026-02-12 19:23:12,754 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-02-09.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=02/day=09\n", - "2026-02-12 19:23:13,223 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-01-30.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=01/day=30\n", - "2026-02-12 19:23:13,252 - INFO - Processing data for 2026-02-04\n", - "2026-02-12 19:23:13,958 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-01-29.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=01/day=29\n", - "2026-02-12 19:23:14,638 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-01-29.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=01/day=29\n", - "2026-02-12 19:23:14,659 - INFO - Processing data for 2026-02-03\n", - "2026-02-12 19:23:14,661 - INFO - Processing data for 2026-02-03\n", - "2026-02-12 19:23:16,732 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-01-20.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=01/day=20\n", - "2026-02-12 19:23:16,829 - INFO - Processing data for 2026-01-25\n", - "2026-02-12 19:23:16,948 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-02-10.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=02/day=10\n", - "2026-02-12 19:23:17,234 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-02-03.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=02/day=03\n", - "2026-02-12 19:23:17,638 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-01-24.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=01/day=24\n", - "2026-02-12 19:23:18,043 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-02-01.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=02/day=01\n", - "2026-02-12 19:23:18,070 - INFO - Processing data for 2026-02-08\n", - "2026-02-12 19:23:18,071 - INFO - Processing data for 2026-01-26\n", - "2026-02-12 19:23:18,074 - INFO - Processing data for 2026-02-05\n", - "2026-02-12 19:23:18,296 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-02-10.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=02/day=10\n", - "2026-02-12 19:23:18,381 - INFO - \u2713 Successfully processed DOTUSDT-aggTrades-2026-02-11.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOTUSDT/year=2026/month=02/day=11\n", - "2026-02-12 19:23:18,950 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-01-30.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=01/day=30\n", - "2026-02-12 19:23:19,505 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-01-31.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=01/day=31\n", - "2026-02-12 19:23:19,529 - INFO - Processing data for 2026-02-04\n", - "2026-02-12 19:23:19,530 - INFO - Processing data for 2026-02-06\n", - "2026-02-12 19:23:19,686 - INFO - \u2713 Successfully processed CAKEUSDT-aggTrades-2026-02-11.zip -> Data/market=futures_um/data_type=aggTrades/symbol=CAKEUSDT/year=2026/month=02/day=11\n", - "2026-02-12 19:23:19,953 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-02-04.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=02/day=04\n", - "2026-02-12 19:23:20,408 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-02-02.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=02/day=02\n", - "2026-02-12 19:23:20,427 - INFO - Processing data for 2026-02-09\n", - "2026-02-12 19:23:20,430 - INFO - Processing data for 2026-02-07\n", - "2026-02-12 19:23:21,292 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-01-31.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=01/day=31\n", - "2026-02-12 19:23:21,358 - INFO - Processing data for 2026-02-05\n", - "2026-02-12 19:23:21,828 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-02-05.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=02/day=05\n", - "2026-02-12 19:23:21,886 - INFO - Processing data for 2026-02-10\n", - "2026-02-12 19:23:22,188 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-02-07.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=02/day=07\n", - "2026-02-12 19:23:22,977 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-02-01.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=02/day=01\n", - "2026-02-12 19:23:23,012 - INFO - Processing data for 2026-02-11\n", - "2026-02-12 19:23:23,014 - INFO - Processing data for 2026-02-06\n", - "2026-02-12 19:23:23,199 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-02-08.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=02/day=08\n", - "2026-02-12 19:23:23,494 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-02-07.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=02/day=07\n", - "2026-02-12 19:23:23,962 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-02-05.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=02/day=05\n", - "2026-02-12 19:23:24,327 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-02-06.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=02/day=06\n", - "2026-02-12 19:23:24,352 - INFO - Processing data for 2026-02-10\n", - "2026-02-12 19:23:24,355 - INFO - Processing data for 2026-02-11\n", - "2026-02-12 19:23:24,604 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-02-09.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=02/day=09\n", - "2026-02-12 19:23:25,986 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-01-22.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=01/day=22\n", - "2026-02-12 19:23:26,676 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-01-30.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=01/day=30\n", - "2026-02-12 19:23:26,692 - INFO - Processing data for 2026-01-27\n", - "2026-02-12 19:23:27,308 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-02-01.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=02/day=01\n", - "2026-02-12 19:23:27,348 - INFO - Processing data for 2026-02-04\n", - "2026-02-12 19:23:27,350 - INFO - Processing data for 2026-02-05\n", - "2026-02-12 19:23:27,868 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-02-06.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=02/day=06\n", - "2026-02-12 19:23:28,707 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-01-31.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=01/day=31\n", - "2026-02-12 19:23:28,735 - INFO - Processing data for 2026-02-06\n", - "2026-02-12 19:23:29,481 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-02-03.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=02/day=03\n", - "2026-02-12 19:23:30,424 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-02-02.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=02/day=02\n", - "2026-02-12 19:23:31,625 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-01-30.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=01/day=30\n", - "2026-02-12 19:23:32,705 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-02-02.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=02/day=02\n", - "2026-02-12 19:23:33,364 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-02-01.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=02/day=01\n", - "2026-02-12 19:23:34,869 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-01-23.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=01/day=23\n", - "2026-02-12 19:23:34,929 - INFO - Processing data for 2026-02-08\n", - "2026-02-12 19:23:34,932 - INFO - Processing data for 2026-02-07\n", - "2026-02-12 19:23:34,934 - INFO - Processing data for 2026-02-04\n", - "2026-02-12 19:23:34,937 - INFO - Processing data for 2026-02-07\n", - "2026-02-12 19:23:34,939 - INFO - Processing data for 2026-02-05\n", - "2026-02-12 19:23:34,942 - INFO - Processing data for 2026-01-28\n", - "2026-02-12 19:23:35,444 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-01-24.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=01/day=24\n", - "2026-02-12 19:23:35,464 - INFO - Processing data for 2026-01-25\n", - "2026-02-12 19:23:35,905 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-02-04.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=02/day=04\n", - "2026-02-12 19:23:38,265 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-01-20.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=01/day=20\n", - "2026-02-12 19:23:38,455 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-02-08.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=02/day=08\n", - "2026-02-12 19:23:38,469 - INFO - Processing data for 2026-02-09\n", - "2026-02-12 19:23:38,470 - INFO - Processing data for 2026-01-26\n", - "2026-02-12 19:23:39,243 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-02-03.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=02/day=03\n", - "2026-02-12 19:23:39,270 - INFO - Processing data for 2026-02-08\n", - "2026-02-12 19:23:39,617 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-02-09.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=02/day=09\n", - "2026-02-12 19:23:40,314 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-02-02.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=02/day=02\n", - "2026-02-12 19:23:40,811 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-02-03.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=02/day=03\n", - "2026-02-12 19:23:40,957 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-02-10.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=02/day=10\n", - "2026-02-12 19:23:41,764 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-01-31.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=01/day=31\n", - "2026-02-12 19:23:41,793 - INFO - Processing data for 2026-02-06\n", - "2026-02-12 19:23:41,795 - INFO - Processing data for 2026-02-08\n", - "2026-02-12 19:23:41,796 - INFO - Processing data for 2026-02-07\n", - "2026-02-12 19:23:42,023 - INFO - \u2713 Successfully processed ADAUSDT-aggTrades-2026-02-11.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ADAUSDT/year=2026/month=02/day=11\n", - "2026-02-12 19:23:42,845 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-02-03.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=02/day=03\n", - "2026-02-12 19:23:43,236 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-02-07.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=02/day=07\n", - "2026-02-12 19:23:43,421 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-02-10.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=02/day=10\n", - "2026-02-12 19:23:43,438 - INFO - Processing data for 2026-02-08\n", - "2026-02-12 19:23:43,440 - INFO - Processing data for 2026-02-10\n", - "2026-02-12 19:23:46,196 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-01-21.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=01/day=21\n", - "2026-02-12 19:23:46,261 - INFO - Processing data for 2026-01-29\n", - "2026-02-12 19:23:46,791 - INFO - \u2713 Successfully processed AVAXUSDT-aggTrades-2026-02-11.zip -> Data/market=futures_um/data_type=aggTrades/symbol=AVAXUSDT/year=2026/month=02/day=11\n", - "2026-02-12 19:23:48,225 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-02-05.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=02/day=05\n", - "2026-02-12 19:23:48,950 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-02-06.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=02/day=06\n", - "2026-02-12 19:23:48,980 - INFO - Processing data for 2026-02-11\n", - "2026-02-12 19:23:49,919 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-02-04.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=02/day=04\n", - "2026-02-12 19:23:49,952 - INFO - Processing data for 2026-02-09\n", - "2026-02-12 19:23:50,435 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-02-08.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=02/day=08\n", - "2026-02-12 19:23:53,574 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-01-21.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=01/day=21\n", - "2026-02-12 19:23:55,460 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-01-22.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=01/day=22\n", - "2026-02-12 19:23:55,549 - INFO - Processing data for 2026-01-27\n", - "2026-02-12 19:23:55,551 - INFO - Processing data for 2026-01-28\n", - "2026-02-12 19:23:55,986 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-02-09.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=02/day=09\n", - "2026-02-12 19:23:56,637 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-02-04.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=02/day=04\n", - "2026-02-12 19:23:57,967 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-01-25.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=01/day=25\n", - "2026-02-12 19:23:58,012 - INFO - Processing data for 2026-02-09\n", - "2026-02-12 19:23:59,919 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-01-23.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=01/day=23\n", - "2026-02-12 19:23:59,965 - INFO - Processing data for 2026-01-30\n", - "2026-02-12 19:23:59,981 - INFO - Processing data for 2026-01-29\n", - "2026-02-12 19:24:00,604 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-02-07.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=02/day=07\n", - "2026-02-12 19:24:00,627 - INFO - Processing data for 2026-02-10\n", - "2026-02-12 19:24:01,077 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-02-10.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=02/day=10\n", - "2026-02-12 19:24:01,527 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-02-08.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=02/day=08\n", - "2026-02-12 19:24:01,543 - INFO - Processing data for 2026-02-11\n", - "2026-02-12 19:24:02,515 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-02-07.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=02/day=07\n", - "2026-02-12 19:24:02,547 - INFO - Processing data for 2026-02-10\n", - "2026-02-12 19:24:04,128 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-01-26.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=01/day=26\n", - "2026-02-12 19:24:04,691 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-02-08.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=02/day=08\n", - "2026-02-12 19:24:04,705 - INFO - Processing data for 2026-01-31\n", - "2026-02-12 19:24:05,180 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-02-08.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=02/day=08\n", - "2026-02-12 19:24:05,208 - INFO - Processing data for 2026-02-11\n", - "2026-02-12 19:24:05,213 - INFO - Processing data for 2026-02-09\n", - "2026-02-12 19:24:06,839 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-02-06.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=02/day=06\n", - "2026-02-12 19:24:07,562 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-02-07.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=02/day=07\n", - "2026-02-12 19:24:07,926 - INFO - \u2713 Successfully processed DOGEUSDT-aggTrades-2026-02-11.zip -> Data/market=futures_um/data_type=aggTrades/symbol=DOGEUSDT/year=2026/month=02/day=11\n", - "2026-02-12 19:24:07,935 - INFO - Processing data for 2026-02-10\n", - "2026-02-12 19:24:08,659 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-02-04.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=02/day=04\n", - "2026-02-12 19:24:08,685 - INFO - Processing data for 2026-02-11\n", - "2026-02-12 19:24:10,037 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-01-27.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=01/day=27\n", - "2026-02-12 19:24:10,077 - INFO - Processing data for 2026-02-01\n", - "2026-02-12 19:24:11,309 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-02-05.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=02/day=05\n", - "2026-02-12 19:24:12,838 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-02-05.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=02/day=05\n", - "2026-02-12 19:24:13,481 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-02-09.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=02/day=09\n", - "2026-02-12 19:24:15,150 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-02-05.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=02/day=05\n", - "2026-02-12 19:24:16,930 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-02-06.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=02/day=06\n", - "2026-02-12 19:24:17,630 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-02-09.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=02/day=09\n", - "2026-02-12 19:24:18,697 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-02-06.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=02/day=06\n", - "2026-02-12 19:24:19,195 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-02-10.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=02/day=10\n", - "2026-02-12 19:24:20,992 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-01-25.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=01/day=25\n", - "2026-02-12 19:24:21,033 - INFO - Processing data for 2026-01-30\n", - "2026-02-12 19:24:21,509 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-02-10.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=02/day=10\n", - "2026-02-12 19:24:23,076 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-01-28.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=01/day=28\n", - "2026-02-12 19:24:23,115 - INFO - Processing data for 2026-02-02\n", - "2026-02-12 19:24:23,868 - INFO - \u2713 Successfully processed BNBUSDT-aggTrades-2026-02-11.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BNBUSDT/year=2026/month=02/day=11\n", - "2026-02-12 19:24:24,384 - INFO - \u2713 Successfully processed XRPUSDT-aggTrades-2026-02-11.zip -> Data/market=futures_um/data_type=aggTrades/symbol=XRPUSDT/year=2026/month=02/day=11\n", - "2026-02-12 19:24:24,898 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-02-09.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=02/day=09\n", - "2026-02-12 19:24:25,359 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-02-10.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=02/day=10\n", - "2026-02-12 19:24:26,229 - INFO - \u2713 Successfully processed SOLUSDT-aggTrades-2026-02-11.zip -> Data/market=futures_um/data_type=aggTrades/symbol=SOLUSDT/year=2026/month=02/day=11\n", - "2026-02-12 19:24:28,515 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-01-26.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=01/day=26\n", - "2026-02-12 19:24:28,589 - INFO - Processing data for 2026-01-31\n", - "2026-02-12 19:24:30,315 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-01-28.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=01/day=28\n", - "2026-02-12 19:24:30,356 - INFO - Processing data for 2026-02-01\n", - "2026-02-12 19:24:32,527 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-01-29.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=01/day=29\n", - "2026-02-12 19:24:32,582 - INFO - Processing data for 2026-02-03\n", - "2026-02-12 19:24:34,467 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-01-27.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=01/day=27\n", - "2026-02-12 19:24:34,530 - INFO - Processing data for 2026-02-02\n", - "2026-02-12 19:24:37,668 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-01-29.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=01/day=29\n", - "2026-02-12 19:24:37,736 - INFO - Processing data for 2026-02-03\n", - "2026-02-12 19:24:41,734 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-01-30.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=01/day=30\n", - "2026-02-12 19:24:41,805 - INFO - Processing data for 2026-02-04\n", - "2026-02-12 19:24:45,114 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-02-01.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=02/day=01\n", - "2026-02-12 19:24:45,189 - INFO - Processing data for 2026-02-05\n", - "2026-02-12 19:24:49,059 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-01-31.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=01/day=31\n", - "2026-02-12 19:24:49,164 - INFO - Processing data for 2026-02-06\n", - "2026-02-12 19:24:57,912 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-01-30.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=01/day=30\n", - "2026-02-12 19:24:58,029 - INFO - Processing data for 2026-02-04\n", - "2026-02-12 19:25:04,390 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-02-01.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=02/day=01\n", - "2026-02-12 19:25:04,518 - INFO - Processing data for 2026-02-05\n", - "2026-02-12 19:25:08,908 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-02-04.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=02/day=04\n", - "2026-02-12 19:25:09,014 - INFO - Processing data for 2026-02-07\n", - "2026-02-12 19:25:13,862 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-02-03.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=02/day=03\n", - "2026-02-12 19:25:13,975 - INFO - Processing data for 2026-02-08\n", - "2026-02-12 19:25:19,822 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-02-02.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=02/day=02\n", - "2026-02-12 19:25:19,950 - INFO - Processing data for 2026-02-09\n", - "2026-02-12 19:25:25,728 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-01-31.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=01/day=31\n", - "2026-02-12 19:25:25,887 - INFO - Processing data for 2026-02-06\n", - "2026-02-12 19:25:33,790 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-02-03.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=02/day=03\n", - "2026-02-12 19:25:33,904 - INFO - Processing data for 2026-02-07\n", - "2026-02-12 19:25:43,716 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-02-06.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=02/day=06\n", - "2026-02-12 19:25:43,912 - INFO - Processing data for 2026-02-10\n", - "2026-02-12 19:25:46,978 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-02-08.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=02/day=08\n", - "2026-02-12 19:25:47,041 - INFO - Processing data for 2026-02-11\n", - "2026-02-12 19:25:52,879 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-02-07.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=02/day=07\n", - "2026-02-12 19:26:00,082 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-02-02.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=02/day=02\n", - "2026-02-12 19:26:00,261 - INFO - Processing data for 2026-02-08\n", - "2026-02-12 19:26:05,940 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-02-10.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=02/day=10\n", - "2026-02-12 19:26:12,079 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-02-04.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=02/day=04\n", - "2026-02-12 19:26:12,207 - INFO - Processing data for 2026-02-09\n", - "2026-02-12 19:26:22,562 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-02-05.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=02/day=05\n", - "2026-02-12 19:26:26,761 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-02-09.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=02/day=09\n", - "2026-02-12 19:26:32,990 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-02-07.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=02/day=07\n", - "2026-02-12 19:26:33,187 - INFO - Processing data for 2026-02-10\n", - "2026-02-12 19:26:41,338 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-02-06.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=02/day=06\n", - "2026-02-12 19:26:41,561 - INFO - Processing data for 2026-02-11\n", - "2026-02-12 19:26:43,832 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-02-08.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=02/day=08\n", - "2026-02-12 19:26:47,792 - INFO - \u2713 Successfully processed BTCUSDT-aggTrades-2026-02-11.zip -> Data/market=futures_um/data_type=aggTrades/symbol=BTCUSDT/year=2026/month=02/day=11\n", - "2026-02-12 19:27:00,270 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-02-05.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=02/day=05\n", - "2026-02-12 19:27:04,009 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-02-11.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=02/day=11\n", - "2026-02-12 19:27:07,094 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-02-10.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=02/day=10\n", - "2026-02-12 19:27:10,158 - INFO - \u2713 Successfully processed ETHUSDT-aggTrades-2026-02-09.zip -> Data/market=futures_um/data_type=aggTrades/symbol=ETHUSDT/year=2026/month=02/day=09\n", - "2026-02-12 19:27:59,836 - INFO - Loaded 411051 bars for 10 symbols\n" - ] + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Momentum Factor Research\n", + "\n", + "This notebook walks through a **complete factor research workflow** using Factorium:\n", + "\n", + "1. Load data from Binance\n", + "2. Explore the AggBar data structure\n", + "3. Build momentum factors (code-based & expression-based)\n", + "4. Visualize factor behavior\n", + "5. Run IC (Information Coefficient) analysis\n", + "6. Analyze quantile returns\n", + "7. Run a vectorized backtest\n", + "8. Generate a quick report\n", + "\n", + "**Prerequisites**: `pip install factorium` (or `uv add factorium`)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Setup & Data Loading\n", + "\n", + "We'll download 30 days of 1-minute futures data for 10 crypto symbols from Binance Vision." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from factorium import BinanceDataLoader, ResearchSession\n", + "from factorium.factors import FactorAnalyzer, CompositeFactor\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import pandas as pd\n", + "import numpy as np\n", + "\n", + "%matplotlib inline\n", + "plt.style.use(\"seaborn-v0_8-whitegrid\")\n", + "plt.rcParams[\"figure.figsize\"] = (14, 6)\n", + "plt.rcParams[\"figure.dpi\"] = 100" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "SYMBOLS = [\n", + " \"BTCUSDT\", \"ETHUSDT\", \"BNBUSDT\", \"SOLUSDT\", \"XRPUSDT\",\n", + " \"DOGEUSDT\", \"ADAUSDT\", \"AVAXUSDT\", \"DOTUSDT\", \"CAKEUSDT\",\n", + "]\n", + "\n", + "loader = BinanceDataLoader()\n", + "\n", + "agg = loader.load_aggbar(\n", + " symbols=SYMBOLS,\n", + " data_type=\"aggTrades\",\n", + " market_type=\"futures\",\n", + " futures_type=\"um\",\n", + " days=30,\n", + " bar_type=\"time\",\n", + " interval=60_000, # 1-minute bars\n", + ")\n", + "\n", + "print(f\"Loaded {len(agg):,} bars\")\n", + "print(f\"Symbols: {agg.symbols}\")\n", + "print(f\"Columns: {agg.cols}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Exploring the AggBar\n", + "\n", + "`AggBar` is a **multi-symbol OHLCV container** stored in long format. You can inspect it, convert to Polars/Pandas, and slice by time or symbols." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Summary info per symbol\n", + "agg.info()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# View raw data as Polars DataFrame\n", + "agg.to_polars().head(10)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Slice by symbols — creates a new AggBar\n", + "btc_eth = agg.slice(symbols=[\"BTCUSDT\", \"ETHUSDT\"])\n", + "print(f\"Sliced AggBar: {btc_eth.symbols}, {len(btc_eth):,} bars\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Building Momentum Factors\n", + "\n", + "### 3.1 Code-Based Factor Construction\n", + "\n", + "Extract a column from `AggBar` with `agg[\"close\"]` to get a `Factor` object, then chain time-series and cross-sectional operators." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "close = agg[\"close\"]\n", + "volume = agg[\"volume\"]\n", + "\n", + "# Simple momentum: percentage change over 60 periods\n", + "momentum_60 = close.ts_delta(60) / close.ts_shift(60)\n", + "momentum_60.name = \"momentum_60\"\n", + "\n", + "# Short-term momentum (20 periods)\n", + "momentum_20 = close.ts_delta(20) / close.ts_shift(20)\n", + "momentum_20.name = \"momentum_20\"\n", + "\n", + "# Volatility-adjusted momentum\n", + "volatility = (close.ts_delta(1) / close.ts_shift(1)).ts_std(60)\n", + "vol_adj_momentum = momentum_60 / volatility\n", + "vol_adj_momentum.name = \"vol_adj_momentum\"\n", + "\n", + "# Cross-sectional rank (normalized to [0, 1])\n", + "momentum_rank = momentum_60.cs_rank()\n", + "momentum_rank.name = \"momentum_rank\"\n", + "\n", + "print(\"Factor shapes:\")\n", + "print(f\" momentum_60: {len(momentum_60):,} rows\")\n", + "print(f\" vol_adj_momentum: {len(vol_adj_momentum):,} rows\")\n", + "print(f\" momentum_rank: {len(momentum_rank):,} rows\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 3.2 Expression-Based Factor Construction\n", + "\n", + "You can also define factors using string expressions via `ResearchSession.create_factor()`. This is useful for rapid experimentation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "session = ResearchSession(agg, default_frequency=\"1m\")\n", + "\n", + "# Same momentum factor, defined via expression\n", + "mom_expr = session.create_factor(\n", + " \"ts_delta(close, 60) / ts_shift(close, 60)\",\n", + " name=\"momentum_60_expr\",\n", + ")\n", + "\n", + "# A more complex expression: MA crossover signal\n", + "ma_cross = session.create_factor(\n", + " \"ts_mean(close, 10) - ts_mean(close, 30)\",\n", + " name=\"ma_crossover\",\n", + ")\n", + "\n", + "print(f\"Expression-based momentum: {len(mom_expr):,} rows\")\n", + "print(f\"MA crossover signal: {len(ma_cross):,} rows\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Visualizing Factors\n", + "\n", + "Use the `.plot` accessor on Factor objects to produce time series, heatmaps, and distributions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Time series of momentum for a few symbols\n", + "momentum_60.plot()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. IC (Information Coefficient) Analysis\n", + "\n", + "The **IC** measures the rank correlation between factor values and subsequent returns. A consistently positive (or negative) IC indicates predictive power.\n", + "\n", + "### 5.1 Single-Period IC" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Use ResearchSession for streamlined analysis\n", + "signal = momentum_rank # Use ranked momentum as our signal\n", + "\n", + "analysis = session.analyze(signal, periods=1)\n", + "\n", + "print(\"IC Summary:\")\n", + "print(analysis.ic_summary)\n", + "print()\n", + "print(f\"IC Series shape: {analysis.ic_series.shape}\")\n", + "print(analysis.ic_series.head())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 5.2 Multi-Horizon IC Analysis\n", + "\n", + "To understand how quickly a factor's signal decays, we compute IC across multiple forward horizons." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Compute IC for multiple horizons\n", + "horizons = [1, 5, 10, 20, 60]\n", + "ic_results = {}\n", + "\n", + "for h in horizons:\n", + " result = session.analyze(signal, periods=h)\n", + " ic_results[h] = result.ic_summary.get(h, {})\n", + "\n", + "# Display IC decay table\n", + "ic_decay_df = pd.DataFrame(ic_results).T\n", + "ic_decay_df.index.name = \"horizon\"\n", + "print(\"IC Decay Analysis:\")\n", + "ic_decay_df" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Plot IC decay curve\n", + "fig, ax = plt.subplots(figsize=(10, 5))\n", + "ax.plot(ic_decay_df.index, ic_decay_df[\"mean_ic\"], \"o-\", linewidth=2, markersize=8)\n", + "ax.axhline(y=0, color=\"gray\", linestyle=\"--\", alpha=0.5)\n", + "ax.set_xlabel(\"Forward Horizon (periods)\")\n", + "ax.set_ylabel(\"Mean IC\")\n", + "ax.set_title(\"IC Decay Curve — Momentum Factor\")\n", + "ax.grid(True, alpha=0.3)\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 6. Quantile Analysis\n", + "\n", + "Split the cross-section into quantiles by factor value and examine the mean returns of each group. A monotonic pattern (Q1 < Q2 < ... < Q5) indicates strong linear predictive power." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Use FactorAnalyzer directly for more control\n", + "analyzer = FactorAnalyzer(signal, agg, quantiles=5)\n", + "\n", + "# Prepare data: align factor values with forward returns\n", + "analyzer.prepare_data(price_col=\"close\", periods=[1])\n", + "\n", + "# Quantile returns\n", + "analyzer.plot_quantile_returns(quantiles=5, period=1)\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Cumulative returns by quantile (includes Long-Short portfolio)\n", + "analyzer.plot_cumulative_returns(quantiles=5, period=1, long_short=True)\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# IC time series plot\n", + "analyzer.plot_ic(period=1, method=\"rank\", plot_type=\"ts\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 7. Backtesting\n", + "\n", + "Run a **market-neutral vectorized backtest** using the momentum signal. The backtester:\n", + "- Converts signals to portfolio weights (cross-sectional normalization)\n", + "- Handles transaction costs\n", + "- Tracks equity, returns, and positions over time" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Market-neutral backtest\n", + "result = session.backtest(\n", + " signal,\n", + " neutralization=\"market\",\n", + " transaction_cost=0.0003,\n", + ")\n", + "\n", + "# Key performance metrics\n", + "print(\"Backtest Metrics:\")\n", + "for key, val in result.metrics.items():\n", + " if isinstance(val, float):\n", + " print(f\" {key:25s}: {val:>10.4f}\")\n", + " else:\n", + " print(f\" {key:25s}: {val}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Plot equity curve\n", + "equity = result.equity_curve.to_pandas()\n", + "equity[\"timestamp\"] = pd.to_datetime(equity[\"end_time\"], unit=\"ms\")\n", + "\n", + "fig, ax = plt.subplots(figsize=(14, 6))\n", + "ax.plot(equity[\"timestamp\"], equity[\"total_value\"], linewidth=1.5)\n", + "ax.set_xlabel(\"Date\")\n", + "ax.set_ylabel(\"Portfolio Equity\")\n", + "ax.set_title(\"Momentum Factor — Equity Curve (Market Neutral)\")\n", + "ax.grid(True, alpha=0.3)\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Plot returns distribution\n", + "returns = result.returns.to_pandas()\n", + "\n", + "fig, ax = plt.subplots(figsize=(10, 5))\n", + "ax.hist(returns[\"return\"].dropna(), bins=100, alpha=0.75, edgecolor=\"black\", linewidth=0.5)\n", + "ax.axvline(x=0, color=\"red\", linestyle=\"--\", alpha=0.7)\n", + "ax.set_xlabel(\"Return\")\n", + "ax.set_ylabel(\"Frequency\")\n", + "ax.set_title(\"Distribution of Portfolio Returns\")\n", + "ax.grid(True, alpha=0.3)\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 8. Quick Report\n", + "\n", + "`ResearchSession.quick_report()` combines IC analysis and backtesting into a single text summary — great for rapid iteration." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "report = session.quick_report(signal)\n", + "print(report)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 9. Summary & Next Steps\n", + "\n", + "In this notebook we:\n", + "- Loaded multi-symbol 1-minute data from Binance using `BinanceDataLoader`\n", + "- Built momentum factors via both **code-based** and **expression-based** methods\n", + "- Visualized factor behavior with time series, distributions, and heatmaps\n", + "- Computed IC and analyzed its decay across multiple horizons\n", + "- Ran a market-neutral backtest and examined performance metrics\n", + "\n", + "**Next notebooks to explore:**\n", + "- `02_mean_reversion_factor.ipynb` — Mean reversion with volatility normalization\n", + "- `03_data_loading_and_exploration.ipynb` — Deep dive into AggBar and data loading\n", + "- `04_multi_factor_combination.ipynb` — Combine multiple factors and compare performance" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.2" + } }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Loaded 411,051 bars\n", - "Symbols: ['ADAUSDT', 'AVAXUSDT', 'BNBUSDT', 'BTCUSDT', 'CAKEUSDT', 'DOGEUSDT', 'DOTUSDT', 'ETHUSDT', 'SOLUSDT', 'XRPUSDT']\n", - "Columns: ['symbol', 'start_time', 'end_time', 'open', 'high', 'low', 'close', 'volume', 'vwap', 'num_buyer', 'num_seller', 'num_buyer_volume', 'num_seller_volume']\n" - ] - } - ], - "source": [ - "SYMBOLS = [\n", - " \"BTCUSDT\", \"ETHUSDT\", \"BNBUSDT\", \"SOLUSDT\", \"XRPUSDT\",\n", - " \"DOGEUSDT\", \"ADAUSDT\", \"AVAXUSDT\", \"DOTUSDT\", \"CAKEUSDT\",\n", - "]\n", - "\n", - "loader = BinanceDataLoader()\n", - "\n", - "agg = loader.load_aggbar(\n", - " symbols=SYMBOLS,\n", - " data_type=\"aggTrades\",\n", - " market_type=\"futures\",\n", - " futures_type=\"um\",\n", - " days=30,\n", - " bar_type=\"time\",\n", - " interval=60_000, # 1-minute bars\n", - ")\n", - "\n", - "print(f\"Loaded {len(agg):,} bars\")\n", - "print(f\"Symbols: {agg.symbols}\")\n", - "print(f\"Columns: {agg.cols}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 2. Exploring the AggBar\n", - "\n", - "`AggBar` is a **multi-symbol OHLCV container** stored in long format. You can inspect it, convert to Polars/Pandas, and slice by time or symbols." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "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", - " \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", - "
num_kbarstart_timeend_timenum_nan
symbol
ADAUSDT411062026-01-14 11:22:002026-02-120
AVAXUSDT411062026-01-14 11:22:002026-02-120
BNBUSDT411062026-01-14 11:22:002026-02-120
BTCUSDT411062026-01-14 11:22:002026-02-120
CAKEUSDT410972026-01-14 11:22:002026-02-120
DOGEUSDT411062026-01-14 11:22:002026-02-120
DOTUSDT411062026-01-14 11:22:002026-02-120
ETHUSDT411062026-01-14 11:22:002026-02-120
SOLUSDT411062026-01-14 11:22:002026-02-120
XRPUSDT411062026-01-14 11:22:002026-02-120
\n", - "
" - ], - "text/plain": [ - " num_kbar start_time end_time num_nan\n", - "symbol \n", - "ADAUSDT 41106 2026-01-14 11:22:00 2026-02-12 0\n", - "AVAXUSDT 41106 2026-01-14 11:22:00 2026-02-12 0\n", - "BNBUSDT 41106 2026-01-14 11:22:00 2026-02-12 0\n", - "BTCUSDT 41106 2026-01-14 11:22:00 2026-02-12 0\n", - "CAKEUSDT 41097 2026-01-14 11:22:00 2026-02-12 0\n", - "DOGEUSDT 41106 2026-01-14 11:22:00 2026-02-12 0\n", - "DOTUSDT 41106 2026-01-14 11:22:00 2026-02-12 0\n", - "ETHUSDT 41106 2026-01-14 11:22:00 2026-02-12 0\n", - "SOLUSDT 41106 2026-01-14 11:22:00 2026-02-12 0\n", - "XRPUSDT 41106 2026-01-14 11:22:00 2026-02-12 0" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Summary info per symbol\n", - "agg.info()" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "shape: (10, 13)
symbolstart_timeend_timeopenhighlowclosevolumevwapnum_buyernum_sellernum_buyer_volumenum_seller_volume
stri64i64f64f64f64f64f64f64decimal[38,0]decimal[38,0]f64f64
"ADAUSDT"176838972000017683897800000.41630.41630.4160.4162422468.00.4160893450247651.0174817.0
"AVAXUSDT"1768389720000176838978000014.56714.56714.55414.5578092.014.55769420344859.03233.0
"BNBUSDT"17683897200001768389780000931.8931.8931.28931.56231.68931.5168789298141.7489.94
"BTCUSDT"1768389720000176838978000094951.594951.694892.694910.053.57894925.47540630556325.43828.14
"CAKEUSDT"176838972000017683897800002.03912.03912.03792.03814596.02.03859130283832.0764.0
"DOGEUSDT"176838972000017683897800000.146850.146850.146730.146792.758362e60.14678262431.479317e61.279045e6
"DOTUSDT"176838972000017683897800002.2572.2572.2522.25561027.12.255454262120337.840689.3
"ETHUSDT"176838972000017683897800003295.73296.03294.423295.612396.2643295.5500975285471155.6971240.567
"SOLUSDT"17683897200001768389780000144.03144.03143.82143.8828761.51143.91300614615517777.7410983.77
"XRPUSDT"176838972000017683897800002.12782.12792.12532.1261371037.52.12683196110193592.5177445.0
" - ], - "text/plain": [ - "shape: (10, 13)\n", - "\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n", - "\u2502 symbol \u2506 start_time \u2506 end_time \u2506 open \u2506 \u2026 \u2506 num_buyer \u2506 num_selle \u2506 num_buyer \u2506 num_selle \u2502\n", - "\u2502 --- \u2506 --- \u2506 --- \u2506 --- \u2506 \u2506 --- \u2506 r \u2506 _volume \u2506 r_volume \u2502\n", - "\u2502 str \u2506 i64 \u2506 i64 \u2506 f64 \u2506 \u2506 decimal[3 \u2506 --- \u2506 --- \u2506 --- \u2502\n", - "\u2502 \u2506 \u2506 \u2506 \u2506 \u2506 8,0] \u2506 decimal[3 \u2506 f64 \u2506 f64 \u2502\n", - "\u2502 \u2506 \u2506 \u2506 \u2506 \u2506 \u2506 8,0] \u2506 \u2506 \u2502\n", - "\u255e\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u256a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u256a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u256a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u256a\u2550\u2550\u2550\u256a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u256a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u256a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u256a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2561\n", - "\u2502 ADAUSDT \u2506 1768389720 \u2506 1768389780 \u2506 0.4163 \u2506 \u2026 \u2506 34 \u2506 50 \u2506 247651.0 \u2506 174817.0 \u2502\n", - "\u2502 \u2506 000 \u2506 000 \u2506 \u2506 \u2506 \u2506 \u2506 \u2506 \u2502\n", - "\u2502 AVAXUSDT \u2506 1768389720 \u2506 1768389780 \u2506 14.567 \u2506 \u2026 \u2506 20 \u2506 34 \u2506 4859.0 \u2506 3233.0 \u2502\n", - "\u2502 \u2506 000 \u2506 000 \u2506 \u2506 \u2506 \u2506 \u2506 \u2506 \u2502\n", - "\u2502 BNBUSDT \u2506 1768389720 \u2506 1768389780 \u2506 931.8 \u2506 \u2026 \u2506 92 \u2506 98 \u2506 141.74 \u2506 89.94 \u2502\n", - "\u2502 \u2506 000 \u2506 000 \u2506 \u2506 \u2506 \u2506 \u2506 \u2506 \u2502\n", - "\u2502 BTCUSDT \u2506 1768389720 \u2506 1768389780 \u2506 94951.5 \u2506 \u2026 \u2506 305 \u2506 563 \u2506 25.438 \u2506 28.14 \u2502\n", - "\u2502 \u2506 000 \u2506 000 \u2506 \u2506 \u2506 \u2506 \u2506 \u2506 \u2502\n", - "\u2502 CAKEUSDT \u2506 1768389720 \u2506 1768389780 \u2506 2.0391 \u2506 \u2026 \u2506 30 \u2506 28 \u2506 3832.0 \u2506 764.0 \u2502\n", - "\u2502 \u2506 000 \u2506 000 \u2506 \u2506 \u2506 \u2506 \u2506 \u2506 \u2502\n", - "\u2502 DOGEUSDT \u2506 1768389720 \u2506 1768389780 \u2506 0.14685 \u2506 \u2026 \u2506 62 \u2506 43 \u2506 1.479317e \u2506 1.279045e \u2502\n", - "\u2502 \u2506 000 \u2506 000 \u2506 \u2506 \u2506 \u2506 \u2506 6 \u2506 6 \u2502\n", - "\u2502 DOTUSDT \u2506 1768389720 \u2506 1768389780 \u2506 2.257 \u2506 \u2026 \u2506 26 \u2506 21 \u2506 20337.8 \u2506 40689.3 \u2502\n", - "\u2502 \u2506 000 \u2506 000 \u2506 \u2506 \u2506 \u2506 \u2506 \u2506 \u2502\n", - "\u2502 ETHUSDT \u2506 1768389720 \u2506 1768389780 \u2506 3295.7 \u2506 \u2026 \u2506 528 \u2506 547 \u2506 1155.697 \u2506 1240.567 \u2502\n", - "\u2502 \u2506 000 \u2506 000 \u2506 \u2506 \u2506 \u2506 \u2506 \u2506 \u2502\n", - "\u2502 SOLUSDT \u2506 1768389720 \u2506 1768389780 \u2506 144.03 \u2506 \u2026 \u2506 146 \u2506 155 \u2506 17777.74 \u2506 10983.77 \u2502\n", - "\u2502 \u2506 000 \u2506 000 \u2506 \u2506 \u2506 \u2506 \u2506 \u2506 \u2502\n", - "\u2502 XRPUSDT \u2506 1768389720 \u2506 1768389780 \u2506 2.1278 \u2506 \u2026 \u2506 96 \u2506 110 \u2506 193592.5 \u2506 177445.0 \u2502\n", - "\u2502 \u2506 000 \u2506 000 \u2506 \u2506 \u2506 \u2506 \u2506 \u2506 \u2502\n", - "\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# View raw data as Polars DataFrame\n", - "agg.to_polars().head(10)" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Sliced AggBar: ['BTCUSDT', 'ETHUSDT'], 82,212 bars\n" - ] - } - ], - "source": [ - "# Slice by symbols \u2014 creates a new AggBar\n", - "btc_eth = agg.slice(symbols=[\"BTCUSDT\", \"ETHUSDT\"])\n", - "print(f\"Sliced AggBar: {btc_eth.symbols}, {len(btc_eth):,} bars\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 3. Building Momentum Factors\n", - "\n", - "### 3.1 Code-Based Factor Construction\n", - "\n", - "Extract a column from `AggBar` with `agg[\"close\"]` to get a `Factor` object, then chain time-series and cross-sectional operators." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Factor shapes:\n", - " momentum_60: 411,607 rows\n", - " vol_adj_momentum: 414,943 rows\n", - " momentum_rank: 411,607 rows\n" - ] - } - ], - "source": [ - "close = agg[\"close\"]\n", - "volume = agg[\"volume\"]\n", - "\n", - "# Simple momentum: percentage change over 60 periods\n", - "momentum_60 = close.ts_delta(60) / close.ts_shift(60)\n", - "momentum_60.name = \"momentum_60\"\n", - "\n", - "# Short-term momentum (20 periods)\n", - "momentum_20 = close.ts_delta(20) / close.ts_shift(20)\n", - "momentum_20.name = \"momentum_20\"\n", - "\n", - "# Volatility-adjusted momentum\n", - "volatility = (close.ts_delta(1) / close.ts_shift(1)).ts_std(60)\n", - "vol_adj_momentum = momentum_60 / volatility\n", - "vol_adj_momentum.name = \"vol_adj_momentum\"\n", - "\n", - "# Cross-sectional rank (normalized to [0, 1])\n", - "momentum_rank = momentum_60.cs_rank()\n", - "momentum_rank.name = \"momentum_rank\"\n", - "\n", - "print(\"Factor shapes:\")\n", - "print(f\" momentum_60: {len(momentum_60):,} rows\")\n", - "print(f\" vol_adj_momentum: {len(vol_adj_momentum):,} rows\")\n", - "print(f\" momentum_rank: {len(momentum_rank):,} rows\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 3.2 Expression-Based Factor Construction\n", - "\n", - "You can also define factors using string expressions via `ResearchSession.create_factor()`. This is useful for rapid experimentation." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Expression-based momentum: 411,607 rows\n", - "MA crossover signal: 411,607 rows\n" - ] - } - ], - "source": [ - "session = ResearchSession(agg, default_frequency=\"1min\")\n", - "\n", - "# Same momentum factor, defined via expression\n", - "mom_expr = session.create_factor(\n", - " \"ts_delta(close, 60) / ts_shift(close, 60)\",\n", - " name=\"momentum_60_expr\",\n", - ")\n", - "\n", - "# A more complex expression: MA crossover signal\n", - "ma_cross = session.create_factor(\n", - " \"ts_mean(close, 10) - ts_mean(close, 30)\",\n", - " name=\"ma_crossover\",\n", - ")\n", - "\n", - "print(f\"Expression-based momentum: {len(mom_expr):,} rows\")\n", - "print(f\"MA crossover signal: {len(ma_cross):,} rows\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 4. Visualizing Factors\n", - "\n", - "Use the `.plot` accessor on Factor objects to produce time series, heatmaps, and distributions." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAABKMAAAJOCAYAAABr8MR3AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzs3Xd4G1XWBvB3JLm3xOmkE0J6g5DQS4Cl7W4gLEvZXXpbFj76Upe29N4SOgESYEMgCSWhpZNOilPtFJe4d8uWrD4z3x+yNTPqtmVJjt/f8+SJrBmNrkajKWfOPVeQZVkGERERERERERFRFOhi3QAiIiIiIiIiIuo+GIwiIiIiIiIiIqKoYTCKiIiIiIiIiIiihsEoIiIiIiIiIiKKGgajiIiIiIiIiIgoahiMIiIiIiIiIiKiqGEwioiIiIiIiIiIoobBKCIiIiIiIiIiihoGo4iIiIiIiIiIKGoMsW4AERERRceDDz6IxYsXB51n2rRpuOSSS/DQQw9h2bJlGDFiRJRap5WTk4NPP/0UOTk5qKmpQUJCAkaMGIHLLrsMl19+eUTeY9SoUbjppptw3333RWR5FN/y8vLw/PPPIycnB8nJyTjjjDPw8MMPIysryzPP8uXLMWfOHBw8eBBpaWk455xz8OCDDyI9PT2GLSciIjryCLIsy7FuBBEREXU+k8kEm83m+fvxxx/H3r178fXXX3ueS0hIQHJyMkwmE7Kzs6HX66Pezs2bN+O6667DBRdcgGuuuQZ9+vRBXV0dFi9ejPnz5+PBBx/Edddd1+H3qampQWpqKtLS0iLQamr15ptvory8HM8//3ysm+JRUlKCWbNm4fzzz8cNN9yA6upqPPjggxg2bBg+/vhjAMCmTZtw3XXX4YYbbsBf//pXlJaW4sknn8TgwYPx4YcfxvgTEBERHVmYGUVERNRNZGRkICMjw/N3UlIS9Ho9+vTp4zNvcnJyNJum8eWXX6Jfv354+eWXIQgCAGDAgAEYP348bDYb9u7dG5H38fe5qeN27NiBfv36xboZGu+88w4GDRqEp556CoIgYNiwYXj77bdRUVEBWZYhCALmzJmDSZMmeTLlhgwZgkcffRQ33ngjduzYgSlTpsT4UxARER05WDOKiIiINBYtWoRRo0YhPz8fgLt73x//+EesWbMGF154ISZMmICLL74Yubm52LhxI2bOnIlJkybh0ksvRV5enmZZ3377LS677DIcd9xxmDZtGu6++25UVVUFfX+bzQZRFOF0On2mPfPMM3j55Zc9f8uyjE8++QQzZ87E5MmTcfLJJ+Oxxx5DU1OTZ54HH3wQM2fOxJdffolp06bhhRdeAODupqdeVk1NDf79739jxowZmDBhAi666CJN1hgA/Prrr7j00ktx3HHH4bjjjsMVV1yBDRs2BFx3/nRkfS5atAh/+tOfMGHCBBx//PG44YYbsGfPHp/337lzJ/7xj39g0qRJOPPMM/Hdd9+hvLwc119/PaZMmYKzzz4by5Yt0yx7586duOGGG3DyySdj8uTJ+Nvf/obt27d7pm/evBmjRo3C5s2bce+992Lq1KmYPn06HnjgAVgsFgDAjBkzsGHDBixevNgzb6B1MmPGDNx9990AgNLSUowaNQpLlizBAw88gKlTp3q+K7vdjsceewzTpk3DSSedhBdffDHguvVHlmUsX74cF154oSe4CQBjx47F2WefDUEQYLfbsXXrVpxxxhma15544olITEzE2rVr2/SeREREFByDUURERBRSQ0MD5s2bh1deeQXz589HfX09/v3vf2POnDl4+umnMW/ePNTU1OCZZ57xvObbb7/Fv//9b0yePBmLFi3CnDlzUFBQgGuvvRYOhyPge51++umoqqrC3/72N/z8888wmUwB533nnXfw/PPP46KLLsJ3332H559/HuvWrcPtt9/u0/7ly5dj3rx5uOWWW3yW43A4cM0112Dbtm144okn8P3332PmzJl49NFHsWTJEgBAYWEh7rrrLpx33nn49ttvsXDhQowfPx4333wzKioqAAAXXngh1q1bh2HDhkV8fX799dd46KGHcM4552DJkiX45JNP4HQ6cfXVV6OyslKz/BdeeAE333wzlixZguHDh+Oxxx7Dww8/jL///e9YtGiRJ+unubnZ89muueYaiKKIDz74AAsWLED//v1x/fXX+wSRnn/+eZx00klYvHgx7r33XixZsgTz58/3tDE7OxsXXHAB1q1b1+ZsonfffRdTpkzBokWLcNlll+Hjjz/Gtddei6OPPhoLFy7EpZdeio8++ghbtmwJe5llZWVobGxE37598fjjj+O0007DqaeeiieffNITRDt8+DBEUcSQIUM0r01ISMBRRx2FgoKCNn0OIiIiCo7BKCIiIgqptrYW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/NCnfo/agw8+iHHjxuHrr7/G0qVL0dDQgOTkZIwcORIPPPAALr/8cs+8t9xyC9LS0vD555/jxRdfRGJiIk444QR8/vnnbQp2JCYm4pNPPsHLL7+Me++9F42Njejduzcuuugi/N///R8A9yh/VqsVH374IZ566ikkJCTgmGOOwRtvvOGpObRs2TI89NBDWLZsGUaMGNHONeffxRdfDEmSMHfuXLz77rtITk7GtGnT8Mwzz7QpC8yfoUOHYt68eXjttddw9dVXw+l0YtiwYXjggQdw5ZVXtmlZt9xyC5555hlceeWVeO6553DBBRfgqaeewuzZs/HnP//Zs9w333yzQ21ui6lTp+K9997D66+/josuuggZGRmYNWuWZzQ9wJ0d9t577+HVV1/1jJB4wQUX4L777otaO4mIiLoLQW5LBUgiIiIiIiIiIqIOYDc9IiIiIiIiIiKKGnbTIyIiIqIu57HHHsP3338fcr4dO3ZEoTVERETUFuymR0RERERdTl1dHcxmc8j5hg4dGoXWEBERUVswGEVERERERERERFHDmlFERERERERERBQ1cR2MKisrw80334zp06fjrLPOwksvvQRJkvzO29zcjPvuuw+jRo1Cfn6+ZtqMGTMwfvx4TJgwwfPv1ltvjcZHICIiIiIiIiIilbguYH7HHXdg3LhxWL58Oerq6nDLLbegd+/euO666zTzVVVV4eqrr8bkyZMDLuujjz7C9OnTw3pfl8uFxsZGJCUlQaeL63gdEREREREREVGbSZIEu92OrKwsGAzRDQ/FbTBq9+7dyMvLw9y5c5GRkYGMjAxce+21+PTTT32CUQ0NDbj//vsxevRoLFmypMPv3djYiKKiog4vh4iIiIiIiIgong0bNgy9evWK6nvGbTBq7969GDhwILKysjzPjRs3DoWFhTCbzUhPT/c8P3r0aIwePRqlpaUBl/fZZ5/hkUceQV1dHU477TQ8/vjjAVd2UlISAPcXkpycHKFPREcCWZY9258gCLFuDsUxbisULm4rFC5uK/GN3w+Fi9sKhYPbCYWrI9uKzWZDUVGRJwYSTXEbjDIajcjMzNQ81xqYamho0ASjQhkzZgwmTpyIF198EU1NTXjggQdw5513Yv78+X7nb+2a53K54HQ62/kJ6EgkyzJEUYTT6eRBgYLitkLh4rZC4eK2Et/4/VC4uK1QOLidULg6sq24XC4AiEl5orgNRgHulRoJs2fP9jxOS0vD448/jgsvvBDFxcUYMmRIwNelp6cjNTU1Im2gI0PrNpmVlcWDAgXFbYXCxW2FwsVtJb7x+6FwcVuhcHA7oXB1ZFuxWCyd0aSwxG0wKjs7G0ajUfOc0WiEIAjIzs7u0LIHDhwIAKiurg4ajBIEgT988tG6XXDboFC4rVC4uK1QuLitxDd+PxQubisUDm4nFK72biux3Lbidqi48ePHo6KiAvX19Z7ndu/ejWOOOQZpaWlhL6esrAyPP/44HA6H57n8/HwAwODBgyPXYCIiIiIiIiIiCilug1Fjx47FhAkT8Morr8BsNiM/Px9z587FlVdeCQA4//zzsXXr1pDL6dWrF1auXInnn38eFosFVVVVeO6553DWWWehX79+nf0xiIiIiIiIiIhIJW6DUQDw5ptvorq6GqeccgquvvpqXHzxxbjqqqsAAIWFhZ7+jXPmzMGECRNw/vnnAwBmzpyJCRMmYM6cOUhOTsaHH36IwsJCnH766bjoooswePBgvPjiizH7XERERERERERE3ZUgR6pK+BHEYrEgNzcXY8aMYQFz0pBlGY2NjSwkSCFxW6FwcVuhcHFbiW/8fihc3FYoHNxOKFwd2VZiGfuI68woIiIiIiIiIiI6sjAYRUREREREREREUcNgFBERERERERERRQ2DUUREREREREREFDUMRhERERERERERUdQwGEVERERERERERFHDYBQREREREREREUUNg1FERERERERERBQ1DEYREREREREREVHUMBhFRERERERERERRw2AUERERERERERFFDYNRREREREREREQUNQxGERERERERERFR1DAYRURERERE1E2ZTLmoqV0R62YQUTdjiHUDiIiIiIiIKDa2/P5HAMC0E75DRsa4GLeGiLoLZkYRERERERF1c83Nh2LdBCLqRhiMIiIiIiIi6uZkWYx1E4ioG2EwioiIiIiIqNuTY90AIupGGIwiIiIiIiIiIqKoYTCKiIiIiIiIiIiihsEoIiIiIiIiIiKKGgajiIiIiIiIiIgoahiMIiIiIiIi6uZkFjAnoihiMIqIiIiIiIiIiKKGwSgiIiIiIiIiIooaBqOIiIiIiIiIiChqGIwiIiIiIiIiIqKoYTCKiIiIiIiou2P9ciKKIgajiIiIiIiIiIgoahiMIiIiIiIi6uZcdbZYN4GIuhEGo4iIiIiIiLo5qdkR6yYQUTfCYBQREREREVE3J8ssGkVE0cNgFBERERERERERRQ2DUUREREREREREFDUMRhERERERERERUdQwGEVERERERERERFHDYBQREREREREREUUNg1FERERERERERBQ1DEYREREREREREVHUMBhFRERERERERERRw2AUERERERFRtyfHugFE1I0wGEVERERERERERFHDYBQREREREREREUUNg1FERERERERERBQ1DEYREREREREREVHUMBhFRERERERERERRw2AUERERERFRt8fR9IgoehiMIiIiIiIiIiKiqGEwioiIiIiIiIiIoobBKCIiIiIiIiIiihoGo4iIiIiIiIiIKGoYjCIiIiIiIurmZBYwJ6IoYjCKiIiIiIiIiIiihsEoIiIiIiKibk6AEOsmEFE3wmAUERERERFRN8duekQUTQxGERERERERdXOSFOsWEFF3wmAUERERERFRN1dutMS6CUTUjTAYRURERERE1M1JMrvpEVH0MBhFRERERETU7bGAORFFD4NRRERERERE3R4zo4goehiMIiIiIiIiIiKiqGEwioiIiIiIiIiIoobBKCIiIiIiou6OvfSIKIoYjCIiIiIiIuruWL+ciKKIwSgiIiIiIqJuTpaZGkVE0cNgFBERERERUTcnu5yxbgIRdSMMRhEREREREXVzrubGWDeBiLqRuA5GlZWV4eabb8b06dNx1lln4aWXXoIkSX7nbW5uxn333YdRo0YhPz9fM81oNOKuu+7CySefjFNPPRWPPPIIbDZbND4CERERERFR/GMvPSKKorgORt1xxx3o168fli9fjrlz52L58uX49NNPfearqqrCrFmzoNfr/S7nP//5D6xWK3744Qd88803yM/Px8svv9zZzSciIiIiIuoSWL+ciKIpboNRu3fvRl5eHu677z5kZGRg2LBhuPbaa7FgwQKfeRsaGnD//ffjjjvu8JlWW1uL5cuX4+6770Z2djb69euH2267Dd988w2cTvaLJiIiIiIikpkaRURRFLfBqL1792LgwIHIysryPDdu3DgUFhbCbDZr5h09ejTOOeccv8vJzc2FXq/HqFGjNMuxWCwoKCjonMYTERERERF1AbswGd9jJoNRRBRVhlg3IBCj0YjMzEzNc62BqYaGBqSnp4e9nPT0dAiCkniqXk4wsixziFPSaN0muF1QKNxWKFzcVihc3FbiG78fCle8bSsvCP8BAAxKWo0/xEmbKP62E4pfHdlWYrl9xW0wCojcimnvcsxmM7vykYYsy7BYLACgCXASeeO2QuHitkLh4rYS3/j9ULjidVup1aWgsZEj6sWLeN1OKP50ZFux2+2d0aSwxG0wKjs7G0ajUfOc0WiEIAjIzs5u03LMZjNEUfQUOG9dbq9evYK+Nj09HampqW1qNx3ZWgObWVlZPChQUNxWKFzcVihc3FbiG78fCle8biuCIGhKpFBsxet2QvGnI9tKaxArFuI2GDV+/HhUVFSgvr7eE3zavXs3jjnmGKSlpYW9nDFjxkCWZeTl5WHcuHGe5WRmZmL48OFBXysIAn/45KN1u+C2QaFwW6FwcVuhcHFbiW/8fihc8bqtxFt7urt43U4o/rR3W4nlthW3BczHjh2LCRMm4JVXXoHZbEZ+fj7mzp2LK6+8EgBw/vnnY+vWrSGXk52djfPOOw+vv/466uvrUVlZidmzZ+Mvf/kLDIa4jcURERERERF1im8q63FXbjGcEusREVFsxG0wCgDefPNNVFdX45RTTsHVV1+Niy++GFdddRUAoLCw0JNSNmfOHEyYMAHnn38+AGDmzJmYMGEC5syZAwB46qmnkJGRgbPPPht//vOfMXHiRNx9992x+VBEREREREQx9K/cYvyvsh4LK+tj3RQi6qbiOjWof//++OCDD/xO279/v+fxbbfdhttuuy3gcjIyMvDqq69GvH1ERERERERdVZ3TpfzBnmBEFEVxnRlFREREREREUcAee0QURQxGERERERERERFR1DAYRURERERE1A1pkqFYzJyIoojBKCIiIiIiom5OFhmMIqLoYTCKiIiIiIiom9NL9lg3gYi6EQajiIiIiIiIuiFZkwzFzCgiih4Go4iIiIiIiLo5IdYNIKJuhcEoIiIiIiIiIiKKGgajiIiIiIiIujv20iOiKGIwioiIiIiIiIiIoobBKCIiIiIiom5I1qRDMTWKiKKHwSgiIiIiIiIiIooaBqOIiIiIiIiIiChqGIwiIiIiIiLqhtQd8xKQFLN2EFH3w2AUERERERFRN5eM5Fg3gYi6EQajiIiIiIiIiIgoahiMIiIiIiIi6obU3fScicZYNYOIuiEGo4iIiIiIiLo5W6+8WDeBiLoRBqOIiIiIiIiIiChqGIwiIiIiIiIiIqKoYTCKiIiIiIiIiIiihsEoIiIiIiKibkiWQ89DRNQZGIwiIiIiIiLqJhqcrlg3gYiIwSgiIiIiIqLu4mCzLdZNICJiMIqIiIiIiKi7kAM8JiKKJgajiIiIiIiIujkzMmLdBCLqRhiMIiIiIiIi6uZ+w5mxbgIRdSMMRhEREREREXUT2m56yl+nYk30G0NE3RaDUURERERERN2QOjB1SBoVs3YQUffDYBQREREREVE3JKuiUdv0J8SuIUTU7TAYRUREREREREREUcNgFBERERERUTckh56FiKhTMBhFRERERETUTagDUJLMcBQRxQaDUURERERERN2QXWIwiohig8EoIiIiIiKiboihKCKKFQajiIiIiIiIugl1zzwXu+kRUYwwGEVERERERNQNfVxWG+smEFE3xWAUERERERERERFFDYNRREREREREREQUNQxGERERERERdXPnGtfFuglE1I0wGEVERERERNRNyAHG0EuUnVFuCRF1ZwxGERERERERdROBxs8TBF4aElH0cI9DRERERETU7QUKUxERRR6DUURERERE4ShYA7wyGtj/U6xbQkRE1KUxGEVEREREFI7P/gyYKoAvL491S4gi4qTUROUPJkYRURQxGEVEREREFIZa9MBCXIgq9Ip1U4giIllQIlAOnRTDlhBRd2OIdQOIiIiIiLqC+ZgFI7JwAMPxSKwbQ9ROsioDSlI/FqLfFiLqvpgZRUREREQUBiOyAABOJIaYk6hrUOdCMRZFRNHEYBQRERERER0xTCYTPvzwQ+zYsSPWTYl7sipNysDUKCKKInbTIyIiIiKiI8bKlStRWlqK0tJSTJkyJdbNiTvqOuV5dtHzWGBuFBFFETOjiIiIiIjoiGG322PdhC5DDD0LEVGnYDCKiIiIiIiOGDodL3GCMYlKCErW5EkREUUP99RERERERHTEEAR2NwvmiUPlnsdyJ8WiSm0OVNudnbNwIjoisGYUEREREREdMRiMCq7E5vA87oxYlMklYurGfQCAyrMmd8I7ENGRgJlRRERERERE3Zw+tSkiyylVB7s6K/WKiLo8BqOIiIiIiIi6OWtCYkSW83JRpeexFJElEtGRiMEoIiIiIiKibknJXFounI9msePj6y2tafQ8lpgYRUQBMBhFRERERETUDXn3oiu2OvzP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UimCgoIwevRoTJ06FbNnz0ZSUpJJxyTLE0TOC0xERETkaATkncMJArNRRGRdTEY5IVEUcefOHezZswc9e/Y0apsTJ04gOzsbAwYMKLAuIiICM2bM0OsaZ4yQkBBs3LgRt2/f1i5TKBSIioqCSqUqctsBAwZAFEWcOHHCpGMSERERERHwRO6h8xeTUURkXRwzygJEUYSYlmbVYwquribfwZgzZw7mzZunHReqefPmGDJkiFHbxsXFISAgwOSEU1E+++wzTJw4EZ07d0ZwcDCaNGmCdu3aoV27dtoxogojk8kQFBSEuLg4i8VD5hF5J42IiIjI4aRIOfEMEdkOk1ElJIoibr00BGm//27V47o2aICq/7fRpITU9OnTMXjwYAA53e42bNiAqKgo7N27Fz4+PsVur9FozI7XkFq1auGHH37ApUuXcOrUKZw+fRrvvPMOQkNDsX79eri7uxe5vVqtLjZpRURERERExeHNRSKyLnbTswQHbBni5eWFt956C3K5HD/++GOx5YODg3H37l2kpqYWWU4ulwMA0tPTC6x79uwZlEplgeV16tTBqFGjsGrVKuzduxc3btzQDk5emJSUFMTGxqJatWrFxk5ERERERERE9oMto0pIEARU/b+NDtFNrzAZGRnFlmnRogVcXFywfv16vPHGG3rr/v77b4wfPx5btmyBn58fVCoV/vzzT4SHh2vLiKKIixcvonnz5tpttm3bhg8++AASSV5OtFq1aqhSpQrSink9v/nmG3h6emr3R7bDAcyJiIiIiIjIFExGWYAgCBDc3GwdhkkyMjKwadMmPH36FB07diy2vIeHBz744AN8+OGHEAQBQ4YMgUKhwKlTp/Dhhx+iZ8+e8PLyAgCMHDkSS5YsQZUqVdCoUSMkJSVh5cqVePjwIUaOHAkA8PPzw759+5Ceno4xY8agUqVKSElJwa5duxAbG4s2bdoYjOPp06fYtm0b1q5di0WLFsHFxcVyLwqZRSYp2NqNiIiIiByIA/b0ICLHxmSUE8kdwBwAlEolateujdWrVyMoKMio7fv164fy5ctj1apV+PrrryEIAoKDgzFhwgRERUVpy73++uuoUKECFixYgNjYWLi7u6NevXpYt24dfH19AQC+vr7YtGkTli9fjoEDByIhIQFKpRIRERFYu3Ytatasqd3fmjVrsG7dOm3cDRo0wLp16xAZ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JbV48WIcOHAAdevWNSme0aNHY/369Th06BAyMjKQlJSEb775BqdOncL48eMBAL6+vvjll1/w3nvv4caNG9BoNEhLS8P+/ftx6tQpdOjQweC+k5OTsXXrVsybNw9TpkwptisfEREREVGZl80W7URkO+ym50TmzJmDefPmQRRFuLu7o3nz5ti+fTtq1KgBAGjevDn+7//+DytWrED37t2RlpYGf39/dOvWDaNHj4arq6t2X88//zwqVKiAr7/+Gt999x3S09Ph4+ODRo0aYcOGDYiMjNQ7tqEBzAFg1apVaNasGXr27AlPT0+sXLkS06dPh1wuR1hYGNauXYuQkBAAObP6bdiwAcuWLcNrr72GJ0+eQCKRICwsDJ9//jlat26t3e/Bgwdx5MgRAIBMJkOdOnXwxRdfoG3bthZ9TYmIiIiIHBPHjCIi2xFEUeS3UD6pqamIiYlBWFgY3NzcbB0O2RFRFJGYmAiVSmWxrpKO7vi0/yFEnfNaVPm0dTGlnQfrChmLdYWMxbpi3/j+kLHspa4c2f4ihHLnAAAdO1y3WRxkmL3UE7J/Jakrtsx9sJseERERERERERFZDZNRRFQilb1dbB0CEREREZmMHWSIyHaYjCKiEvFwkds6BCIiIiIiInIgTEYRUclw2DkiIiIixyPwHI6IbIfJKCIiIiIiIiIishomo4iIiIiIiIiIyGqYjCKikuFUs0REREQOiN30iMh2mIwiIiIiIiJyOkxGEZHtMBlFRERERERERERWw2QUERERERERERFZjczWAZB1dOjQAfHx8ZBIcvKPfn5+aNq0KUaOHIkaNWpoy126dAkrV67E2bNnkZaWhvLly6NLly5444034OXlBQCoW7eutnxWVhYkEgmkUikAoHLlyjh06BCmTJmCjIwMLF68WC+OjIwMREREYP369WjatCkA4Oeff8aqVavw999/IzU1FZUqVcKAAQMwatQoCIKA3377DcOGDYNCoQAASKVSBAcHo1u3bnjllVfg4uKCu3fvolu3btrjZGZmQiaTaZ9v48aNsWbNGku/rAQAIpt4ExERETkaV4UE6bYOgoicFpNRTmT69OkYPHgwsrKycPv2bezYsQP9+vXDypUr0bx5c5w8eRJvvfUWxowZgzlz5kClUuHGjRuYP38+Bg8ejK1bt8LDwwPR0dHafQ4dOhT16tXDpEmTzIrp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FYMHDwYAVK1aFePGjcO0adOQlJRkcJuBAwciJCQEc+bMMelY4eHhaNWqFSZPnozr169DrVbj+vXrePfdd9GsWTPtWFVubm5YuHAhNmzYgMTERAA541R99tlnCAgIQK1atQzu//bt25g4cSJiY2Mxbdo0k2IjIiIiIiIiIttiNz0ncODAARw7dgz79u3Ta1E1fPhw/Pjjj5g7d67BllaCIGDmzJno168f/vvf/6J9+/ZGH3P58uVYvXo1xowZg/j4eJQvXx5t2rTB+PHjtcdq0aIFVqxYgTVr1mDFihVITk6Gt7c3mjdvjo0bN0KhUGj3N2bMGAiCAFEU4e3tjbZt22Lnzp2oVKlSCV4ZIiIiIiIiIrI2QRRF0dZB2JvU1FTExMQgLCwMbm5utg6H7IgoikhMTIRKpTK5qyQ5F9YVMhbrChmLdcW+8f0hY7GukDFYT8hYJakrtsx9sJseERERERERERFZDZNRRERERERERERkNUxGERERERERERGR1TAZRUREREREREREVsNkFBERERERERERWQ2TUUREREREREREZDVMRhERERERERERkdUwGUVERERERERERFbDZBQREREREREREVkNk1FERERERERERGQ1TEYREREREREREZHVMBlFRERERERERERWw2QUERERERERERFZDZNRRERERERERERkNTJbB2CPNBoNACAtLc3GkZC9EUURGRkZSE1NhSAItg6H7BjrChmLdYWMxbpi3/j+kLFYV8gYrCdkrJLUldycR24OxJqYjDIgIyMDABAbG2vbQIiIiIiIiIiISlFGRgY8PDysekxBFEXRqkd0ANnZ2UhMTIRSqYREwp6MRERERERERFS2aDQaZGRkQKVSQSazblslJqOIiIiIiIiIiMhq2OyHiIiIiIiIiIishskoIiIiIiIiIiKyGiajyGHcvXsXb731Fpo2bYoWLVpgypQpSEpKAgDExMTg5ZdfRsOGDdGlSxesWbNGb9v//Oc/6N27NyIjI9G1a1ds27ZNb/3169cxdOhQ1KtXD23btsV3331XZCzFHS8rKwvz589HrVq18L///c+o57d7925ERkZi4cKFestHjBiBunXr6v0LCwvD8uXLjdqvMyrLdeXKlSt45ZVX0KhRI7Rp0wZz585FZmamdr0oivj2229Rp04dbN68udj9OTtnrSu//fYbQkNDC3y3/Pjjj8Xu11k5a10BgP3796NXr16oX78+evTogRMnThS7T2tzpPenuOPll5CQgPHjx6NFixZo1aoVpk2bhvT0dL0yhZ1DkD5nrSdxcXEGv/O//fZbY142p+SsdQUATp48iQEDBiAyMhIdO3bE7t27i3m1nFtZrisAEB0djc6dO+PFF18ssO7HH3/Unh+0b98eCxYsQHZ2drH71BKJHETPnj3FKVOmiMnJyeL9+/fFF154Qfzggw/EtLQ0sXXr1uKyZcvElJQU8dKlS2KTJk3EQ4cOiaIoin/88YdYt25d8fDhw2JWVpb4008/ieHh4eKZM2dEURTFtLQ0sV27duKqVavE1NRU8Y8//hB79OghXrt2zWAcxR0vJSVF7N+/vzhlyhSxZs2a4s8//1zsc/v444/Ffv36ic8//7y4YMGCIssmJiaKLVu2FK9cuWLKy+dUympdSU5OFlu2bCkuWrRIzMjIEK9duya2b99eXLFihbbMqFGjxJEjR4rNmzcXN23aZImXs0xz1rry66+/iu3bt7fUy+gUnLWunD59Wqxdu7b4n//8R8zIyBCPHDkiNmjQQLx7966lXlqLcJT3p7jjGfL222+Lr7/+uvj48WPxwYMH4sCBA8XZs2dr15tyDuHsnLWe3LlzR6xZs6YlX8oyz1nrys2bN8U6deqIGzduFDMyMsQzZ86ITZo0ES9cuGDJl7dMKct1Zc+ePWLbtm3F1157TRwwYIDeuujoaDEiIkL86aefRLVaLf71119i8+bNxe+++87o147JKHIIiYmJ4pQpU8SHDx9ql23YsEHs0qWL+OOPP4rNmjUTs7OztesWLFggjhgxQhRFUfz555/F5cuX6+2vb9++4ldffSWKoiju2rVL7Nmzp9GxFHe8hw8fips3bxZFUTQ6GfX111+LGRkZ4ssvv1zsieTMmTPFjz/+2Oh4nU1Zriu3bt0Sp0yZImZlZWmXffrpp+Krr76q/XvFihWiRqMR27dvz2RUMZy5rjAZZRpnriuffvqpOHz4cL1txo0bJ65cudLomEubI70/xR0vv4cPH4q1atUSY2JitMt+/vlnsX79+mJmZqYoiqadQzgzZ64nTEaZxpnrysaNG8VOnTrpbTN//nxxxowZRsfsTMpyXRFFUdy2bZv44MEDcenSpQWSUdeuXRMPHz6st+ytt94Sp02bZnTM7KZHDsHLywuffPIJ/Pz8tMvu37+PChUq4PLlywgNDYVUKtWuq127Ni5dugQAaNOmDd566y3tuuzsbDx8+BD+/v4AgHPnzqFmzZqYOnUqGjVqhG7dumHv3r2FxlLc8fz8/DBo0CCTnt/rr78OhUJRbLlbt25h9+7dGDt2rEn7dyZlua4EBQXhk08+0Zt29f79+9r4AGDMmDEQBMHofTozZ68rKSkp2mblrVu3xtq1ayFygl2DnL2u5P9OUalUiImJMfoYpc2R3p/ijpdfTEwMpFIpQkNDtcvCw8ORmpqKGzduADD+HMLZOXs9AYD3338frVq1QrNmzfD5558jKyur6BfNSTl7XbH373x7UpbrCgAMGDCg0PXVq1dHp06dAABqtRqnTp3C2bNn0aVLl0L3lx+TUeSQoqOjsXHjRrz55ptISEiAl5eX3npvb28kJCRAo9EU2HbhwoVwc3PD888/DwB48OABjh49ihYtWuD48eMYPXo0Jk+ejD///NPgsU09niV988036NevH3x9fUv1OGVJWa4rR48exX//+1+MGDGixPsi56orHh4eqFmzJoYPH47jx4/jk08+wfLly7Fz584SH8sZOFNdad++PX777TccOXIEmZmZOHPmDI4dO4bExMQSH6u0ONL7k/94hvbn4eGhd3GoUqkAAE+fPi3iVaDiOFM9USgUiIyMROfOnfHf//4X33zzDfbu3Ysvv/yyiFeIcjlTXWnVqhXu3buHTZs2ITMzE1euXMGePXvs+jvfnpSlumKs3bt3o27duhgzZgwmTJiANm3aGL0tk1HkcM6dO4fXXnsNEydORIsWLQotlz+rL4oiFixYgP379+Orr76CUqnULg8PD0evXr3g6uqKvn37IiIiAgcPHjQprtJujZKQkIA9e/Zg2LBhpXqcsqQs15X//Oc/mDRpEj777DM899xzJd6fs3O2uhIeHo4NGzagSZMmUCgUaNWqFQYNGoRdu3aV+HhlnbPVlSZNmuDDDz/EggUL0Lx5c2zcuBFRUVF6d17tiaO8P4UdzxC2WLQ8Z6snFSpUwJYtW9C5c2fI5XJERERg9OjR/M43grPVlapVq+KLL77A5s2b0axZMyxYsAB9+/a12+98e1IW64oxoqKicPHiRaxatQpffvkltmzZYvS2suKLENmPY8eO4b333sOMGTMQFRUFAPD19UVsbKxeuYSE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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# Time series of momentum for a few symbols\n", - "momentum_60.plot()\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 5. IC (Information Coefficient) Analysis\n", - "\n", - "The **IC** measures the rank correlation between factor values and subsequent returns. A consistently positive (or negative) IC indicates predictive power.\n", - "\n", - "### 5.1 Single-Period IC" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2026-02-12 20:40:58,547 - INFO - prepare_data: 411607 rows -> 412109 rows (100.1% retained)\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "IC Summary:\n", - "{1: {'mean_ic': -0.026684615700049246, 'ic_std': 0.3761825466053544, 'ic_ir': -0.07093528378934481, 't-stat': -14.36627769408405}}\n", - "\n", - "IC Series shape: (41017, 1)\n", - " period_1\n", - "start_time \n", - "1768393320000 0.369697\n", - "1768393380000 0.345455\n", - "1768393440000 -0.406061\n", - "1768393500000 0.260606\n", - "1768393560000 0.527273\n" - ] - } - ], - "source": [ - "# Use ResearchSession for streamlined analysis\n", - "signal = momentum_rank # Use ranked momentum as our signal\n", - "\n", - "analysis = session.analyze(signal, periods=1)\n", - "\n", - "print(\"IC Summary:\")\n", - "print(analysis.ic_summary)\n", - "print()\n", - "print(f\"IC Series shape: {analysis.ic_series.shape}\")\n", - "print(analysis.ic_series.head())" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 5.2 Multi-Horizon IC Analysis\n", - "\n", - "To understand how quickly a factor's signal decays, we compute IC across multiple forward horizons." - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2026-02-12 20:41:17,067 - INFO - prepare_data: 411607 rows -> 412109 rows (100.1% retained)\n", - "2026-02-12 20:41:18,547 - INFO - prepare_data: 411607 rows -> 412069 rows (100.1% retained)\n", - "2026-02-12 20:41:20,086 - INFO - prepare_data: 411607 rows -> 412019 rows (100.1% retained)\n", - "2026-02-12 20:41:21,566 - INFO - prepare_data: 411607 rows -> 411919 rows (100.1% retained)\n", - "2026-02-12 20:41:23,129 - INFO - prepare_data: 411607 rows -> 411519 rows (100.0% retained)\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "IC Decay Analysis:\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", - "
mean_icic_stdic_irt-stat
horizon
1-0.0266850.376183-0.070935-14.366278
5-0.0363140.383139-0.094780-19.194464
10-0.0407830.385369-0.105829-21.430799
20-0.0486770.386069-0.126085-25.529622
60-0.0415490.384516-0.108055-21.868208
\n", - "
" - ], - "text/plain": [ - " mean_ic ic_std ic_ir t-stat\n", - "horizon \n", - "1 -0.026685 0.376183 -0.070935 -14.366278\n", - "5 -0.036314 0.383139 -0.094780 -19.194464\n", - "10 -0.040783 0.385369 -0.105829 -21.430799\n", - "20 -0.048677 0.386069 -0.126085 -25.529622\n", - "60 -0.041549 0.384516 -0.108055 -21.868208" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Compute IC for multiple horizons\n", - "horizons = [1, 5, 10, 20, 60]\n", - "ic_results = {}\n", - "\n", - "for h in horizons:\n", - " result = session.analyze(signal, periods=h)\n", - " ic_results[h] = result.ic_summary.get(h, {})\n", - "\n", - "# Display IC decay table\n", - "ic_decay_df = pd.DataFrame(ic_results).T\n", - "ic_decay_df.index.name = \"horizon\"\n", - "print(\"IC Decay Analysis:\")\n", - "ic_decay_df" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAA90AAAHqCAYAAAAZLi26AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAc2BJREFUeJzt3XdclXX/x/H3YasMBTcOXJCIAxeKJrnTcmSOzJHW3S5HmdowG7buzIZaav3S0ixzj7g1R5Y7zQWKZiqKJrgARTac3x/GySOgHOV4DvB6Ph4W57q+nPO54MMF73Nd3+syGI1GowAAAAAAQKFzsHUBAAAAAAAUV4RuAAAAAACshNANAAAAAICVELoBAAAAALASQjcAAAAAAFZC6AYAAAAAwEoI3QAAAAAAWAmhGwAAAAAAKyF0AwAAAABgJU62LgAAYFtTp07VtGnTtH//frm6upqWp6amau7cuQoPD1d0dLQkqWrVqurYsaOGDRsmb2/vfJ9zyZIlevnll02PHRwc5OXlpfr16+v+++9X79695ejoaLVtspZjx47pyy+/1LZt23T+/Hl5enqqTp066tu3r3r16mXr8mwiICBAkvTtt98qJCQk1/qLFy+qXbt2ysjI0Pr161WtWrU7XWKRlvP1zc+vv/6qypUr36FqAAC3gtANAMglPj5ew4cPV3x8vJ577jm1aNFCWVlZ2r17t6ZNm6aVK1fq22+/VfXq1W/4PN99951q1qyp7OxsxcXF6bffftO7776rpUuXaubMmSpTpswd2qLb98svv2jkyJFq3bq13n33XdWsWVPnz5/XqlWrNH78eG3evFkffvihrcu0idKlS2vJkiV5hu6VK1fK2dlZGRkZNqjs9sTExKhTp046fPiwTesYPHiwnnrqqTzX+fj4FOprtW/fXu+//36e30sAwK0hdAMAcnnzzTd15swZLVu2TFWqVDEtr1Onjtq0aaNevXpp2rRp+uCDD274POXKlVOFChUkSZUqVVKjRo3UrVs3DRgwQG+++ab++9//WnU7Csu5c+c0ZswYtW/fXp988okMBoMkydfXV40bN1aNGjU0adIk9enTR61bt7ZxtXdeSEiI1qxZowkTJsjd3d1s3bJly9SiRQv9+uuvNqru1u3Zs8fWJUiSSpUqZfo5sqa4uDj9/fffVn8dAChpmNMNADBz+vRprV69WsOHDzcL3DmqVq2qpUuX6r333rul569Tp44effRRrVixQmfOnDEt/+233zR48GC1bNlSTZs21eOPP66jR4+afe6xY8f01FNPqWnTpgoJCdEzzzxjOvVduhqOx48fr9atWysoKEgdOnTQ+++/r9TUVEnSBx98oODgYF25csXseffu3auAgIB8g+GPP/6o5ORkjR8/3hS4rzVo0CBt2LDBFLjHjx+vNm3amI05deqUAgIC9P3330u6egp+zmt27NhRDz74oF566SW1a9dORqPR7HN/+uknBQQE6ODBg5Kk48eP6/nnn1e7du3UqFEj9enTRxs2bMj3a25tbdq0UXZ2tv73v/+ZLT98+LAOHjyoDh065Pqco0eP6qmnnlLz5s0VFBSk7t27a+7cuWZjAgIC9OWXX+qDDz5Qq1atFBwcrHHjxiktLU0ff/yx2rRpoxYtWujll19Wenq66fOSkpL09ttvq2vXrmrYsKE6deqkWbNmmX1dO3TooHfeeUffffedOnbsqCZNmqhv377av3+/pKvTLl566SVTHePHjzd9PHnyZLM6p06dqoCAAKWlpUmShgwZoieffFLLli1Tx44d1ahRIw0cOFB///23wsPD1bVrVwUHB2vo0KGFFnJzeqJly5YKCgpS586d9cUXXyg7O9ts3NKlS9WjRw81atRInTp10qeffqrMzEzt2LFD7dq1kyQNHTrU7Hu2ZMkS9ejRQw0bNlSzZs302GOPKTIy0mz99b0MAPgXoRsAYGbnzp0yGo2655578h1TrVo1OTjc+q+Qjh07ymg0aseOHZKk33//XU8++aQqVqyo+fPn65tvvlF6eroGDx6sixcvSpISEhI0dOhQGY1GzZ07V998840uX76sRx99VCkpKZKkF198Ubt27dLnn3+utWvXauLEiVq8eLE++eQTSVL//v2VkpKiNWvWmNXz008/qUqVKrr77rvzrPf3339XQEBAnm9CSFfnrPv6+t7S12LmzJl69913NWPGDPXo0UNxcXG5jrCGh4erXr16CgwMVHx8vAYPHqyYmBhNmTJFS5cuVfPmzfXss89q+/btt1TD7SpTpozat2+vJUuWmC1ftmyZAgMDVatWLbPlFy5c0KBBg5SQkKBZs2Zp1apV6tWrl9555x19++23ZmMXLFggDw8PLViwQKNHj9ayZcv0yCOPKDMzU999951GjBihJUuW6KeffjJ9znPPPadVq1Zp5MiR+umnn/T4449r2rRpmj59utlzb9q0Sfv27dOMGTP07bffKjExUWPHjpUkPfrooxo8eLAkafPmzXr11Vct+pocOXJEv/zyi2bOnKkZM2bo0KFDGjlypJYvX66pU6fq888/V0REhKZOnWrR8+bFaDTqiSee0JkzZzRnzhytWbNGI0eO1PTp0/Xdd9+Zxq1cuVKvvvqqHnzwQa1cuVLjx4/XnDlzNGXKFAUHB+ujjz6SdPVNhEWLFkmSFi1apJdfflmdOnXSsmXLNGfOHGVkZGjo0KGKjY01q+PaXgYA/IvQDQAwc/bsWUm65RBZEFWrVjV7rVmzZsnX11cffvih6tatq4YNG+qjjz5SUlKSfvzxR0lXj6bFx8frvffeU4MGDXTXXXfpjTfeUNOmTU1HC99//33NnTtXwcHBqlKlisLCwtS2bVtt2rRJklSrVi2FhISYhcPs7GytXr1affr0yfeNhLi4OKt9Pbp3766QkBBVqFBBoaGh8vHx0erVq03rk5KS9Ntvv6lnz56SpIULF+rChQv67LPP1Lx5c9WpU0evvPKKAgICNGvWLKvUWBA9e/bU7t27dfz4cUlSZmamVq5cqR49euQau2jRIiUmJuqzzz5T06ZN5efnpyeffFL33HNPrqPd5cuX1zPPPKOaNWtqyJAhKlOmjOLj4zVmzBj5+flp8ODBKlOmjOksgH379mnbtm0aO3asunfvrho1amjAgAEaMGCAvv7661xHxCdNmqR69eqpUaNG6tWrl44fP66kpCSVKVNGpUqVkiRVqFBBHh4eFn09Lly4oEmTJqlu3boKDQ1VSEiI9u/frzfffFP+/v5q3bq1QkJCTHXfrq+//lozZsxQYGCgfH19df/99yswMNDU+9LVn7N77rlHw4YNU82aNdWpUyeNHTtWWVlZcnFxkaenpyTJy8vLdKHEL7/8Uu3atdPIkSNVp04dNWzYUFOmTFFqamquN1mu7WUAwL8I3QCAPF1/inNhyrmolpPT1UuL7N+/X61atTK7onn58uVVr149UyjZv3+/qlWrZnbV9Dp16mjy5MmqU6eO6XmnTZumzp07q1mzZgoODtbPP/+shIQE0+c89NBD2rVrl2JiYiRdPbJ//vz5G54SazAYrPb1CAoKMn3s5OSkbt266eeffza93rp165SZmWkK3fv371eNGjVUo0YNs+dp1aqVDhw4YJUaC6Jdu3YqW7asKYht3rxZFy5c0H333ZdrbEREhGrUqKGKFSuaLQ8ODtbJkyeVlJRkWtagQQPTxwaDQV5eXgoICDCd5p+zLOdz9u3bJ0lq27at2XO3bt1aV65cMZuO0KBBA7m4uJge5/RWYmKixdt/vRo1apgFdS8vL5UrV87sSuNeXl66fPnyTZ9rzpw5Cg4OzvUv5xRwg8GgS5cu6Z133lGHDh3UtGlTBQcHKyIiwtT7qamp+vPPP9W4cWOz5x44cKDZnQaulZSUpOjoaDVv3txsefny5VW9evVcbxhc28sAgH9xITUAgJmco9DR0dFq1KiRVV7jxIkTkv49mp6UlKRly5aZnSIsSWlpaaZQdPny5Rte7fzKlSsaPHiwnJ2d9dJLL6levXpydnbW5MmTtXv3btO4Tp06ycfHR0uWLDGdfhwaGnrDI9lVq1Y11VzYrj+C2qNHD82bN0/79u1TkyZN9L///U8tW7Y0hbWkpCTFxMQoODjY7PMyMjKUkZGh9PR0syApSa+//rpWrlx5W3Xe7KJizs7O6t69u5YtW6ZRo0Zp6dKlatmypSpVqmQWdHO2Ia8jxzkXYbty5Yrp45yjzTkMBoNKly6da1nOmxQ54fvee+81G5Mzt/ncuXPy9/eXpDyfRyqcN5wKWndB9OnTR4899liu5TlnZpw5c0aDBw9WzZo19frrr6t69epycnLSmDFjTGMvXbokSRbdMSDna3n9xfFyll1/bQRLzwYAgJKC0A0AMNOiRQs5Ojpq7dq1+YbuLVu2yMPD45ZD+Zo1a+Ti4mK6LZGnp6fatm2r559/PtfYnADp7e19w+C7Y8cOnT17Vl999ZXZ3Ozk5GSzcc7OznrwwQe1atUqPfvss/r555/1xhtv3LDeVq1aafLkyTp69KjpqPr15s+fr+7du6ts2bJ5Hhm/vo78NGnSRDVq1NDq1atVq1YtbdmyRW+99ZZpvaenp6pXr64vv/wyz8/POXvgWiNHjswztBW2Xr16af78+dq4caM2bNigiRMn5jnO09PT7CJ6OXKO+uYV8grKy8tLkvTNN9+YPr5WYZz6fKvf21vl6empmjVr5rt+3bp1Sk5O1pQpU1S7dm3T8kuXLpm+BuXKlZODg4NFR/Fzvg/XnnmQIykpyapTUACgOOH0cgCAmUqVKqlHjx6aO3eu/vzzz1zrT58+rbFjx97yxZIiIiL03XffacCAASpbtqykq0Hz6NGjqlmzptm/zMxMU0jy9/fXqVOnzMLaqVOnNHDgQO3atct0yvq1p5+fOnVKO3bsyBWS+vfvr1OnTmnGjBkyGAzq2LHjDWt+8MEHVbZsWU2aNCnP+03/8MMPevPNN7Vr1y5JV4/4Xbp0SZmZmaYxOac9F8R9992ndevWaf369XJ0dFSXLl1M65o0aaIzZ87I3d3d7Gvl6OgoHx+fPOel+/j45PraWvqvIJo0aaKaNWtqypQpkqSuXbvmOa5Ro0aKiYlRXFyc2fI//vhDderUua37t+ecPn327Fmz+j09PVWqVKlcR5sL4tr+8fT0NF3cL8fevXtvud7CkFfv7969W9HR0abanZ2dVatWLe3cudPsc+fPn68nnnjCbFnO57i7u6tu3bq5Pufs2bOKiYlRw4YNC31bAKA4InQDAHJ55ZVXVLt2bQ0ePFhz5szR0aNHdfz4cS1ZskQPP/ywypcvb3b0NT/x8fE6d+6czp07p8OHD2vGjBkaOnSomjZtarodkyT95z//0eHDh/XGG2/o0KFDio6O1qxZs9SjRw/TbbwefPBBlStXTi+99JL+/PNPHTp0SBMnTlRcXJzq16+voKAgOTk56euvv1ZMTIy2bdumZ599Vt26dVNCQoIOHjxouohWtWrV1LZtW33xxRfq3bu3nJ2db7gd3t7eptPUhwwZoo0bN+r06dOKjIzUu+++qzfffFNPPPGEOnXqJOlqqMzIyNCMGTMUExOjdevW5bro1I306NFDMTExmjt3rjp16mR25LdPnz7y8vLSiBEj9Mcff+jUqVMKDw9Xv379CuVK2LerZ8+e+uuvv9S+fft8Tzfu06ePypYtq9GjR2v//v06fvy4PvvsM/3222+5AqClgoKC1LZtW7399ttat26dTp06pd9//13/+c9/9NRTT1l06njOhcXWrVunY8eOSbr6vd2wYYO2b9+u48eP66OPPsoVwu+0Jk2aSLp69fBTp05p3bp1euutt9S+fXvFxMTo+PHjys7O1hNPPKFt27ZpxowZOn36tDZs2KBPPvnEdHQ856j4li1bdPDgQRmNRj3++OPatGmTpk2bpujoaO3du1cjR45U2bJluTUYABQQp5cDAHLx8vLS999/r7lz52rFihX69NNP5eDgoOrVq2vIkCEaOHBggY5GDho0yPRx6dKl5e/vr7Fjx6pfv35mp0E3b95cX331laZOnaoBAwYoOztbAQEB+vjjj01Hob29vTV37ly9//77GjBggFxcXNS0aVPNnj1bZcqUUZkyZfTOO+/os88+0/333y9/f3+9/vrrKleunHbu3KlBgwZp4cKFqlu3rqSrV1retGmT+vbtW6Cvyd13363ly5dr1qxZevPNN3Xu3DmVLVtW9evX18yZM033OM557r1792r+/Pn66quvFBwcrLfffjvPi4rlpU6dOmrQoIEOHDigUaNGma0rW7as5s+fr8mTJ+upp55ScnKyqlSpokceeUSPP/54gZ7fmnr27KmpU6fmedXyHDnfy//+978aPny40tLSVLt2bX3wwQfq3bv3bdcwdepUffzxx3rrrbd0/vx5eXl5qVOnTho9enSB51HnbMvKlSs1atQotW/fXtOmTdNrr72mCRMm6Omnn1apUqX04IMPaujQoQV6E8pamjZtqhdffFFz587VDz/8YLr6f3x8vJ577jk99NBDWrdunXr37q3MzEx9/fXXmj59uipWrKjBgwfr6aefliQ1bNhQHTt21OzZs7V48WJt2rRJvXv3VnZ2tmbPnq0ZM2bIzc1NLVu21DvvvGN2ZB0AkD+D0ZqXpwUAwE7lHPWcOXOmrUsBAADFGEe6AQAlRnp6us6dO6cFCxZo8+bNFp3yDQAAcCsI3QCAEmP//v0aMmSI/Pz8NH36dNOtowAAAKyF08sBAAAAALASrl4OAAAAAICVELoBAAAAALASQjcAAAAAAFbChdTykJmZqcTERLm6usrBgfclAAAAAADmsrOzlZaWJi8vLzk55R+tCd15SExMVHR0tK3LAAAAAADYOT8/P/n4+OS7ntCdB1dXV0lXv3ilSpWy6msZjUYlJSXJ3d1dBoPBqq+Foo9+gaXoGViCfoEl6BdYgn6BpYpCz6SkpCg6OtqUH/ND6M5DzinlpUqVUunSpa36WkajURkZGSpdurTdNhPsB/0CS9EzsAT9AkvQL7AE/QJLFaWeudmUZCYsAwAAAABgJYRuAAAAAACshNANAAAAAICVELoBAAAAALASQjcAAAAAAFZC6AYAAAAAwEoI3QAAAAAAWAmhGwAAAAAAKyF0AwAAAABgJYRuAAAAAACshNANAAAAAICVELoBAAAA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eXl5qVKlSnrooYe0ceNGSVePjN9zzz1q27atHB0d1bx5c3Xr1k3Lly83fa6vr6/at2+f53YtWrRITz75pKpVq2Z6I+LcuXPavXu3IiMjdfbsWT399NNydXVV48aNza4DcOnSJTk7O8vNzU0uLi565plnzC54V6dOHTk4OOjPP//M9+sJACi+CN0AgBJl0qRJuYLp448/Lunq6dV16tQxG1+zZk2dPn3a9NjX19f0cUhIiCIjI5WZmamdO3eqadOmaty4sf744w9lZGRo3759Cg0NlXT1yO2AAQMUHByshg0bKjw8XOnp6WavVbVqVdPHcXFxqlatmumxt7e3vLy8brhtjz76aK756p9//rlp/d9//y2j0Wi2jTVq1JAks228to68bN26VfPmzdPHH38sV1dXSQX72lWtWlUODv/+6VGqVCmlpqbm+RoJCQkqW7ZsruW1a9c2e76zZ89Kkk6ePKk1a9aYfV9XrFihuLg40/hrv3fXSkxM1OXLl82eu0yZMvLx8dHp06cVGxsrT09PeXh4mNb7+fmZPn744Yd1/PhxhYWFafz48Vq/fr3Z8zs4OMjLy0sXL17M8/UBAMWbk60LAADgTnrttdc0cODAPNddH4JzXHtk1NHR0fSxr6+vypcvr4MHD2rXrl2m8P5///d/OnjwoLy9vVWjRg0dPXpUI0eO1Lhx49S/f3+5ubnppZdeynXk2snp31/L6enpudbnzI++Vflt34228Xrnz5/XSy+9pAkTJpiF1IJ87a4N3AWR1xHprKysPMe4ublp4MCBZqd1Xy+/7brZ1yXnFPNrXfu9qFatmsLDw7Vjxw5t2LBBr7/+ulasWKHPPvvshtsCACgZONINAMA/atSooWPHjpktO3bsmKpXr57v54SEhGjnzp2KiIhQkyZN1KhRI0VERGjnzp2mU8ujoqLk4uKioUOHys3NTUaj0ewiXXmpWLGi2Rzgs2fP6tKlS7exdTJtx7XbmPNxzhHvGzEajRo7dqzatWun3r17m627la/djZQtW1bx8fG5lp88edL08enTp1WpUiXT6187916SYmNjc4XlvPj4+KhMmTJm9ScmJurChQuqUaOGKlasqKSkJF2+fNm0/trT4q9cuaKsrCyFhobqtdde08KFC7VmzRpT/dnZ2UpMTFS5cuUKuPUAgOKE0A0AwD969eqllStXau/evcrIyNCSJUt05MiRXBcUu1arVq20cOFC+fn5qXTp0nJ3d1eVKlW0dOlSU+j29fVVamqqoqKilJiYqA8//FAuLi46e/asjEZjns979913a9WqVYqOjlZSUpLZqdy3ysfHR23bttWnn36qhIQEJSYm6pNPPlFISIiqVKly08+fNWuW4uLi9Prrr+da161bN8XExGjBggXKzMzU/v37tXTpUj3wwAO3VGu9evV05MiRXMvnzJmjy5cvKzY2VgsWLFD79u0lSX379tXu3bu1ePFipaenKyoqSv369TObj58fBwcH3X///Zo1a5ZiY2OVnJysyZMnq3r16goODlbjxo3l5eWlr776Sunp6dq1a5d++eUX0+ePGDFCH3zwgZKSkpSdna09e/aobNmypukAx44dU1ZW1m3dLx0AUHQRugEA+Md9992nJ598UmPHjlVISIjmz5+vr7/+2mz+7vVCQkJ0/PhxNWvWzLSsadOmOnr0qCl0BwcHa9CgQRo8eLDuu+8++fr66pVXXtGff/6p0aNH5/m8w4YNU/v27dW/f3/de++9Cg4OVuXKlW97Gz/44AOVLl1a3bp1U/fu3eXu7m66AvjNLFiwQNHR0WrZsqXZ3OmdO3fK19dX06ZN04IFC9SiRQu99NJLGjlyZK4j4gXVqlUrbd++Pdfyjh07qnfv3urcubNq166tESNGSLp6sbKPPvpIX331lZo3b67nn39ejz32mLp3716g1xs/frzq16+vfv36qX379jp37pxmz54tR0dHubm5afr06Vq/fr1atGihadOm6dFHHzV97ttvv60TJ06oXbt2atGihebNm6fp06ebTqffsWOH/Pz8CuX7BwAoegzG/N5iBwAAsJELFy6oQ4cOmjt3rho1aqQdO3Zo6NCh2r9//20f8b/TevfurV69emn48OG2LgUAYAMc6QYAAHbHx8dHQ4cONbv6elG0bt06JSQkqH///rYuBQBgI4RuAABgl0aMGKELFy5o0aJFti7llsTHx+utt97S5MmTVaZMGVuXAwCwEU4vBwAAAADASjjSDQAAAACAlRC6AQAAAACwEkI3AAAAAABWQugGAAAAAMBKCN0AAAAAAFgJoRsAAAAAACshdAMAAAAAYCWEbgAAAAAArITQDQAAAACAlfw/p5zFhxPRM8sAAAAASUVORK5CYII=", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# Plot IC decay curve\n", - "fig, ax = plt.subplots(figsize=(10, 5))\n", - "ax.plot(ic_decay_df.index, ic_decay_df[\"mean_ic\"], \"o-\", linewidth=2, markersize=8)\n", - "ax.axhline(y=0, color=\"gray\", linestyle=\"--\", alpha=0.5)\n", - "ax.set_xlabel(\"Forward Horizon (periods)\")\n", - "ax.set_ylabel(\"Mean IC\")\n", - "ax.set_title(\"IC Decay Curve \u2014 Momentum Factor\")\n", - "ax.grid(True, alpha=0.3)\n", - "plt.tight_layout()\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 6. Quantile Analysis\n", - "\n", - "Split the cross-section into quantiles by factor value and examine the mean returns of each group. A monotonic pattern (Q1 < Q2 < ... < Q5) indicates strong linear predictive power." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Use FactorAnalyzer directly for more control\n", - "analyzer = FactorAnalyzer(signal, agg, quantiles=5)\n", - "\n", - "# Prepare data: align factor values with forward returns\n", - "analyzer.prepare_data(price_col=\"close\", periods=[1])\n", - "\n", - "# Quantile returns\n", - "analyzer.plot_quantile_returns(quantiles=5, period=1)\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Cumulative returns by quantile (includes Long-Short portfolio)\n", - "analyzer.plot_cumulative_returns(quantiles=5, period=1, long_short=True)\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": {}, - "outputs": [ - { - "ename": "ValueError", - "evalue": "Data not prepared. Call prepare_data() first.", - "output_type": "error", - "traceback": [ - "\u001b[31m---------------------------------------------------------------------------\u001b[39m", - "\u001b[31mValueError\u001b[39m Traceback (most recent call last)", - "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[19]\u001b[39m\u001b[32m, line 2\u001b[39m\n\u001b[32m 1\u001b[39m \u001b[38;5;66;03m# IC time series plot\u001b[39;00m\n\u001b[32m----> \u001b[39m\u001b[32m2\u001b[39m \u001b[43manalyzer\u001b[49m\u001b[43m.\u001b[49m\u001b[43mplot_ic\u001b[49m\u001b[43m(\u001b[49m\u001b[43mperiod\u001b[49m\u001b[43m=\u001b[49m\u001b[32;43m1\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmethod\u001b[49m\u001b[43m=\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mrank\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mplot_type\u001b[49m\u001b[43m=\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mts\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[32m 3\u001b[39m plt.show()\n", - "\u001b[36mFile \u001b[39m\u001b[32m/mnt/raid1/novis/factorium/src/factorium/factors/analyzer.py:544\u001b[39m, in \u001b[36mFactorAnalyzer.plot_ic\u001b[39m\u001b[34m(self, period, method, plot_type)\u001b[39m\n\u001b[32m 534\u001b[39m \u001b[38;5;250m\u001b[39m\u001b[33;03m\"\"\"\u001b[39;00m\n\u001b[32m 535\u001b[39m \u001b[33;03mPlot Information Coefficient (IC).\u001b[39;00m\n\u001b[32m 536\u001b[39m \n\u001b[32m (...)\u001b[39m\u001b[32m 540\u001b[39m \u001b[33;03m plot_type: 'ts' for time series, 'hist' for histogram.\u001b[39;00m\n\u001b[32m 541\u001b[39m \u001b[33;03m\"\"\"\u001b[39;00m\n\u001b[32m 542\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01m.\u001b[39;00m\u001b[34;01mplotting_analyzer\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m FactorAnalyzerPlotter\n\u001b[32m--> \u001b[39m\u001b[32m544\u001b[39m ic = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mcalculate_ic\u001b[49m\u001b[43m(\u001b[49m\u001b[43mmethod\u001b[49m\u001b[43m=\u001b[49m\u001b[43mmethod\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 545\u001b[39m col = \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mperiod_\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mperiod\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m\n\u001b[32m 546\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m col \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;129;01min\u001b[39;00m ic.columns:\n", - "\u001b[36mFile \u001b[39m\u001b[32m/mnt/raid1/novis/factorium/src/factorium/factors/analyzer.py:385\u001b[39m, in \u001b[36mFactorAnalyzer.calculate_ic\u001b[39m\u001b[34m(self, method)\u001b[39m\n\u001b[32m 375\u001b[39m \u001b[38;5;250m\u001b[39m\u001b[33;03m\"\"\"\u001b[39;00m\n\u001b[32m 376\u001b[39m \u001b[33;03mCalculate Information Coefficient (IC) for each period.\u001b[39;00m\n\u001b[32m 377\u001b[39m \n\u001b[32m (...)\u001b[39m\u001b[32m 382\u001b[39m \u001b[33;03m pd.DataFrame: IC values indexed by start_time.\u001b[39;00m\n\u001b[32m 383\u001b[39m \u001b[33;03m\"\"\"\u001b[39;00m\n\u001b[32m 384\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28mhasattr\u001b[39m(\u001b[38;5;28mself\u001b[39m, \u001b[33m\"\u001b[39m\u001b[33m_clean_data\u001b[39m\u001b[33m\"\u001b[39m):\n\u001b[32m--> \u001b[39m\u001b[32m385\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[33m\"\u001b[39m\u001b[33mData not prepared. Call prepare_data() first.\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m 387\u001b[39m period_cols = [c \u001b[38;5;28;01mfor\u001b[39;00m c \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m._clean_data.columns \u001b[38;5;28;01mif\u001b[39;00m c.startswith(\u001b[33m\"\u001b[39m\u001b[33mperiod_\u001b[39m\u001b[33m\"\u001b[39m)]\n\u001b[32m 388\u001b[39m corr_method = \u001b[33m\"\u001b[39m\u001b[33mspearman\u001b[39m\u001b[33m\"\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m method == \u001b[33m\"\u001b[39m\u001b[33mrank\u001b[39m\u001b[33m\"\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m \u001b[33m\"\u001b[39m\u001b[33mpearson\u001b[39m\u001b[33m\"\u001b[39m\n", - "\u001b[31mValueError\u001b[39m: Data not prepared. Call prepare_data() first." - ] - } - ], - "source": [ - "# IC time series plot\n", - "analyzer.plot_ic(period=1, method=\"rank\", plot_type=\"ts\")\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 7. Backtesting\n", - "\n", - "Run a **market-neutral vectorized backtest** using the momentum signal. The backtester:\n", - "- Converts signals to portfolio weights (cross-sectional normalization)\n", - "- Handles transaction costs\n", - "- Tracks equity, returns, and positions over time" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Market-neutral backtest\n", - "result = session.backtest(\n", - " signal,\n", - " neutralization=\"market\",\n", - " transaction_cost=0.0003,\n", - ")\n", - "\n", - "# Key performance metrics\n", - "print(\"Backtest Metrics:\")\n", - "for key, val in result.metrics.items():\n", - " if isinstance(val, float):\n", - " print(f\" {key:25s}: {val:>10.4f}\")\n", - " else:\n", - " print(f\" {key:25s}: {val}\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Plot equity curve\n", - "equity = result.equity_curve.to_pandas()\n", - "equity[\"timestamp\"] = pd.to_datetime(equity[\"start_time\"], unit=\"ms\")\n", - "\n", - "fig, ax = plt.subplots(figsize=(14, 6))\n", - "ax.plot(equity[\"timestamp\"], equity[\"equity\"], linewidth=1.5)\n", - "ax.set_xlabel(\"Date\")\n", - "ax.set_ylabel(\"Portfolio Equity\")\n", - "ax.set_title(\"Momentum Factor \u2014 Equity Curve (Market Neutral)\")\n", - "ax.grid(True, alpha=0.3)\n", - "plt.tight_layout()\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Plot returns distribution\n", - "returns = result.returns.to_pandas()\n", - "\n", - "fig, ax = plt.subplots(figsize=(10, 5))\n", - "ax.hist(returns[\"portfolio_return\"].dropna(), bins=100, alpha=0.75, edgecolor=\"black\", linewidth=0.5)\n", - "ax.axvline(x=0, color=\"red\", linestyle=\"--\", alpha=0.7)\n", - "ax.set_xlabel(\"Return\")\n", - "ax.set_ylabel(\"Frequency\")\n", - "ax.set_title(\"Distribution of Portfolio Returns\")\n", - "ax.grid(True, alpha=0.3)\n", - "plt.tight_layout()\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 8. Quick Report\n", - "\n", - "`ResearchSession.quick_report()` combines IC analysis and backtesting into a single text summary \u2014 great for rapid iteration." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "report = session.quick_report(signal)\n", - "print(report)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 9. Summary & Next Steps\n", - "\n", - "In this notebook we:\n", - "- Loaded multi-symbol 1-minute data from Binance using `BinanceDataLoader`\n", - "- Built momentum factors via both **code-based** and **expression-based** methods\n", - "- Visualized factor behavior with time series, distributions, and heatmaps\n", - "- Computed IC and analyzed its decay across multiple horizons\n", - "- Ran a market-neutral backtest and examined performance metrics\n", - "\n", - "**Next notebooks to explore:**\n", - "- `02_mean_reversion_factor.ipynb` \u2014 Mean reversion with volatility normalization\n", - "- `03_data_loading_and_exploration.ipynb` \u2014 Deep dive into AggBar and data loading\n", - "- `04_multi_factor_combination.ipynb` \u2014 Combine multiple factors and compare performance" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.13.2" - } - }, - "nbformat": 4, - "nbformat_minor": 4 + "nbformat": 4, + "nbformat_minor": 4 } diff --git a/examples/02_mean_reversion_factor.ipynb b/examples/02_mean_reversion_factor.ipynb index 4a9a67b..e0b04d0 100644 --- a/examples/02_mean_reversion_factor.ipynb +++ b/examples/02_mean_reversion_factor.ipynb @@ -69,7 +69,7 @@ "\n", "print(f\"Loaded {len(agg):,} bars for {len(agg.symbols)} symbols\")\n", "\n", - "session = ResearchSession(agg, default_frequency=\"1min\")" + "session = ResearchSession(agg, default_frequency=\"1m\")" ] }, { @@ -277,12 +277,12 @@ "fig, ax = plt.subplots(figsize=(14, 6))\n", "\n", "eq_neutral = result_neutral.equity_curve.to_pandas()\n", - "eq_neutral[\"ts\"] = pd.to_datetime(eq_neutral[\"start_time\"], unit=\"ms\")\n", - "ax.plot(eq_neutral[\"ts\"], eq_neutral[\"equity\"], label=\"Market Neutral\", linewidth=1.5)\n", + "eq_neutral[\"ts\"] = pd.to_datetime(eq_neutral[\"end_time\"], unit=\"ms\")\n", + "ax.plot(eq_neutral[\"ts\"], eq_neutral[\"total_value\"], label=\"Market Neutral\", linewidth=1.5)\n", "\n", "eq_long = result_long.equity_curve.to_pandas()\n", - "eq_long[\"ts\"] = pd.to_datetime(eq_long[\"start_time\"], unit=\"ms\")\n", - "ax.plot(eq_long[\"ts\"], eq_long[\"equity\"], label=\"Long Only\", linewidth=1.5)\n", + "eq_long[\"ts\"] = pd.to_datetime(eq_long[\"end_time\"], unit=\"ms\")\n", + "ax.plot(eq_long[\"ts\"], eq_long[\"total_value\"], label=\"Long Only\", linewidth=1.5)\n", "\n", "ax.set_xlabel(\"Date\")\n", "ax.set_ylabel(\"Equity\")\n", @@ -337,7 +337,7 @@ "outputs": [], "source": [ "# Visualize autocorrelation for a few symbols\n", - "autocorr.plot.plot_timeseries(symbols=[\"BTCUSDT\", \"ETHUSDT\", \"SOLUSDT\"])\n", + "autocorr.plot(symbols=[\"BTCUSDT\", \"ETHUSDT\", \"SOLUSDT\"])\n", "plt.axhline(y=0, color=\"red\", linestyle=\"--\", alpha=0.5)\n", "plt.title(\"Rolling Return Autocorrelation (60-period window, lag=1)\")\n", "plt.ylabel(\"Autocorrelation\")\n", @@ -391,4 +391,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} \ No newline at end of file +} diff --git a/examples/03_data_loading_and_exploration.ipynb b/examples/03_data_loading_and_exploration.ipynb index 23d48e0..bd78af6 100644 --- a/examples/03_data_loading_and_exploration.ipynb +++ b/examples/03_data_loading_and_exploration.ipynb @@ -128,7 +128,7 @@ "# Metadata\n", "meta = agg.metadata\n", "print(f\"Number of rows: {meta.num_rows:,}\")\n", - "print(f\"Number of symbols: {meta.num_symbols}\")\n", + "print(f\"Number of symbols: {len(meta.symbols)}\")\n", "print(f\"Time range: {meta.min_time} → {meta.max_time}\")" ] }, @@ -347,7 +347,7 @@ "from factorium import ResearchSession\n", "\n", "# Create from AggBar\n", - "session = ResearchSession(agg, default_frequency=\"1min\")\n", + "session = ResearchSession(agg, default_frequency=\"1m\")\n", "\n", "print(f\"Session symbols: {session.symbols}\")\n", "print(f\"Session columns: {session.cols}\")\n", @@ -367,7 +367,7 @@ "# ResearchSession can also load from files directly\n", "session_from_csv = ResearchSession.from_csv(\n", " output_dir / \"crypto_1min.csv\",\n", - " default_frequency=\"1min\",\n", + " default_frequency=\"1m\",\n", ")\n", "print(f\"Session from CSV: {len(session_from_csv.symbols)} symbols\")" ] @@ -427,4 +427,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} \ No newline at end of file +} diff --git a/examples/04_multi_factor_combination.ipynb b/examples/04_multi_factor_combination.ipynb index 90e9324..3b28997 100644 --- a/examples/04_multi_factor_combination.ipynb +++ b/examples/04_multi_factor_combination.ipynb @@ -68,7 +68,7 @@ " interval=60_000,\n", ")\n", "\n", - "session = ResearchSession(agg, default_frequency=\"1min\")\n", + "session = ResearchSession(agg, default_frequency=\"1m\")\n", "print(f\"Loaded {len(agg):,} bars for {len(agg.symbols)} symbols\")" ] }, @@ -227,7 +227,7 @@ "rolling_corr = momentum.ts_corr(inv_vol_ranked, window=120)\n", "rolling_corr.name = \"mom_vol_corr\"\n", "\n", - "rolling_corr.plot.plot_timeseries(symbols=[\"BTCUSDT\", \"ETHUSDT\"])\n", + "rolling_corr.plot(symbols=[\"BTCUSDT\", \"ETHUSDT\"])\n", "plt.axhline(y=0, color=\"red\", linestyle=\"--\", alpha=0.5)\n", "plt.title(\"Rolling Correlation: Momentum vs. Inverse Volatility (120-period window)\")\n", "plt.ylabel(\"Correlation\")\n", @@ -428,10 +428,10 @@ "\n", "for name, result in bt_results.items():\n", " eq = result.equity_curve.to_pandas()\n", - " eq[\"ts\"] = pd.to_datetime(eq[\"start_time\"], unit=\"ms\")\n", + " eq[\"ts\"] = pd.to_datetime(eq[\"end_time\"], unit=\"ms\")\n", " style = \"--\" if name in factors else \"-\"\n", " linewidth = 1.0 if name in factors else 2.0\n", - " ax.plot(eq[\"ts\"], eq[\"equity\"], style, label=name, linewidth=linewidth)\n", + " ax.plot(eq[\"ts\"], eq[\"total_value\"], style, label=name, linewidth=linewidth)\n", "\n", "ax.set_xlabel(\"Date\")\n", "ax.set_ylabel(\"Equity\")\n", @@ -546,4 +546,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} \ No newline at end of file +} diff --git a/pyproject.toml b/pyproject.toml index 6c1d055..9e959bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,6 +80,7 @@ python_files = ["test_*.py"] [dependency-groups] dev = [ "freezegun>=1.5.5", + "jupyterlab>=4.5.4", "mkdocs>=1.6.1", "mkdocs-material>=9.7.1", "mypy>=1.0.0", diff --git a/uv.lock b/uv.lock index 0f05705..b075e29 100644 --- a/uv.lock +++ b/uv.lock @@ -2,7 +2,8 @@ version = 1 revision = 3 requires-python = ">=3.11" resolution-markers = [ - "python_full_version >= '3.12'", + "python_full_version >= '3.14'", + "python_full_version >= '3.12' and python_full_version < '3.14'", "python_full_version < '3.12'", ] @@ -139,6 +140,102 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, ] +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "appnope" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170, upload-time = "2024-02-06T09:43:11.258Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" }, +] + +[[package]] +name = "argon2-cffi" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argon2-cffi-bindings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657, upload-time = "2025-06-03T06:55:30.804Z" }, +] + +[[package]] +name = "argon2-cffi-bindings" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/db8af0df73c1cf454f71b2bbe5e356b8c1f8041c979f505b3d3186e520a9/argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", size = 1783441, upload-time = "2025-07-30T10:02:05.147Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/97/3c0a35f46e52108d4707c44b95cfe2afcafc50800b5450c197454569b776/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:3d3f05610594151994ca9ccb3c771115bdb4daef161976a266f0dd8aa9996b8f", size = 54393, upload-time = "2025-07-30T10:01:40.97Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/98bbd6ee89febd4f212696f13c03ca302b8552e7dbf9c8efa11ea4a388c3/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8b8efee945193e667a396cbc7b4fb7d357297d6234d30a489905d96caabde56b", size = 29328, upload-time = "2025-07-30T10:01:41.916Z" }, + { url = "https://files.pythonhosted.org/packages/43/24/90a01c0ef12ac91a6be05969f29944643bc1e5e461155ae6559befa8f00b/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3c6702abc36bf3ccba3f802b799505def420a1b7039862014a65db3205967f5a", size = 31269, upload-time = "2025-07-30T10:01:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/d4/d3/942aa10782b2697eee7af5e12eeff5ebb325ccfb86dd8abda54174e377e4/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1c70058c6ab1e352304ac7e3b52554daadacd8d453c1752e547c76e9c99ac44", size = 86558, upload-time = "2025-07-30T10:01:43.943Z" }, + { url = "https://files.pythonhosted.org/packages/0d/82/b484f702fec5536e71836fc2dbc8c5267b3f6e78d2d539b4eaa6f0db8bf8/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2fd3bfbff3c5d74fef31a722f729bf93500910db650c925c2d6ef879a7e51cb", size = 92364, upload-time = "2025-07-30T10:01:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c1/a606ff83b3f1735f3759ad0f2cd9e038a0ad11a3de3b6c673aa41c24bb7b/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4f9665de60b1b0e99bcd6be4f17d90339698ce954cfd8d9cf4f91c995165a92", size = 85637, upload-time = "2025-07-30T10:01:46.225Z" }, + { url = "https://files.pythonhosted.org/packages/44/b4/678503f12aceb0262f84fa201f6027ed77d71c5019ae03b399b97caa2f19/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ba92837e4a9aa6a508c8d2d7883ed5a8f6c308c89a4790e1e447a220deb79a85", size = 91934, upload-time = "2025-07-30T10:01:47.203Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c7/f36bd08ef9bd9f0a9cff9428406651f5937ce27b6c5b07b92d41f91ae541/argon2_cffi_bindings-25.1.0-cp314-cp314t-win32.whl", hash = "sha256:84a461d4d84ae1295871329b346a97f68eade8c53b6ed9a7ca2d7467f3c8ff6f", size = 28158, upload-time = "2025-07-30T10:01:48.341Z" }, + { url = "https://files.pythonhosted.org/packages/b3/80/0106a7448abb24a2c467bf7d527fe5413b7fdfa4ad6d6a96a43a62ef3988/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b55aec3565b65f56455eebc9b9f34130440404f27fe21c3b375bf1ea4d8fbae6", size = 32597, upload-time = "2025-07-30T10:01:49.112Z" }, + { url = "https://files.pythonhosted.org/packages/05/b8/d663c9caea07e9180b2cb662772865230715cbd573ba3b5e81793d580316/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:87c33a52407e4c41f3b70a9c2d3f6056d88b10dad7695be708c5021673f55623", size = 28231, upload-time = "2025-07-30T10:01:49.92Z" }, + { url = "https://files.pythonhosted.org/packages/1d/57/96b8b9f93166147826da5f90376e784a10582dd39a393c99bb62cfcf52f0/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500", size = 54121, upload-time = "2025-07-30T10:01:50.815Z" }, + { url = "https://files.pythonhosted.org/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44", size = 29177, upload-time = "2025-07-30T10:01:51.681Z" }, + { url = "https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0", size = 31090, upload-time = "2025-07-30T10:01:53.184Z" }, + { url = "https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6", size = 81246, upload-time = "2025-07-30T10:01:54.145Z" }, + { url = "https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a", size = 87126, upload-time = "2025-07-30T10:01:55.074Z" }, + { url = "https://files.pythonhosted.org/packages/72/70/7a2993a12b0ffa2a9271259b79cc616e2389ed1a4d93842fac5a1f923ffd/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d", size = 80343, upload-time = "2025-07-30T10:01:56.007Z" }, + { url = "https://files.pythonhosted.org/packages/78/9a/4e5157d893ffc712b74dbd868c7f62365618266982b64accab26bab01edc/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99", size = 86777, upload-time = "2025-07-30T10:01:56.943Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/15777dfde1c29d96de7f18edf4cc94c385646852e7c7b0320aa91ccca583/argon2_cffi_bindings-25.1.0-cp39-abi3-win32.whl", hash = "sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2", size = 27180, upload-time = "2025-07-30T10:01:57.759Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl", hash = "sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98", size = 31715, upload-time = "2025-07-30T10:01:58.56Z" }, + { url = "https://files.pythonhosted.org/packages/42/b9/f8d6fa329ab25128b7e98fd83a3cb34d9db5b059a9847eddb840a0af45dd/argon2_cffi_bindings-25.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94", size = 27149, upload-time = "2025-07-30T10:01:59.329Z" }, +] + +[[package]] +name = "arrow" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/33/032cdc44182491aa708d06a68b62434140d8c50820a087fac7af37703357/arrow-1.4.0.tar.gz", hash = "sha256:ed0cc050e98001b8779e84d461b0098c4ac597e88704a655582b21d116e526d7", size = 152931, upload-time = "2025-10-18T17:46:46.761Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-py3-none-any.whl", hash = "sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205", size = 68797, upload-time = "2025-10-18T17:46:45.663Z" }, +] + +[[package]] +name = "asttokens" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, +] + +[[package]] +name = "async-lru" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/c3/bbf34f15ea88dfb649ab2c40f9d75081784a50573a9ea431563cab64adb8/async_lru-2.1.0.tar.gz", hash = "sha256:9eeb2fecd3fe42cc8a787fc32ead53a3a7158cc43d039c3c55ab3e4e5b2a80ed", size = 12041, upload-time = "2026-01-17T22:52:18.931Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/e9/eb6a5db5ac505d5d45715388e92bced7a5bb556facc4d0865d192823f2d2/async_lru-2.1.0-py3-none-any.whl", hash = "sha256:fa12dcf99a42ac1280bc16c634bbaf06883809790f6304d85cdab3f666f33a7e", size = 6933, upload-time = "2026-01-17T22:52:17.389Z" }, +] + [[package]] name = "attrs" version = "25.4.0" @@ -171,6 +268,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/02/e3/a4fa1946722c4c7b063cc25043a12d9ce9b4323777f89643be74cef2993c/backrefs-6.1-py39-none-any.whl", hash = "sha256:a9e99b8a4867852cad177a6430e31b0f6e495d65f8c6c134b68c14c3c95bf4b0", size = 381058, upload-time = "2025-11-15T14:52:06.698Z" }, ] +[[package]] +name = "beautifulsoup4" +version = "4.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, +] + +[[package]] +name = "bleach" +version = "6.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/18/3c8523962314be6bf4c8989c79ad9531c825210dd13a8669f6b84336e8bd/bleach-6.3.0.tar.gz", hash = "sha256:6f3b91b1c0a02bb9a78b5a454c92506aa0fdf197e1d5e114d2e00c6f64306d22", size = 203533, upload-time = "2025-10-27T17:57:39.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-py3-none-any.whl", hash = "sha256:fe10ec77c93ddf3d13a73b035abaac7a9f5e436513864ccdad516693213c65d6", size = 164437, upload-time = "2025-10-27T17:57:37.538Z" }, +] + +[package.optional-dependencies] +css = [ + { name = "tinycss2" }, +] + [[package]] name = "boto3" version = "1.42.39" @@ -208,6 +335,76 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, ] +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + [[package]] name = "charset-normalizer" version = "3.4.4" @@ -302,6 +499,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "comm" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/13/7d740c5849255756bc17888787313b61fd38a0a8304fc4f073dfc46122aa/comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971", size = 6319, upload-time = "2025-07-25T14:02:04.452Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" }, +] + [[package]] name = "contourpy" version = "1.3.3" @@ -485,6 +691,49 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, ] +[[package]] +name = "debugpy" +version = "1.8.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/b7/cd8080344452e4874aae67c40d8940e2b4d47b01601a8fd9f44786c757c7/debugpy-1.8.20.tar.gz", hash = "sha256:55bc8701714969f1ab89a6d5f2f3d40c36f91b2cbe2f65d98bf8196f6a6a2c33", size = 1645207, upload-time = "2026-01-29T23:03:28.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/56/c3baf5cbe4dd77427fd9aef99fcdade259ad128feeb8a786c246adb838e5/debugpy-1.8.20-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:eada6042ad88fa1571b74bd5402ee8b86eded7a8f7b827849761700aff171f1b", size = 2208318, upload-time = "2026-01-29T23:03:36.481Z" }, + { url = "https://files.pythonhosted.org/packages/9a/7d/4fa79a57a8e69fe0d9763e98d1110320f9ecd7f1f362572e3aafd7417c9d/debugpy-1.8.20-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:7de0b7dfeedc504421032afba845ae2a7bcc32ddfb07dae2c3ca5442f821c344", size = 3171493, upload-time = "2026-01-29T23:03:37.775Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f2/1e8f8affe51e12a26f3a8a8a4277d6e60aa89d0a66512f63b1e799d424a4/debugpy-1.8.20-cp311-cp311-win32.whl", hash = "sha256:773e839380cf459caf73cc533ea45ec2737a5cc184cf1b3b796cd4fd98504fec", size = 5209240, upload-time = "2026-01-29T23:03:39.109Z" }, + { url = "https://files.pythonhosted.org/packages/d5/92/1cb532e88560cbee973396254b21bece8c5d7c2ece958a67afa08c9f10dc/debugpy-1.8.20-cp311-cp311-win_amd64.whl", hash = "sha256:1f7650546e0eded1902d0f6af28f787fa1f1dbdbc97ddabaf1cd963a405930cb", size = 5233481, upload-time = "2026-01-29T23:03:40.659Z" }, + { url = "https://files.pythonhosted.org/packages/14/57/7f34f4736bfb6e00f2e4c96351b07805d83c9a7b33d28580ae01374430f7/debugpy-1.8.20-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:4ae3135e2089905a916909ef31922b2d733d756f66d87345b3e5e52b7a55f13d", size = 2550686, upload-time = "2026-01-29T23:03:42.023Z" }, + { url = "https://files.pythonhosted.org/packages/ab/78/b193a3975ca34458f6f0e24aaf5c3e3da72f5401f6054c0dfd004b41726f/debugpy-1.8.20-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:88f47850a4284b88bd2bfee1f26132147d5d504e4e86c22485dfa44b97e19b4b", size = 4310588, upload-time = "2026-01-29T23:03:43.314Z" }, + { url = "https://files.pythonhosted.org/packages/c1/55/f14deb95eaf4f30f07ef4b90a8590fc05d9e04df85ee379712f6fb6736d7/debugpy-1.8.20-cp312-cp312-win32.whl", hash = "sha256:4057ac68f892064e5f98209ab582abfee3b543fb55d2e87610ddc133a954d390", size = 5331372, upload-time = "2026-01-29T23:03:45.526Z" }, + { url = "https://files.pythonhosted.org/packages/a1/39/2bef246368bd42f9bd7cba99844542b74b84dacbdbea0833e610f384fee8/debugpy-1.8.20-cp312-cp312-win_amd64.whl", hash = "sha256:a1a8f851e7cf171330679ef6997e9c579ef6dd33c9098458bd9986a0f4ca52e3", size = 5372835, upload-time = "2026-01-29T23:03:47.245Z" }, + { url = "https://files.pythonhosted.org/packages/15/e2/fc500524cc6f104a9d049abc85a0a8b3f0d14c0a39b9c140511c61e5b40b/debugpy-1.8.20-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:5dff4bb27027821fdfcc9e8f87309a28988231165147c31730128b1c983e282a", size = 2539560, upload-time = "2026-01-29T23:03:48.738Z" }, + { url = "https://files.pythonhosted.org/packages/90/83/fb33dcea789ed6018f8da20c5a9bc9d82adc65c0c990faed43f7c955da46/debugpy-1.8.20-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:84562982dd7cf5ebebfdea667ca20a064e096099997b175fe204e86817f64eaf", size = 4293272, upload-time = "2026-01-29T23:03:50.169Z" }, + { url = "https://files.pythonhosted.org/packages/a6/25/b1e4a01bfb824d79a6af24b99ef291e24189080c93576dfd9b1a2815cd0f/debugpy-1.8.20-cp313-cp313-win32.whl", hash = "sha256:da11dea6447b2cadbf8ce2bec59ecea87cc18d2c574980f643f2d2dfe4862393", size = 5331208, upload-time = "2026-01-29T23:03:51.547Z" }, + { url = "https://files.pythonhosted.org/packages/13/f7/a0b368ce54ffff9e9028c098bd2d28cfc5b54f9f6c186929083d4c60ba58/debugpy-1.8.20-cp313-cp313-win_amd64.whl", hash = "sha256:eb506e45943cab2efb7c6eafdd65b842f3ae779f020c82221f55aca9de135ed7", size = 5372930, upload-time = "2026-01-29T23:03:53.585Z" }, + { url = "https://files.pythonhosted.org/packages/33/2e/f6cb9a8a13f5058f0a20fe09711a7b726232cd5a78c6a7c05b2ec726cff9/debugpy-1.8.20-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:9c74df62fc064cd5e5eaca1353a3ef5a5d50da5eb8058fcef63106f7bebe6173", size = 2538066, upload-time = "2026-01-29T23:03:54.999Z" }, + { url = "https://files.pythonhosted.org/packages/c5/56/6ddca50b53624e1ca3ce1d1e49ff22db46c47ea5fb4c0cc5c9b90a616364/debugpy-1.8.20-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:077a7447589ee9bc1ff0cdf443566d0ecf540ac8aa7333b775ebcb8ce9f4ecad", size = 4269425, upload-time = "2026-01-29T23:03:56.518Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d9/d64199c14a0d4c476df46c82470a3ce45c8d183a6796cfb5e66533b3663c/debugpy-1.8.20-cp314-cp314-win32.whl", hash = "sha256:352036a99dd35053b37b7803f748efc456076f929c6a895556932eaf2d23b07f", size = 5331407, upload-time = "2026-01-29T23:03:58.481Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d9/1f07395b54413432624d61524dfd98c1a7c7827d2abfdb8829ac92638205/debugpy-1.8.20-cp314-cp314-win_amd64.whl", hash = "sha256:a98eec61135465b062846112e5ecf2eebb855305acc1dfbae43b72903b8ab5be", size = 5372521, upload-time = "2026-01-29T23:03:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl", hash = "sha256:5be9bed9ae3be00665a06acaa48f8329d2b9632f15fd09f6a9a8c8d9907e54d7", size = 5337658, upload-time = "2026-01-29T23:04:17.404Z" }, +] + +[[package]] +name = "decorator" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + [[package]] name = "duckdb" version = "1.4.3" @@ -521,6 +770,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b1/5a/8af5b96ce5622b6168854f479ce846cf7fb589813dcc7d8724233c37ded3/duckdb-1.4.3-cp314-cp314-win_arm64.whl", hash = "sha256:90f241f25cffe7241bf9f376754a5845c74775e00e1c5731119dc88cd71e0cb2", size = 13527759, upload-time = "2025-12-09T10:59:05.496Z" }, ] +[[package]] +name = "executing" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, +] + [[package]] name = "factorium" version = "0.3.2" @@ -550,6 +808,7 @@ s3 = [ [package.dev-dependencies] dev = [ { name = "freezegun" }, + { name = "jupyterlab" }, { name = "mkdocs" }, { name = "mkdocs-material" }, { name = "mypy" }, @@ -579,6 +838,7 @@ provides-extras = ["jupyter", "s3"] [package.metadata.requires-dev] dev = [ { name = "freezegun", specifier = ">=1.5.5" }, + { name = "jupyterlab", specifier = ">=4.5.4" }, { name = "mkdocs", specifier = ">=1.6.1" }, { name = "mkdocs-material", specifier = ">=9.7.1" }, { name = "mypy", specifier = ">=1.0.0" }, @@ -587,6 +847,15 @@ dev = [ { name = "ruff", specifier = ">=0.1.0" }, ] +[[package]] +name = "fastjsonschema" +version = "2.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/b5/23b216d9d985a956623b6bd12d4086b60f0059b27799f23016af04a74ea1/fastjsonschema-2.21.2.tar.gz", hash = "sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de", size = 374130, upload-time = "2025-08-14T18:49:36.666Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463", size = 24024, upload-time = "2025-08-14T18:49:34.776Z" }, +] + [[package]] name = "fonttools" version = "4.61.1" @@ -636,6 +905,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/4e/ce75a57ff3aebf6fc1f4e9d508b8e5810618a33d900ad6c19eb30b290b97/fonttools-4.61.1-py3-none-any.whl", hash = "sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371", size = 1148996, upload-time = "2025-12-12T17:31:21.03Z" }, ] +[[package]] +name = "fqdn" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/3e/a80a8c077fd798951169626cde3e239adeba7dab75deb3555716415bd9b0/fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f", size = 6015, upload-time = "2021-03-11T07:16:29.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014", size = 9121, upload-time = "2021-03-11T07:16:28.351Z" }, +] + [[package]] name = "freezegun" version = "1.5.5" @@ -765,6 +1043,43 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, ] +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + [[package]] name = "idna" version = "3.11" @@ -783,6 +1098,88 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] +[[package]] +name = "ipykernel" +version = "7.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "appnope", marker = "sys_platform == 'darwin'" }, + { name = "comm" }, + { name = "debugpy" }, + { name = "ipython" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "matplotlib-inline" }, + { name = "nest-asyncio" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/8d/b68b728e2d06b9e0051019640a40a9eb7a88fcd82c2e1b5ce70bef5ff044/ipykernel-7.2.0.tar.gz", hash = "sha256:18ed160b6dee2cbb16e5f3575858bc19d8f1fe6046a9a680c708494ce31d909e", size = 176046, upload-time = "2026-02-06T16:43:27.403Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/b9/e73d5d9f405cba7706c539aa8b311b49d4c2f3d698d9c12f815231169c71/ipykernel-7.2.0-py3-none-any.whl", hash = "sha256:3bbd4420d2b3cc105cbdf3756bfc04500b1e52f090a90716851f3916c62e1661", size = 118788, upload-time = "2026-02-06T16:43:25.149Z" }, +] + +[[package]] +name = "ipython" +version = "9.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "decorator" }, + { name = "ipython-pygments-lexers" }, + { name = "jedi" }, + { name = "matplotlib-inline" }, + { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit" }, + { name = "pygments" }, + { name = "stack-data" }, + { name = "traitlets" }, + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/60/2111715ea11f39b1535bed6024b7dec7918b71e5e5d30855a5b503056b50/ipython-9.10.0.tar.gz", hash = "sha256:cd9e656be97618a0676d058134cd44e6dc7012c0e5cb36a9ce96a8c904adaf77", size = 4426526, upload-time = "2026-02-02T10:00:33.594Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/aa/898dec789a05731cd5a9f50605b7b44a72bd198fd0d4528e11fc610177cc/ipython-9.10.0-py3-none-any.whl", hash = "sha256:c6ab68cc23bba8c7e18e9b932797014cc61ea7fd6f19de180ab9ba73e65ee58d", size = 622774, upload-time = "2026-02-02T10:00:31.503Z" }, +] + +[[package]] +name = "ipython-pygments-lexers" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, +] + +[[package]] +name = "isoduration" +version = "20.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "arrow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/1a/3c8edc664e06e6bd06cce40c6b22da5f1429aa4224d0c590f3be21c91ead/isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9", size = 11649, upload-time = "2020-11-01T11:00:00.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042", size = 11321, upload-time = "2020-11-01T10:59:58.02Z" }, +] + +[[package]] +name = "jedi" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parso" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, +] + [[package]] name = "jinja2" version = "3.1.6" @@ -804,6 +1201,218 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, ] +[[package]] +name = "json5" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/77/e8/a3f261a66e4663f22700bc8a17c08cb83e91fbf086726e7a228398968981/json5-0.13.0.tar.gz", hash = "sha256:b1edf8d487721c0bf64d83c28e91280781f6e21f4a797d3261c7c828d4c165bf", size = 52441, upload-time = "2026-01-01T19:42:14.99Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/9e/038522f50ceb7e74f1f991bf1b699f24b0c2bbe7c390dd36ad69f4582258/json5-0.13.0-py3-none-any.whl", hash = "sha256:9a08e1dd65f6a4d4c6fa82d216cf2477349ec2346a38fd70cc11d2557499fbcc", size = 36163, upload-time = "2026-01-01T19:42:13.962Z" }, +] + +[[package]] +name = "jsonpointer" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114, upload-time = "2024-06-10T19:24:42.462Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595, upload-time = "2024-06-10T19:24:40.698Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[package.optional-dependencies] +format-nongpl = [ + { name = "fqdn" }, + { name = "idna" }, + { name = "isoduration" }, + { name = "jsonpointer" }, + { name = "rfc3339-validator" }, + { name = "rfc3986-validator" }, + { name = "rfc3987-syntax" }, + { name = "uri-template" }, + { name = "webcolors" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "jupyter-client" +version = "8.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-core" }, + { name = "python-dateutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/e4/ba649102a3bc3fbca54e7239fb924fd434c766f855693d86de0b1f2bec81/jupyter_client-8.8.0.tar.gz", hash = "sha256:d556811419a4f2d96c869af34e854e3f059b7cc2d6d01a9cd9c85c267691be3e", size = 348020, upload-time = "2026-01-08T13:55:47.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl", hash = "sha256:f93a5b99c5e23a507b773d3a1136bd6e16c67883ccdbd9a829b0bbdb98cd7d7a", size = 107371, upload-time = "2026-01-08T13:55:45.562Z" }, +] + +[[package]] +name = "jupyter-core" +version = "5.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "platformdirs" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" }, +] + +[[package]] +name = "jupyter-events" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonschema", extra = ["format-nongpl"] }, + { name = "packaging" }, + { name = "python-json-logger" }, + { name = "pyyaml" }, + { name = "referencing" }, + { name = "rfc3339-validator" }, + { name = "rfc3986-validator" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/c3/306d090461e4cf3cd91eceaff84bede12a8e52cd821c2d20c9a4fd728385/jupyter_events-0.12.0.tar.gz", hash = "sha256:fc3fce98865f6784c9cd0a56a20644fc6098f21c8c33834a8d9fe383c17e554b", size = 62196, upload-time = "2025-02-03T17:23:41.485Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl", hash = "sha256:6464b2fa5ad10451c3d35fabc75eab39556ae1e2853ad0c0cc31b656731a97fb", size = 19430, upload-time = "2025-02-03T17:23:38.643Z" }, +] + +[[package]] +name = "jupyter-lsp" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-server" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/5a/9066c9f8e94ee517133cd98dba393459a16cd48bba71a82f16a65415206c/jupyter_lsp-2.3.0.tar.gz", hash = "sha256:458aa59339dc868fb784d73364f17dbce8836e906cd75fd471a325cba02e0245", size = 54823, upload-time = "2025-08-27T17:47:34.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/60/1f6cee0c46263de1173894f0fafcb3475ded276c472c14d25e0280c18d6d/jupyter_lsp-2.3.0-py3-none-any.whl", hash = "sha256:e914a3cb2addf48b1c7710914771aaf1819d46b2e5a79b0f917b5478ec93f34f", size = 76687, upload-time = "2025-08-27T17:47:33.15Z" }, +] + +[[package]] +name = "jupyter-server" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "argon2-cffi" }, + { name = "jinja2" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "jupyter-events" }, + { name = "jupyter-server-terminals" }, + { name = "nbconvert" }, + { name = "nbformat" }, + { name = "overrides", marker = "python_full_version < '3.12'" }, + { name = "packaging" }, + { name = "prometheus-client" }, + { name = "pywinpty", marker = "os_name == 'nt'" }, + { name = "pyzmq" }, + { name = "send2trash" }, + { name = "terminado" }, + { name = "tornado" }, + { name = "traitlets" }, + { name = "websocket-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/ac/e040ec363d7b6b1f11304cc9f209dac4517ece5d5e01821366b924a64a50/jupyter_server-2.17.0.tar.gz", hash = "sha256:c38ea898566964c888b4772ae1ed58eca84592e88251d2cfc4d171f81f7e99d5", size = 731949, upload-time = "2025-08-21T14:42:54.042Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/80/a24767e6ca280f5a49525d987bf3e4d7552bf67c8be07e8ccf20271f8568/jupyter_server-2.17.0-py3-none-any.whl", hash = "sha256:e8cb9c7db4251f51ed307e329b81b72ccf2056ff82d50524debde1ee1870e13f", size = 388221, upload-time = "2025-08-21T14:42:52.034Z" }, +] + +[[package]] +name = "jupyter-server-terminals" +version = "0.5.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywinpty", marker = "os_name == 'nt'" }, + { name = "terminado" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/a7/bcd0a9b0cbba88986fe944aaaf91bfda603e5a50bda8ed15123f381a3b2f/jupyter_server_terminals-0.5.4.tar.gz", hash = "sha256:bbda128ed41d0be9020349f9f1f2a4ab9952a73ed5f5ac9f1419794761fb87f5", size = 31770, upload-time = "2026-01-14T16:53:20.213Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl", hash = "sha256:55be353fc74a80bc7f3b20e6be50a55a61cd525626f578dcb66a5708e2007d14", size = 13704, upload-time = "2026-01-14T16:53:18.738Z" }, +] + +[[package]] +name = "jupyterlab" +version = "4.5.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-lru" }, + { name = "httpx" }, + { name = "ipykernel" }, + { name = "jinja2" }, + { name = "jupyter-core" }, + { name = "jupyter-lsp" }, + { name = "jupyter-server" }, + { name = "jupyterlab-server" }, + { name = "notebook-shim" }, + { name = "packaging" }, + { name = "setuptools" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/6b/21af7c0512bdf67e0c54c121779a1f2a97a164a7657e13fced79db8fa5a0/jupyterlab-4.5.4.tar.gz", hash = "sha256:c215f48d8e4582bd2920ad61cc6a40d8ebfef7e5a517ae56b8a9413c9789fdfb", size = 23943597, upload-time = "2026-02-11T00:26:55.308Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/9f/a70972ece62ead2d81acc6223188f6d18a92f665ccce17796a0cdea4fcf5/jupyterlab-4.5.4-py3-none-any.whl", hash = "sha256:cc233f70539728534669fb0015331f2a3a87656207b3bb2d07916e9289192f12", size = 12391867, upload-time = "2026-02-11T00:26:51.23Z" }, +] + +[[package]] +name = "jupyterlab-pygments" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/51/9187be60d989df97f5f0aba133fa54e7300f17616e065d1ada7d7646b6d6/jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d", size = 512900, upload-time = "2023-11-23T09:26:37.44Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780", size = 15884, upload-time = "2023-11-23T09:26:34.325Z" }, +] + +[[package]] +name = "jupyterlab-server" +version = "2.28.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "jinja2" }, + { name = "json5" }, + { name = "jsonschema" }, + { name = "jupyter-server" }, + { name = "packaging" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/2c/90153f189e421e93c4bb4f9e3f59802a1f01abd2ac5cf40b152d7f735232/jupyterlab_server-2.28.0.tar.gz", hash = "sha256:35baa81898b15f93573e2deca50d11ac0ae407ebb688299d3a5213265033712c", size = 76996, upload-time = "2025-10-22T13:59:18.37Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl", hash = "sha256:e4355b148fdcf34d312bbbc80f22467d6d20460e8b8736bf235577dd18506968", size = 59830, upload-time = "2025-10-22T13:59:16.767Z" }, +] + [[package]] name = "kiwisolver" version = "1.4.9" @@ -894,6 +1503,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929, upload-time = "2025-08-10T21:27:48.236Z" }, ] +[[package]] +name = "lark" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/34/28fff3ab31ccff1fd4f6c7c7b0ceb2b6968d8ea4950663eadcb5720591a0/lark-1.3.1.tar.gz", hash = "sha256:b426a7a6d6d53189d318f2b6236ab5d6429eaf09259f1ca33eb716eed10d2905", size = 382732, upload-time = "2025-10-27T18:25:56.653Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl", hash = "sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12", size = 113151, upload-time = "2025-10-27T18:25:54.882Z" }, +] + [[package]] name = "librt" version = "0.7.4" @@ -1128,6 +1746,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/73/e4/6d6f14b2a759c622f191b2d67e9075a3f56aaccb3be4bb9bb6890030d0a0/matplotlib-3.10.8-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2", size = 8713867, upload-time = "2025-12-10T22:56:48.954Z" }, ] +[[package]] +name = "matplotlib-inline" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" }, +] + [[package]] name = "mergedeep" version = "1.3.4" @@ -1137,6 +1767,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, ] +[[package]] +name = "mistune" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/55/d01f0c4b45ade6536c51170b9043db8b2ec6ddf4a35c7ea3f5f559ac935b/mistune-3.2.0.tar.gz", hash = "sha256:708487c8a8cdd99c9d90eb3ed4c3ed961246ff78ac82f03418f5183ab70e398a", size = 95467, upload-time = "2025-12-23T11:36:34.994Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.0-py3-none-any.whl", hash = "sha256:febdc629a3c78616b94393c6580551e0e34cc289987ec6c35ed3f4be42d0eee1", size = 53598, upload-time = "2025-12-23T11:36:33.211Z" }, +] + [[package]] name = "mkdocs" version = "1.6.1" @@ -1371,6 +2010,61 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] +[[package]] +name = "nbclient" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "nbformat" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/91/1c1d5a4b9a9ebba2b4e32b8c852c2975c872aec1fe42ab5e516b2cecd193/nbclient-0.10.4.tar.gz", hash = "sha256:1e54091b16e6da39e297b0ece3e10f6f29f4ac4e8ee515d29f8a7099bd6553c9", size = 62554, upload-time = "2025-12-23T07:45:46.369Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/a0/5b0c2f11142ed1dddec842457d3f65eaf71a0080894eb6f018755b319c3a/nbclient-0.10.4-py3-none-any.whl", hash = "sha256:9162df5a7373d70d606527300a95a975a47c137776cd942e52d9c7e29ff83440", size = 25465, upload-time = "2025-12-23T07:45:44.51Z" }, +] + +[[package]] +name = "nbconvert" +version = "7.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "bleach", extra = ["css"] }, + { name = "defusedxml" }, + { name = "jinja2" }, + { name = "jupyter-core" }, + { name = "jupyterlab-pygments" }, + { name = "markupsafe" }, + { name = "mistune" }, + { name = "nbclient" }, + { name = "nbformat" }, + { name = "packaging" }, + { name = "pandocfilters" }, + { name = "pygments" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/47/81f886b699450d0569f7bc551df2b1673d18df7ff25cc0c21ca36ed8a5ff/nbconvert-7.17.0.tar.gz", hash = "sha256:1b2696f1b5be12309f6c7d707c24af604b87dfaf6d950794c7b07acab96dda78", size = 862855, upload-time = "2026-01-29T16:37:48.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/4b/8d5f796a792f8a25f6925a96032f098789f448571eb92011df1ae59e8ea8/nbconvert-7.17.0-py3-none-any.whl", hash = "sha256:4f99a63b337b9a23504347afdab24a11faa7d86b405e5c8f9881cd313336d518", size = 261510, upload-time = "2026-01-29T16:37:46.322Z" }, +] + +[[package]] +name = "nbformat" +version = "5.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastjsonschema" }, + { name = "jsonschema" }, + { name = "jupyter-core" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/fd/91545e604bc3dad7dca9ed03284086039b294c6b3d75c0d2fa45f9e9caf3/nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a", size = 142749, upload-time = "2024-04-04T11:20:37.371Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b", size = 78454, upload-time = "2024-04-04T11:20:34.895Z" }, +] + [[package]] name = "nest-asyncio" version = "1.6.0" @@ -1380,6 +2074,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, ] +[[package]] +name = "notebook-shim" +version = "0.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-server" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/54/d2/92fa3243712b9a3e8bafaf60aac366da1cada3639ca767ff4b5b3654ec28/notebook_shim-0.2.4.tar.gz", hash = "sha256:b4b2cfa1b65d98307ca24361f5b30fe785b53c3fd07b7a47e89acb5e6ac638cb", size = 13167, upload-time = "2024-02-14T23:35:18.353Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl", hash = "sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef", size = 13307, upload-time = "2024-02-14T23:35:16.286Z" }, +] + [[package]] name = "numba" version = "0.63.1" @@ -1489,6 +2195,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2d/ee/346fa473e666fe14c52fcdd19ec2424157290a032d4c41f98127bfb31ac7/numpy-2.3.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f16417ec91f12f814b10bafe79ef77e70113a2f5f7018640e7425ff979253425", size = 12967213, upload-time = "2025-11-16T22:52:39.38Z" }, ] +[[package]] +name = "overrides" +version = "7.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/86/b585f53236dec60aba864e050778b25045f857e17f6e5ea0ae95fe80edd2/overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a", size = 22812, upload-time = "2024-01-27T21:01:33.423Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49", size = 17832, upload-time = "2024-01-27T21:01:31.393Z" }, +] + [[package]] name = "packaging" version = "25.0" @@ -1561,6 +2276,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, ] +[[package]] +name = "pandocfilters" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/6f/3dd4940bbe001c06a65f88e36bad298bc7a0de5036115639926b0c5c0458/pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e", size = 8454, upload-time = "2024-01-18T20:08:13.726Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc", size = 8663, upload-time = "2024-01-18T20:08:11.28Z" }, +] + +[[package]] +name = "parso" +version = "0.8.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/76/a1e769043c0c0c9fe391b702539d594731a4362334cdf4dc25d0c09761e7/parso-0.8.6.tar.gz", hash = "sha256:2b9a0332696df97d454fa67b81618fd69c35a7b90327cbe6ba5c92d2c68a7bfd", size = 401621, upload-time = "2026-02-09T15:45:24.425Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.py3-none-any.whl", hash = "sha256:2c549f800b70a5c4952197248825584cb00f033b29c692671d3bf08bf380baff", size = 106894, upload-time = "2026-02-09T15:45:21.391Z" }, +] + [[package]] name = "pathspec" version = "0.12.1" @@ -1570,6 +2303,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, ] +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, +] + [[package]] name = "pillow" version = "12.0.0" @@ -1703,6 +2448,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/f3/061bb702465904b6502f7c9081daee34b09ccbaa4f8c94cf43a2a3b6dd6f/polars_runtime_32-1.37.1-cp310-abi3-win_arm64.whl", hash = "sha256:55f2c4847a8d2e267612f564de7b753a4bde3902eaabe7b436a0a4abf75949a0", size = 41001914, upload-time = "2026-01-12T23:26:12.997Z" }, ] +[[package]] +name = "prometheus-client" +version = "0.24.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/58/a794d23feb6b00fc0c72787d7e87d872a6730dd9ed7c7b3e954637d8f280/prometheus_client-0.24.1.tar.gz", hash = "sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9", size = 85616, upload-time = "2026-01-14T15:26:26.965Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl", hash = "sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055", size = 64057, upload-time = "2026-01-14T15:26:24.42Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + [[package]] name = "propcache" version = "0.4.1" @@ -1802,6 +2568,52 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, ] +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, +] + [[package]] name = "pyarrow" version = "22.0.0" @@ -1852,6 +2664,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/03/f335d6c52b4a4761bcc83499789a1e2e16d9d201a58c327a9b5cc9a41bd9/pyarrow-22.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0c34fe18094686194f204a3b1787a27456897d8a2d62caf84b61e8dfbc0252ae", size = 29185594, upload-time = "2025-10-24T10:09:53.111Z" }, ] +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + [[package]] name = "pygments" version = "2.19.2" @@ -1925,6 +2746,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] +[[package]] +name = "python-json-logger" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/bf/eca6a3d43db1dae7070f70e160ab20b807627ba953663ba07928cdd3dc58/python_json_logger-4.0.0.tar.gz", hash = "sha256:f58e68eb46e1faed27e0f574a55a0455eecd7b8a5b88b85a784519ba3cff047f", size = 17683, upload-time = "2025-10-06T04:15:18.984Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl", hash = "sha256:af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2", size = 15548, upload-time = "2025-10-06T04:15:17.553Z" }, +] + [[package]] name = "pytz" version = "2025.2" @@ -1934,6 +2764,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, ] +[[package]] +name = "pywinpty" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/54/37c7370ba91f579235049dc26cd2c5e657d2a943e01820844ffc81f32176/pywinpty-3.0.3.tar.gz", hash = "sha256:523441dc34d231fb361b4b00f8c99d3f16de02f5005fd544a0183112bcc22412", size = 31309, upload-time = "2026-02-04T21:51:09.524Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/c3/3e75075c7f71735f22b66fab0481f2c98e3a4d58cba55cb50ba29114bcf6/pywinpty-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:dff25a9a6435f527d7c65608a7e62783fc12076e7d44487a4911ee91be5a8ac8", size = 2114430, upload-time = "2026-02-04T21:54:19.485Z" }, + { url = "https://files.pythonhosted.org/packages/8d/1e/8a54166a8c5e4f5cb516514bdf4090be4d51a71e8d9f6d98c0aa00fe45d4/pywinpty-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:fbc1e230e5b193eef4431cba3f39996a288f9958f9c9f092c8a961d930ee8f68", size = 236191, upload-time = "2026-02-04T21:50:36.239Z" }, + { url = "https://files.pythonhosted.org/packages/7c/d4/aeb5e1784d2c5bff6e189138a9ca91a090117459cea0c30378e1f2db3d54/pywinpty-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:c9081df0e49ffa86d15db4a6ba61530630e48707f987df42c9d3313537e81fc0", size = 2113098, upload-time = "2026-02-04T21:54:37.711Z" }, + { url = "https://files.pythonhosted.org/packages/b9/53/7278223c493ccfe4883239cf06c823c56460a8010e0fc778eef67858dc14/pywinpty-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:15e79d870e18b678fb8a5a6105fd38496b55697c66e6fc0378236026bc4d59e9", size = 234901, upload-time = "2026-02-04T21:53:31.35Z" }, + { url = "https://files.pythonhosted.org/packages/e5/cb/58d6ed3fd429c96a90ef01ac9a617af10a6d41469219c25e7dc162abbb71/pywinpty-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9c91dbb026050c77bdcef964e63a4f10f01a639113c4d3658332614544c467ab", size = 2112686, upload-time = "2026-02-04T21:52:03.035Z" }, + { url = "https://files.pythonhosted.org/packages/fd/50/724ed5c38c504d4e58a88a072776a1e880d970789deaeb2b9f7bd9a5141a/pywinpty-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:fe1f7911805127c94cf51f89ab14096c6f91ffdcacf993d2da6082b2142a2523", size = 234591, upload-time = "2026-02-04T21:52:29.821Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ad/90a110538696b12b39fd8758a06d70ded899308198ad2305ac68e361126e/pywinpty-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:3f07a6cf1c1d470d284e614733c3d0f726d2c85e78508ea10a403140c3c0c18a", size = 2112360, upload-time = "2026-02-04T21:55:33.397Z" }, + { url = "https://files.pythonhosted.org/packages/44/0f/7ffa221757a220402bc79fda44044c3f2cc57338d878ab7d622add6f4581/pywinpty-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:15c7c0b6f8e9d87aabbaff76468dabf6e6121332c40fc1d83548d02a9d6a3759", size = 233107, upload-time = "2026-02-04T21:51:45.455Z" }, + { url = "https://files.pythonhosted.org/packages/28/88/2ff917caff61e55f38bcdb27de06ee30597881b2cae44fbba7627be015c4/pywinpty-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:d4b6b7b0fe0cdcd02e956bd57cfe9f4e5a06514eecf3b5ae174da4f951b58be9", size = 2113282, upload-time = "2026-02-04T21:52:08.188Z" }, + { url = "https://files.pythonhosted.org/packages/63/32/40a775343ace542cc43ece3f1d1fce454021521ecac41c4c4573081c2336/pywinpty-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:34789d685fc0d547ce0c8a65e5a70e56f77d732fa6e03c8f74fefb8cbb252019", size = 234207, upload-time = "2026-02-04T21:51:58.687Z" }, + { url = "https://files.pythonhosted.org/packages/8d/54/5d5e52f4cb75028104ca6faf36c10f9692389b1986d34471663b4ebebd6d/pywinpty-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:0c37e224a47a971d1a6e08649a1714dac4f63c11920780977829ed5c8cadead1", size = 2112910, upload-time = "2026-02-04T21:52:30.976Z" }, + { url = "https://files.pythonhosted.org/packages/0a/44/dcd184824e21d4620b06c7db9fbb15c3ad0a0f1fa2e6de79969fb82647ec/pywinpty-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:c4e9c3dff7d86ba81937438d5819f19f385a39d8f592d4e8af67148ceb4f6ab5", size = 233425, upload-time = "2026-02-04T21:51:56.754Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -2001,6 +2851,78 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, ] +[[package]] +name = "pyzmq" +version = "27.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86", size = 1333328, upload-time = "2025-09-08T23:07:45.946Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a0/fc7e78a23748ad5443ac3275943457e8452da67fda347e05260261108cbc/pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581", size = 908803, upload-time = "2025-09-08T23:07:47.551Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/37d15eb05f3bdfa4abea6f6d96eb3bb58585fbd3e4e0ded4e743bc650c97/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f", size = 668836, upload-time = "2025-09-08T23:07:49.436Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c4/2a6fe5111a01005fc7af3878259ce17684fabb8852815eda6225620f3c59/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e", size = 857038, upload-time = "2025-09-08T23:07:51.234Z" }, + { url = "https://files.pythonhosted.org/packages/cb/eb/bfdcb41d0db9cd233d6fb22dc131583774135505ada800ebf14dfb0a7c40/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e", size = 1657531, upload-time = "2025-09-08T23:07:52.795Z" }, + { url = "https://files.pythonhosted.org/packages/ab/21/e3180ca269ed4a0de5c34417dfe71a8ae80421198be83ee619a8a485b0c7/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2", size = 2034786, upload-time = "2025-09-08T23:07:55.047Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b1/5e21d0b517434b7f33588ff76c177c5a167858cc38ef740608898cd329f2/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394", size = 1894220, upload-time = "2025-09-08T23:07:57.172Z" }, + { url = "https://files.pythonhosted.org/packages/03/f2/44913a6ff6941905efc24a1acf3d3cb6146b636c546c7406c38c49c403d4/pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f", size = 567155, upload-time = "2025-09-08T23:07:59.05Z" }, + { url = "https://files.pythonhosted.org/packages/23/6d/d8d92a0eb270a925c9b4dd039c0b4dc10abc2fcbc48331788824ef113935/pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97", size = 633428, upload-time = "2025-09-08T23:08:00.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/14/01afebc96c5abbbd713ecfc7469cfb1bc801c819a74ed5c9fad9a48801cb/pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07", size = 559497, upload-time = "2025-09-08T23:08:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, + { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, + { url = "https://files.pythonhosted.org/packages/60/cb/84a13459c51da6cec1b7b1dc1a47e6db6da50b77ad7fd9c145842750a011/pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5", size = 1122436, upload-time = "2025-09-08T23:08:20.801Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/94414759a69a26c3dd674570a81813c46a078767d931a6c70ad29fc585cb/pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6", size = 1156301, upload-time = "2025-09-08T23:08:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ad/15906493fd40c316377fd8a8f6b1f93104f97a752667763c9b9c1b71d42d/pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7", size = 1341197, upload-time = "2025-09-08T23:08:24.286Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d343f3ce13db53a54cb8946594e567410b2125394dafcc0268d8dda027e0/pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05", size = 897275, upload-time = "2025-09-08T23:08:26.063Z" }, + { url = "https://files.pythonhosted.org/packages/69/2d/d83dd6d7ca929a2fc67d2c3005415cdf322af7751d773524809f9e585129/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9", size = 660469, upload-time = "2025-09-08T23:08:27.623Z" }, + { url = "https://files.pythonhosted.org/packages/3e/cd/9822a7af117f4bc0f1952dbe9ef8358eb50a24928efd5edf54210b850259/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128", size = 847961, upload-time = "2025-09-08T23:08:29.672Z" }, + { url = "https://files.pythonhosted.org/packages/9a/12/f003e824a19ed73be15542f172fd0ec4ad0b60cf37436652c93b9df7c585/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39", size = 1650282, upload-time = "2025-09-08T23:08:31.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4a/e82d788ed58e9a23995cee70dbc20c9aded3d13a92d30d57ec2291f1e8a3/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97", size = 2024468, upload-time = "2025-09-08T23:08:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/d9/94/2da0a60841f757481e402b34bf4c8bf57fa54a5466b965de791b1e6f747d/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db", size = 1885394, upload-time = "2025-09-08T23:08:35.51Z" }, + { url = "https://files.pythonhosted.org/packages/4f/6f/55c10e2e49ad52d080dc24e37adb215e5b0d64990b57598abc2e3f01725b/pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c", size = 574964, upload-time = "2025-09-08T23:08:37.178Z" }, + { url = "https://files.pythonhosted.org/packages/87/4d/2534970ba63dd7c522d8ca80fb92777f362c0f321900667c615e2067cb29/pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2", size = 641029, upload-time = "2025-09-08T23:08:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/f6/fa/f8aea7a28b0641f31d40dea42d7ef003fded31e184ef47db696bc74cd610/pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e", size = 561541, upload-time = "2025-09-08T23:08:42.668Z" }, + { url = "https://files.pythonhosted.org/packages/87/45/19efbb3000956e82d0331bafca5d9ac19ea2857722fa2caacefb6042f39d/pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a", size = 1341197, upload-time = "2025-09-08T23:08:44.973Z" }, + { url = "https://files.pythonhosted.org/packages/48/43/d72ccdbf0d73d1343936296665826350cb1e825f92f2db9db3e61c2162a2/pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea", size = 897175, upload-time = "2025-09-08T23:08:46.601Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2e/a483f73a10b65a9ef0161e817321d39a770b2acf8bcf3004a28d90d14a94/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96", size = 660427, upload-time = "2025-09-08T23:08:48.187Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d2/5f36552c2d3e5685abe60dfa56f91169f7a2d99bbaf67c5271022ab40863/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d", size = 847929, upload-time = "2025-09-08T23:08:49.76Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2a/404b331f2b7bf3198e9945f75c4c521f0c6a3a23b51f7a4a401b94a13833/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146", size = 1650193, upload-time = "2025-09-08T23:08:51.7Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0b/f4107e33f62a5acf60e3ded67ed33d79b4ce18de432625ce2fc5093d6388/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd", size = 2024388, upload-time = "2025-09-08T23:08:53.393Z" }, + { url = "https://files.pythonhosted.org/packages/0d/01/add31fe76512642fd6e40e3a3bd21f4b47e242c8ba33efb6809e37076d9b/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a", size = 1885316, upload-time = "2025-09-08T23:08:55.702Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472, upload-time = "2025-09-08T23:08:58.18Z" }, + { url = "https://files.pythonhosted.org/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401, upload-time = "2025-09-08T23:08:59.802Z" }, + { url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c6/c4dcdecdbaa70969ee1fdced6d7b8f60cfabe64d25361f27ac4665a70620/pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066", size = 836265, upload-time = "2025-09-08T23:09:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/3e/79/f38c92eeaeb03a2ccc2ba9866f0439593bb08c5e3b714ac1d553e5c96e25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604", size = 800208, upload-time = "2025-09-08T23:09:51.073Z" }, + { url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747, upload-time = "2025-09-08T23:09:52.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cf/f2b3784d536250ffd4be70e049f3b60981235d70c6e8ce7e3ef21e1adb25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271", size = 747371, upload-time = "2025-09-08T23:09:54.563Z" }, + { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + [[package]] name = "requests" version = "2.32.5" @@ -2016,6 +2938,147 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] +[[package]] +name = "rfc3339-validator" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513, upload-time = "2021-05-12T16:37:54.178Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490, upload-time = "2021-05-12T16:37:52.536Z" }, +] + +[[package]] +name = "rfc3986-validator" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/88/f270de456dd7d11dcc808abfa291ecdd3f45ff44e3b549ffa01b126464d0/rfc3986_validator-0.1.1.tar.gz", hash = "sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055", size = 6760, upload-time = "2019-10-28T16:00:19.144Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/51/17023c0f8f1869d8806b979a2bffa3f861f26a3f1a66b094288323fba52f/rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9", size = 4242, upload-time = "2019-10-28T16:00:13.976Z" }, +] + +[[package]] +name = "rfc3987-syntax" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lark" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/06/37c1a5557acf449e8e406a830a05bf885ac47d33270aec454ef78675008d/rfc3987_syntax-1.1.0.tar.gz", hash = "sha256:717a62cbf33cffdd16dfa3a497d81ce48a660ea691b1ddd7be710c22f00b4a0d", size = 14239, upload-time = "2025-07-18T01:05:05.015Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl", hash = "sha256:6c3d97604e4c5ce9f714898e05401a0445a641cfa276432b0a648c80856f6a3f", size = 8046, upload-time = "2025-07-18T01:05:03.843Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + [[package]] name = "ruff" version = "0.14.10" @@ -2125,6 +3188,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/47/a494741db7280eae6dc033510c319e34d42dd41b7ac0c7ead39354d1a2b5/scipy-1.16.3-cp314-cp314t-win_arm64.whl", hash = "sha256:21d9d6b197227a12dcbf9633320a4e34c6b0e51c57268df255a0942983bac562", size = 26464127, upload-time = "2025-10-28T17:38:11.34Z" }, ] +[[package]] +name = "send2trash" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/f0/184b4b5f8d00f2a92cf96eec8967a3d550b52cf94362dad1100df9e48d57/send2trash-2.1.0.tar.gz", hash = "sha256:1c72b39f09457db3c05ce1d19158c2cbef4c32b8bedd02c155e49282b7ea7459", size = 17255, upload-time = "2026-01-14T06:27:36.056Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl", hash = "sha256:0da2f112e6d6bb22de6aa6daa7e144831a4febf2a87261451c4ad849fe9a873c", size = 17610, upload-time = "2026-01-14T06:27:35.218Z" }, +] + +[[package]] +name = "setuptools" +version = "82.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/f3/748f4d6f65d1756b9ae577f329c951cda23fb900e4de9f70900ced962085/setuptools-82.0.0.tar.gz", hash = "sha256:22e0a2d69474c6ae4feb01951cb69d515ed23728cf96d05513d36e42b62b37cb", size = 1144893, upload-time = "2026-02-08T15:08:40.206Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/c6/76dc613121b793286a3f91621d7b75a2b493e0390ddca50f11993eadf192/setuptools-82.0.0-py3-none-any.whl", hash = "sha256:70b18734b607bd1da571d097d236cfcfacaf01de45717d59e6e04b96877532e0", size = 1003468, upload-time = "2026-02-08T15:08:38.723Z" }, +] + [[package]] name = "six" version = "1.17.0" @@ -2134,6 +3215,55 @@ 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 = "soupsieve" +version = "2.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "executing" }, + { name = "pure-eval" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, +] + +[[package]] +name = "terminado" +version = "0.18.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess", marker = "os_name != 'nt'" }, + { name = "pywinpty", marker = "os_name == 'nt'" }, + { name = "tornado" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/11/965c6fd8e5cc254f1fe142d547387da17a8ebfd75a3455f637c663fb38a0/terminado-0.18.1.tar.gz", hash = "sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e", size = 32701, upload-time = "2024-03-12T14:34:39.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl", hash = "sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0", size = 14154, upload-time = "2024-03-12T14:34:36.569Z" }, +] + +[[package]] +name = "tinycss2" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/fd/7a5ee21fd08ff70d3d33a5781c255cbe779659bd03278feb98b19ee550f4/tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7", size = 87085, upload-time = "2024-10-24T14:58:29.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" }, +] + [[package]] name = "tomli" version = "2.3.0" @@ -2183,6 +3313,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, ] +[[package]] +name = "tornado" +version = "6.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/1d/0a336abf618272d53f62ebe274f712e213f5a03c0b2339575430b8362ef2/tornado-6.5.4.tar.gz", hash = "sha256:a22fa9047405d03260b483980635f0b041989d8bcc9a313f8fe18b411d84b1d7", size = 513632, upload-time = "2025-12-15T19:21:03.836Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/a9/e94a9d5224107d7ce3cc1fab8d5dc97f5ea351ccc6322ee4fb661da94e35/tornado-6.5.4-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d6241c1a16b1c9e4cc28148b1cda97dd1c6cb4fb7068ac1bedc610768dff0ba9", size = 443909, upload-time = "2025-12-15T19:20:48.382Z" }, + { url = "https://files.pythonhosted.org/packages/db/7e/f7b8d8c4453f305a51f80dbb49014257bb7d28ccb4bbb8dd328ea995ecad/tornado-6.5.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2d50f63dda1d2cac3ae1fa23d254e16b5e38153758470e9956cbc3d813d40843", size = 442163, upload-time = "2025-12-15T19:20:49.791Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b5/206f82d51e1bfa940ba366a8d2f83904b15942c45a78dd978b599870ab44/tornado-6.5.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1cf66105dc6acb5af613c054955b8137e34a03698aa53272dbda4afe252be17", size = 445746, upload-time = "2025-12-15T19:20:51.491Z" }, + { url = "https://files.pythonhosted.org/packages/8e/9d/1a3338e0bd30ada6ad4356c13a0a6c35fbc859063fa7eddb309183364ac1/tornado-6.5.4-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50ff0a58b0dc97939d29da29cd624da010e7f804746621c78d14b80238669335", size = 445083, upload-time = "2025-12-15T19:20:52.778Z" }, + { url = "https://files.pythonhosted.org/packages/50/d4/e51d52047e7eb9a582da59f32125d17c0482d065afd5d3bc435ff2120dc5/tornado-6.5.4-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5fb5e04efa54cf0baabdd10061eb4148e0be137166146fff835745f59ab9f7f", size = 445315, upload-time = "2025-12-15T19:20:53.996Z" }, + { url = "https://files.pythonhosted.org/packages/27/07/2273972f69ca63dbc139694a3fc4684edec3ea3f9efabf77ed32483b875c/tornado-6.5.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9c86b1643b33a4cd415f8d0fe53045f913bf07b4a3ef646b735a6a86047dda84", size = 446003, upload-time = "2025-12-15T19:20:56.101Z" }, + { url = "https://files.pythonhosted.org/packages/d1/83/41c52e47502bf7260044413b6770d1a48dda2f0246f95ee1384a3cd9c44a/tornado-6.5.4-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:6eb82872335a53dd063a4f10917b3efd28270b56a33db69009606a0312660a6f", size = 445412, upload-time = "2025-12-15T19:20:57.398Z" }, + { url = "https://files.pythonhosted.org/packages/10/c7/bc96917f06cbee182d44735d4ecde9c432e25b84f4c2086143013e7b9e52/tornado-6.5.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6076d5dda368c9328ff41ab5d9dd3608e695e8225d1cd0fd1e006f05da3635a8", size = 445392, upload-time = "2025-12-15T19:20:58.692Z" }, + { url = "https://files.pythonhosted.org/packages/0c/1a/d7592328d037d36f2d2462f4bc1fbb383eec9278bc786c1b111cbbd44cfa/tornado-6.5.4-cp39-abi3-win32.whl", hash = "sha256:1768110f2411d5cd281bac0a090f707223ce77fd110424361092859e089b38d1", size = 446481, upload-time = "2025-12-15T19:21:00.008Z" }, + { url = "https://files.pythonhosted.org/packages/d6/6d/c69be695a0a64fd37a97db12355a035a6d90f79067a3cf936ec2b1dc38cd/tornado-6.5.4-cp39-abi3-win_amd64.whl", hash = "sha256:fa07d31e0cd85c60713f2b995da613588aa03e1303d75705dca6af8babc18ddc", size = 446886, upload-time = "2025-12-15T19:21:01.287Z" }, + { url = "https://files.pythonhosted.org/packages/50/49/8dc3fd90902f70084bd2cd059d576ddb4f8bb44c2c7c0e33a11422acb17e/tornado-6.5.4-cp39-abi3-win_arm64.whl", hash = "sha256:053e6e16701eb6cbe641f308f4c1a9541f91b6261991160391bfc342e8a551a1", size = 445910, upload-time = "2025-12-15T19:21:02.571Z" }, +] + +[[package]] +name = "traitlets" +version = "5.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -2201,6 +3359,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, ] +[[package]] +name = "uri-template" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/31/c7/0336f2bd0bcbada6ccef7aaa25e443c118a704f828a0620c6fa0207c1b64/uri-template-1.3.0.tar.gz", hash = "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7", size = 21678, upload-time = "2023-06-21T01:49:05.374Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl", hash = "sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363", size = 11140, upload-time = "2023-06-21T01:49:03.467Z" }, +] + [[package]] name = "urllib3" version = "2.6.3" @@ -2237,6 +3404,42 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, ] +[[package]] +name = "wcwidth" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, +] + +[[package]] +name = "webcolors" +version = "25.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/7a/eb316761ec35664ea5174709a68bbd3389de60d4a1ebab8808bfc264ed67/webcolors-25.10.0.tar.gz", hash = "sha256:62abae86504f66d0f6364c2a8520de4a0c47b80c03fc3a5f1815fedbef7c19bf", size = 53491, upload-time = "2025-10-31T07:51:03.977Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl", hash = "sha256:032c727334856fc0b968f63daa252a1ac93d33db2f5267756623c210e57a4f1d", size = 14905, upload-time = "2025-10-31T07:51:01.778Z" }, +] + +[[package]] +name = "webencodings" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, +] + +[[package]] +name = "websocket-client" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576, upload-time = "2025-10-07T21:16:36.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, +] + [[package]] name = "yarl" version = "1.22.0" From 29a5c7810313b566ba46c613f35852b4b0479bde Mon Sep 17 00:00:00 2001 From: novis10813 Date: Fri, 13 Feb 2026 11:40:50 +0800 Subject: [PATCH 04/25] docs: document safe_* operations semantics and add regression tests - Create docs/dev/safe-operations.md with formal documentation: - NaN propagation rules (strict vs. ignore) - Division safety (EPSILON threshold) - Window size requirements - Degenerate-case handling (constant values, zero variance) - Edge cases (empty data, all NaN, single-element window) - Summary table of all safety patterns - Add tests/factors/test_safe_operations.py with 57 regression tests: - safe_divide: scalar, array, Series, edge cases - Factor division: Factor/Factor, Factor/scalar, rtruediv - Factor.inverse(): near-zero guard - ts_* NaN propagation and window completeness - ts_* degenerate cases (constant values, zero variance) - cs_* NaN propagation (cross-sectional strict mask) - cs_neutralize degenerate case - Math operations (log, sqrt domain safety) - Multi-symbol consistency - EPSILON threshold boundary tests - Update AGENTS.md/GEMINI.md to reference formal docs - Add safe-operations.md to mkdocs.yml navigation Closes #10 --- AGENTS.md | 12 +- docs/dev/safe-operations.md | 250 +++++++++++ mkdocs.yml | 1 + tests/factors/test_safe_operations.py | 584 ++++++++++++++++++++++++++ 4 files changed, 843 insertions(+), 4 deletions(-) create mode 100644 docs/dev/safe-operations.md create mode 100644 tests/factors/test_safe_operations.py diff --git a/AGENTS.md b/AGENTS.md index df54cb9..b0e6024 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,6 +15,9 @@ Factorium 是一個量化因子分析與回測框架,主要模組: ## `safe_` 函數模式 +> **完整文檔**: 請參閱 [`docs/dev/safe-operations.md`](docs/dev/safe-operations.md) +> **回歸測試**: [`tests/factors/test_safe_operations.py`](tests/factors/test_safe_operations.py) + 在此專案中,以 `safe_` 開頭的函數(例如:`safe_mean`, `safe_sum`, `safe_div`)旨在確保計算的「嚴格性」與「安全性」,這對於金融因子的計算尤為重要。 ### 共同特點 @@ -28,9 +31,9 @@ Factorium 是一個量化因子分析與回測框架,主要模組: * **資料充裕度檢查**: 如 `safe_corr` 會在計算前確認是否有足夠的有效數據點(例如:多於 2 個)。 3. **safe_div 一致性規範**: - * **閾值**: 使用 `POSITION_EPSILON`(`1e-10`)判斷分母接近 0 的情況。 - * **缺失值回傳**: Pandas 路徑回傳 `np.nan`,Polars 路徑回傳 `null`(建議使用 `pl.lit(None)`)。 - * **語義**: 分母為 0 或 `abs(denominator) <= POSITION_EPSILON` 時視為缺失,避免產生 `inf`。 + * **閾值**: 使用 `EPSILON`(`1e-10`,定義於 `factorium.constants`)判斷分母接近 0 的情況。`POSITION_EPSILON` 是向後相容的別名。 + * **缺失值回傳**: Pandas/NumPy 路徑回傳 `np.nan`,Polars 路徑回傳 `null`(使用 `pl.lit(None)`)。 + * **語義**: 分母為 0 或 `abs(denominator) <= EPSILON` 時視為缺失,避免產生 `inf`。 ### 範例 @@ -71,7 +74,8 @@ docs/ │ └── backtest.md └── dev/ # 開發者文檔 ├── testing.md - └── regression-operators.md + ├── regression-operators.md + └── safe-operations.md ``` ### 本地預覽 diff --git a/docs/dev/safe-operations.md b/docs/dev/safe-operations.md new file mode 100644 index 0000000..2fac7f6 --- /dev/null +++ b/docs/dev/safe-operations.md @@ -0,0 +1,250 @@ +# Safe Operations Semantics + +This document formalizes the **safe operation semantics** used throughout Factorium for +numerical safety in factor calculations and backtesting. These conventions ensure +deterministic, reproducible results and prevent silent errors from corrupting financial signals. + +--- + +## Core Principles + +1. **Strict NaN Propagation** — Any `NaN` (or `null` in Polars) in the input window + causes the entire window result to be `NaN`/`null`. +2. **Division Safety** — Division by values within `EPSILON` of zero returns `NaN`/`null` + instead of `inf`. +3. **Window Completeness** — Rolling operations require a full window; partial windows + produce `NaN`/`null`. + +These rules are intentionally **stricter** than the defaults in Pandas / Polars / NumPy, +which typically skip `NaN` values. In quantitative finance, silently ignoring missing data +can produce misleading signals, so Factorium opts for explicit failure. + +--- + +## Constants + +All numeric thresholds are defined in `factorium.constants`: + +| Constant | Value | Purpose | +|----------|-------|---------| +| `EPSILON` | `1e-10` | Near-zero threshold for safe division and degenerate-case detection | +| `POSITION_EPSILON` | `EPSILON` (alias) | Legacy alias used in `backtest.utils`; identical to `EPSILON` | +| `MIN_PERIODS_PER_YEAR` | `1.0` | Lower bound for `periods_per_year` in metrics | +| `MAX_PERIODS_PER_YEAR` | `525960.0` | Upper bound (minute-level data) | + +```python +# factorium/constants.py +EPSILON = 1e-10 +``` + +--- + +## Safe Division + +The safe division pattern appears in three contexts in the codebase: + +### 1. `backtest.utils.safe_divide(a, b, default=np.nan)` + +A general-purpose safe division function supporting scalar, NumPy array, and Pandas Series inputs. + +**Rules:** +- If `b` is `NaN` → return `default` (default: `np.nan`) +- If `|b| <= EPSILON` → return `default` +- Otherwise → return `a / b` + +```python +from factorium.backtest.utils import safe_divide + +safe_divide(1.0, 0.0) # → np.nan +safe_divide(1.0, 1e-15) # → np.nan (within EPSILON) +safe_divide(1.0, 2.0) # → 0.5 +safe_divide(1.0, np.nan) # → np.nan +``` + +**Supported input types:** + +| Type of `b` | Near-zero detection | NaN detection | +|-------------|-------------------|---------------| +| Scalar (`float`, `int`) | `abs(b) <= EPSILON` | `np.isnan(b)` | +| `np.ndarray` | `np.abs(b) <= EPSILON` | `np.isnan(b)` | +| `pd.Series` | `b.abs() <= EPSILON` | `b.isna()` | + +### 2. Factor Division (`Factor.__truediv__`) + +The `Factor / Factor` and `Factor / scalar` operations use Polars expressions with the +same EPSILON threshold: + +```python +# Polars path (Factor / Factor) +pl.when(pl.col("other").abs() <= EPSILON) + .then(pl.lit(None)) # → null (Polars equivalent of NaN) + .otherwise(pl.col("factor") / pl.col("other")) + +# Polars path (Factor / scalar) +pl.when(pl.lit(other).abs() <= EPSILON) + .then(pl.lit(None)) + .otherwise(pl.col("factor") / pl.lit(other)) +``` + +**Key difference:** The Polars path returns `null` (not `NaN`) for near-zero denominators. +This is consistent with Polars conventions where `null` represents missing data. + +### 3. `MathOpsMixin.inverse()` + +```python +# 1 / factor with safe division +pl.when(pl.col("factor").abs() <= EPSILON) + .then(pl.lit(None)) + .otherwise(1 / pl.col("factor")) +``` + +--- + +## Strict NaN Propagation in Rolling Operations + +All time-series operations (`ts_*`) follow **strict NaN propagation**: if any value within +the rolling window is `NaN`/`null`, or if the window is not full, the result is `NaN`/`null`. + +### Mechanism + +Polars rolling functions control this via the `min_samples` parameter: + +```python +# min_samples=window ensures NaN if window is not full +pl.col("factor").rolling_mean(window_size=window, min_samples=window).over("symbol") +``` + +When `min_samples == window_size`, Polars will return `null` if: +- The window has fewer than `window` non-null values +- Any value in the window is `null` + +### Operations Using This Pattern + +| Operation | Polars Function | EPSILON Check | +|-----------|----------------|---------------| +| `ts_mean` | `rolling_mean(min_samples=window)` | No | +| `ts_std` | `rolling_std(min_samples=window)` | No | +| `ts_sum` | `rolling_sum(min_samples=window)` | No | +| `ts_min` | `rolling_min(min_samples=window)` | No | +| `ts_max` | `rolling_max(min_samples=window)` | No | +| `ts_median` | `rolling_median(min_samples=window)` | No | +| `ts_kurtosis` | `rolling_kurtosis(min_samples=window)` | No | +| `ts_skewness` | `rolling_skew(min_samples=window)` | No | +| `ts_rank` | `rolling_rank(min_samples=window)` | Yes (constant std check) | +| `ts_scale` | min/max + division | Yes (range < EPSILON) | +| `ts_zscore` | mean/std + division | Yes (std < EPSILON) | +| `ts_corr` | manual cov / (std_x × std_y) | Yes (either std < EPSILON) | +| `ts_beta` | manual cov / var | Yes (var < EPSILON) | +| `ts_cv` | std / \|mean\| | Yes (adds 1e-10 bias term) | + +### Explicit NaN-in-Window Mask + +For operations requiring EPSILON checks, an explicit NaN mask is computed: + +```python +nan_in_window = ( + (pl.col("factor").is_null() | pl.col("factor").is_nan()) + .cast(pl.Int64) + .rolling_max(window_size=window, min_samples=window) + .over("symbol") + .fill_null(1) # Treat incomplete windows as having NaN +) +``` + +This mask is `> 0` if **any** value in the window is `NaN` or `null`, or if the window is +not fully populated. Result computation then uses: + +```python +pl.when(nan_in_window > 0).then(pl.lit(None)).otherwise(computed_expr) +``` + +--- + +## Strict NaN Propagation in Cross-Sectional Operations + +Cross-sectional operations (`cs_*`) apply a **strict NaN mask across the entire +cross-section** at each time step: + +```python +# If ANY symbol has NaN at time t, ALL symbols get NaN at time t +nan_mask = (pl.col("factor").is_null() | pl.col("factor").is_nan()).any().over("end_time") +``` + +### Operations Using This Pattern + +| Operation | EPSILON Check | Special Handling | +|-----------|---------------|------------------| +| `cs_rank` | No | Returns rank / count | +| `cs_zscore` | No (std=0 → ±inf, but caught by NaN mask) | — | +| `cs_demean` | No | — | +| `cs_winsorize` | No | Clips to quantile bounds | +| `cs_neutralize` | Yes (var_x < EPSILON → null) | OLS regression | +| `cs_mean` / `cs_median` | No | — | + +--- + +## Degenerate-Case Handling + +Beyond NaN propagation and division safety, specific operations have additional +degenerate-case guards: + +| Operation | Degenerate Condition | Result | +|-----------|---------------------|--------| +| `ts_rank` | `std < EPSILON` (all values identical) | `null` | +| `ts_scale` | `max - min <= EPSILON` (no range) | `null` | +| `ts_zscore` | `std <= EPSILON` (no variance) | `null` | +| `ts_corr` | `std_x <= EPSILON` or `std_y <= EPSILON` | `null` | +| `ts_beta` | `var_x <= EPSILON` | `null` | +| `cs_neutralize` | `var_x <= EPSILON` | `null` | +| `cs_neutralize` (engine) | `std(x) < EPSILON` | `NaN` (NumPy path) | +| `inverse()` | `|factor| <= EPSILON` | `null` | +| `log()` | `factor <= 0` | `null` | +| `sqrt()` | `factor <= 0` | `null` | + +--- + +## Edge Cases + +### Empty Data + +- `safe_divide` with empty `pd.Series` → empty `pd.Series` +- `neutralize_weights` with empty input → empty `pd.Series(dtype=float)` +- Factor operations on empty DataFrames → empty result DataFrame + +### All NaN Input + +- Rolling operations → all `null` output (no valid windows) +- Cross-sectional operations → all `null` output (NaN mask activates) + +### Single-Element Window + +- `ts_std(window=1)` → always `0.0` (single-value std is 0) +- `ts_corr(window=1)` → all `null` (needs window >= 2) +- `ts_beta(window=1)` → all `null` (needs window >= 2) + +### Infinity Handling + +Some operations explicitly replace `inf` / `-inf` with `null`: + +```python +# ts_scale, ts_zscore +z_expr = pl.when(z_expr.is_finite()).then(z_expr).otherwise(pl.lit(None)) + +# ts_jumpiness, ts_vr +result._lf = result._lf.with_columns( + pl.col("factor").replace(float("inf"), None).replace(float("-inf"), None) +) +``` + +--- + +## Summary Table + +| Pattern | Where Used | Threshold | Missing Value | +|---------|-----------|-----------|---------------| +| Safe division | `safe_divide`, `__truediv__`, `inverse` | `EPSILON` (1e-10) | `NaN` (Pandas/NumPy) / `null` (Polars) | +| Strict NaN propagation (rolling) | All `ts_*` operations | `min_samples=window` | `null` | +| Strict NaN propagation (cross-section) | All `cs_*` operations | `.any().over("end_time")` | `null` | +| Variance/std guard | `ts_corr`, `ts_beta`, `ts_rank`, `cs_neutralize` | `EPSILON` | `null` | +| Range guard | `ts_scale` | `EPSILON` | `null` | +| Infinity filter | `ts_zscore`, `ts_scale`, `ts_jumpiness`, `ts_vr` | `is_finite()` | `null` | diff --git a/mkdocs.yml b/mkdocs.yml index 86832ca..9003bf0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -61,3 +61,4 @@ nav: - 開發者: - 測試策略: dev/testing.md - 回歸運算子設計: dev/regression-operators.md + - Safe Operations 語義: dev/safe-operations.md diff --git a/tests/factors/test_safe_operations.py b/tests/factors/test_safe_operations.py new file mode 100644 index 0000000..c7cc9c3 --- /dev/null +++ b/tests/factors/test_safe_operations.py @@ -0,0 +1,584 @@ +"""Regression tests for safe_* operations and strict NaN propagation semantics. + +These tests lock in the behavior documented in docs/dev/safe-operations.md: +1. Strict NaN propagation (any NaN in window → NaN result) +2. Safe division (near-zero denominator → NaN/null) +3. Window completeness (partial windows → NaN/null) +4. Degenerate-case handling (constant values, zero variance, etc.) + +Reference: Issue #10 +""" + +import numpy as np +import pandas as pd +import polars as pl +import pytest + +from factorium.backtest.utils import safe_divide +from factorium.constants import EPSILON +from factorium.factors.core import Factor + + +# ────────────────────────────────────────────────────────── +# Helpers +# ────────────────────────────────────────────────────────── + + +def _make_factor( + values: list[float], + symbols: list[str] | None = None, + name: str = "test", +) -> Factor: + """Create a Factor from a flat list of values for a single or multiple symbols. + + If symbols is None, uses a single symbol "A" for all values. + If symbols is provided, len(values) must be divisible by len(symbols). + """ + if symbols is None: + symbols = ["A"] + + n_symbols = len(symbols) + n_times = len(values) // n_symbols + assert len(values) == n_symbols * n_times, "values length must be divisible by number of symbols" + + rows = [] + for t in range(n_times): + for s_idx, sym in enumerate(symbols): + rows.append( + { + "start_time": t * 60000, + "end_time": (t + 1) * 60000, + "symbol": sym, + "factor": values[t * n_symbols + s_idx], + } + ) + + df = pl.DataFrame(rows) + return Factor(df, name=name) + + +def _get_values(factor: Factor) -> list[float | None]: + """Extract factor values as a list (None for null).""" + return factor.data["factor"].to_list() + + +def _is_missing(value: float | None) -> bool: + """Check if a value is missing (None/null or NaN). + + Polars uses null (None) for incomplete windows and NaN for windows + contaminated by NaN input. Both represent "missing" in our semantics. + """ + if value is None: + return True + try: + return np.isnan(value) + except (TypeError, ValueError): + return False + + +# ══════════════════════════════════════════════════════════ +# 1. safe_divide (backtest.utils) +# ══════════════════════════════════════════════════════════ + + +class TestSafeDivideScalar: + """Test safe_divide with scalar inputs.""" + + def test_normal_division(self): + assert safe_divide(10.0, 2.0) == 5.0 + + def test_zero_denominator(self): + result = safe_divide(1.0, 0.0) + assert np.isnan(result) + + def test_near_zero_denominator(self): + result = safe_divide(1.0, 1e-15) + assert np.isnan(result) + + def test_epsilon_boundary_below(self): + """Value at exactly EPSILON should return default.""" + result = safe_divide(1.0, EPSILON) + assert np.isnan(result) + + def test_epsilon_boundary_above(self): + """Value above EPSILON should compute normally.""" + result = safe_divide(1.0, EPSILON * 10) + assert not np.isnan(result) + assert result == pytest.approx(1.0 / (EPSILON * 10)) + + def test_nan_denominator(self): + result = safe_divide(1.0, np.nan) + assert np.isnan(result) + + def test_negative_near_zero(self): + result = safe_divide(1.0, -1e-15) + assert np.isnan(result) + + def test_custom_default(self): + result = safe_divide(1.0, 0.0, default=0.0) + assert result == 0.0 + + def test_negative_denominator(self): + assert safe_divide(10.0, -2.0) == -5.0 + + def test_zero_numerator(self): + assert safe_divide(0.0, 2.0) == 0.0 + + +class TestSafeDivideArray: + """Test safe_divide with numpy array inputs.""" + + def test_normal_array(self): + a = np.array([1.0, 2.0, 3.0]) + b = np.array([2.0, 4.0, 6.0]) + result = safe_divide(a, b) + np.testing.assert_array_almost_equal(result, [0.5, 0.5, 0.5]) + + def test_zero_in_array(self): + a = np.array([1.0, 2.0, 3.0]) + b = np.array([2.0, 0.0, 6.0]) + result = safe_divide(a, b) + assert result[0] == pytest.approx(0.5) + assert np.isnan(result[1]) + assert result[2] == pytest.approx(0.5) + + def test_nan_in_array(self): + a = np.array([1.0, 2.0]) + b = np.array([2.0, np.nan]) + result = safe_divide(a, b) + assert result[0] == pytest.approx(0.5) + assert np.isnan(result[1]) + + def test_near_zero_in_array(self): + a = np.array([1.0, 2.0]) + b = np.array([2.0, 1e-15]) + result = safe_divide(a, b) + assert result[0] == pytest.approx(0.5) + assert np.isnan(result[1]) + + +class TestSafeDivideSeries: + """Test safe_divide with pandas Series inputs.""" + + def test_normal_series(self): + a = pd.Series([1.0, 2.0, 3.0]) + b = pd.Series([2.0, 4.0, 6.0]) + result = safe_divide(a, b) + pd.testing.assert_series_equal(result, pd.Series([0.5, 0.5, 0.5])) + + def test_zero_in_series(self): + a = pd.Series([1.0, 2.0, 3.0]) + b = pd.Series([2.0, 0.0, 6.0]) + result = safe_divide(a, b) + assert result.iloc[0] == pytest.approx(0.5) + assert np.isnan(result.iloc[1]) + assert result.iloc[2] == pytest.approx(0.5) + + def test_nan_in_series(self): + a = pd.Series([1.0, 2.0]) + b = pd.Series([2.0, np.nan]) + result = safe_divide(a, b) + assert result.iloc[0] == pytest.approx(0.5) + assert np.isnan(result.iloc[1]) + + def test_empty_series(self): + a = pd.Series(dtype=float) + b = pd.Series(dtype=float) + result = safe_divide(a, b) + assert len(result) == 0 + + +# ══════════════════════════════════════════════════════════ +# 2. Factor Division (safe_div via __truediv__) +# ══════════════════════════════════════════════════════════ + + +class TestFactorDivision: + """Test Factor / Factor and Factor / scalar with EPSILON guard.""" + + def test_normal_factor_division(self): + f1 = _make_factor([10.0, 20.0, 30.0]) + f2 = _make_factor([2.0, 4.0, 5.0]) + result = _get_values(f1 / f2) + assert result == pytest.approx([5.0, 5.0, 6.0]) + + def test_factor_division_by_zero(self): + f1 = _make_factor([10.0, 20.0]) + f2 = _make_factor([2.0, 0.0]) + result = _get_values(f1 / f2) + assert result[0] == pytest.approx(5.0) + assert result[1] is None # Polars null + + def test_factor_division_by_near_zero(self): + f1 = _make_factor([10.0, 20.0]) + f2 = _make_factor([2.0, 1e-15]) + result = _get_values(f1 / f2) + assert result[0] == pytest.approx(5.0) + assert result[1] is None + + def test_scalar_division_by_zero(self): + f1 = _make_factor([10.0, 20.0]) + result = _get_values(f1 / 0.0) + assert all(v is None for v in result) + + def test_scalar_division_by_near_zero(self): + f1 = _make_factor([10.0, 20.0]) + result = _get_values(f1 / 1e-15) + assert all(v is None for v in result) + + def test_scalar_division_normal(self): + f1 = _make_factor([10.0, 20.0]) + result = _get_values(f1 / 2.0) + assert result == pytest.approx([5.0, 10.0]) + + def test_reverse_division(self): + """Test scalar / Factor (rtruediv).""" + f1 = _make_factor([2.0, 0.0, 4.0]) + result = _get_values(10.0 / f1) + assert result[0] == pytest.approx(5.0) + assert result[1] is None # near zero + assert result[2] == pytest.approx(2.5) + + +class TestFactorInverse: + """Test MathOpsMixin.inverse().""" + + def test_normal_inverse(self): + f = _make_factor([2.0, 4.0, 5.0]) + result = _get_values(f.inverse()) + assert result == pytest.approx([0.5, 0.25, 0.2]) + + def test_inverse_near_zero(self): + f = _make_factor([2.0, 0.0, 1e-15]) + result = _get_values(f.inverse()) + assert result[0] == pytest.approx(0.5) + assert result[1] is None + assert result[2] is None + + +# ══════════════════════════════════════════════════════════ +# 3. Strict NaN Propagation — Time-Series Operations +# ══════════════════════════════════════════════════════════ + + +class TestTsNanPropagation: + """Any NaN in window → null output; incomplete window → null output.""" + + def test_ts_mean_nan_in_window(self): + f = _make_factor([1.0, np.nan, 3.0, 4.0, 5.0]) + result = _get_values(f.ts_mean(3)) + # Window [1, NaN, 3] → missing, [NaN, 3, 4] → missing, [3, 4, 5] → 4.0 + assert _is_missing(result[0]) # incomplete window + assert _is_missing(result[1]) # incomplete window + assert _is_missing(result[2]) # NaN in window + assert _is_missing(result[3]) # NaN in window + assert result[4] == pytest.approx(4.0) + + def test_ts_mean_full_clean_window(self): + f = _make_factor([1.0, 2.0, 3.0, 4.0, 5.0]) + result = _get_values(f.ts_mean(3)) + assert _is_missing(result[0]) # window not full + assert _is_missing(result[1]) # window not full + assert result[2] == pytest.approx(2.0) + assert result[3] == pytest.approx(3.0) + assert result[4] == pytest.approx(4.0) + + def test_ts_std_nan_propagation(self): + f = _make_factor([1.0, 2.0, np.nan, 4.0, 5.0]) + result = _get_values(f.ts_std(3)) + assert _is_missing(result[0]) # incomplete window + assert _is_missing(result[1]) # incomplete window + assert _is_missing(result[2]) # NaN in window + assert _is_missing(result[3]) # NaN in window + assert _is_missing(result[4]) # [nan,4,5] → NaN still in window + + def test_ts_sum_incomplete_window(self): + """Partial windows return null.""" + f = _make_factor([1.0, 2.0, 3.0]) + result = _get_values(f.ts_sum(3)) + assert _is_missing(result[0]) + assert _is_missing(result[1]) + assert result[2] == pytest.approx(6.0) + + def test_ts_min_max_nan_propagation(self): + f = _make_factor([1.0, np.nan, 3.0, 4.0]) + result_min = _get_values(f.ts_min(2)) + result_max = _get_values(f.ts_max(2)) + assert _is_missing(result_min[0]) # incomplete + assert _is_missing(result_min[1]) # NaN in window + assert _is_missing(result_min[2]) # NaN in window + assert result_min[3] == pytest.approx(3.0) + assert result_max[3] == pytest.approx(4.0) + + +class TestTsWindowCompleteness: + """Window must have exactly window_size samples.""" + + def test_window_larger_than_data(self): + f = _make_factor([1.0, 2.0]) + result = _get_values(f.ts_mean(5)) + assert all(_is_missing(v) for v in result) + + def test_window_one(self): + f = _make_factor([1.0, 2.0, 3.0]) + result = _get_values(f.ts_mean(1)) + assert result == pytest.approx([1.0, 2.0, 3.0]) + + +class TestTsDegenerateCases: + """Degenerate cases: constant values, zero variance, etc.""" + + def test_ts_zscore_constant_values(self): + """All values identical → std = 0 → null.""" + f = _make_factor([5.0, 5.0, 5.0, 5.0, 5.0]) + result = _get_values(f.ts_zscore(3)) + # std = 0 → should return null for all valid windows + assert result[2] is None + assert result[3] is None + assert result[4] is None + + def test_ts_scale_constant_values(self): + """All values identical → max-min = 0 → null.""" + f = _make_factor([3.0, 3.0, 3.0, 3.0]) + result = _get_values(f.ts_scale(3)) + assert result[2] is None + assert result[3] is None + + def test_ts_rank_constant_values(self): + """All values identical → std < EPSILON → null.""" + f = _make_factor([7.0, 7.0, 7.0, 7.0]) + result = _get_values(f.ts_rank(3)) + assert result[2] is None + assert result[3] is None + + def test_ts_corr_constant_x(self): + """One factor is constant → std = 0 → null.""" + f1 = _make_factor([1.0, 2.0, 3.0, 4.0, 5.0]) + f2 = _make_factor([3.0, 3.0, 3.0, 3.0, 3.0]) + result = _get_values(f1.ts_corr(f2, 3)) + # f2 has zero std → correlation is undefined + assert result[2] is None + assert result[3] is None + assert result[4] is None + + def test_ts_beta_zero_variance(self): + """Regressor has zero variance → beta undefined → null.""" + f1 = _make_factor([1.0, 2.0, 3.0, 4.0]) + f2 = _make_factor([5.0, 5.0, 5.0, 5.0]) + result = _get_values(f1.ts_beta(f2, 3)) + assert result[2] is None + assert result[3] is None + + def test_ts_corr_minimum_window(self): + """ts_corr with window < 2 should return all null.""" + f1 = _make_factor([1.0, 2.0, 3.0]) + f2 = _make_factor([4.0, 5.0, 6.0]) + result = _get_values(f1.ts_corr(f2, 1)) + assert all(v is None for v in result) + + +# ══════════════════════════════════════════════════════════ +# 4. Strict NaN Propagation — Cross-Sectional Operations +# ══════════════════════════════════════════════════════════ + + +class TestCsNanPropagation: + """If ANY symbol has NaN at time t, ALL symbols get null at time t.""" + + def test_cs_rank_with_nan(self): + """One symbol has NaN → entire cross-section is null.""" + f = _make_factor( + [1.0, 2.0, 3.0, np.nan, 5.0, 6.0], + symbols=["A", "B"], + ) + result = _get_values(f.cs_rank()) + # t=0: [1, 2] → valid + assert result[0] is not None + assert result[1] is not None + # t=1: [3, NaN] → both null + assert result[2] is None + assert result[3] is None + # t=2: [5, 6] → valid + assert result[4] is not None + assert result[5] is not None + + def test_cs_zscore_with_nan(self): + f = _make_factor( + [1.0, 2.0, np.nan, 4.0], + symbols=["A", "B"], + ) + result = _get_values(f.cs_zscore()) + # t=0: [1, 2] → valid + assert result[0] is not None + assert result[1] is not None + # t=1: [NaN, 4] → both null + assert result[2] is None + assert result[3] is None + + def test_cs_demean_with_nan(self): + f = _make_factor( + [10.0, np.nan, 30.0, 40.0], + symbols=["A", "B"], + ) + result = _get_values(f.cs_demean()) + # t=0: [10, NaN] → both null + assert result[0] is None + assert result[1] is None + # t=1: [30, 40] → valid + assert result[2] is not None + assert result[3] is not None + + def test_cs_winsorize_with_nan(self): + f = _make_factor( + [1.0, 2.0, 3.0, np.nan, 5.0, 6.0, 7.0, 8.0], + symbols=["A", "B", "C", "D"], + ) + result = _get_values(f.cs_winsorize(0.25)) + # t=0: [1, 2, 3, NaN] → all null + assert all(v is None for v in result[:4]) + # t=1: [5, 6, 7, 8] → all valid + assert all(v is not None for v in result[4:]) + + def test_cs_rank_all_valid(self): + f = _make_factor( + [10.0, 20.0, 30.0, 15.0, 25.0, 35.0], + symbols=["A", "B", "C"], + ) + result = _get_values(f.cs_rank()) + assert all(v is not None for v in result) + + +class TestCsDegenerateCases: + """Cross-sectional degenerate cases.""" + + def test_cs_neutralize_constant_regressor(self): + """Regressor has zero variance → beta undefined → null.""" + f = _make_factor( + [1.0, 2.0, 3.0, 4.0, 5.0, 6.0], + symbols=["A", "B", "C"], + ) + const = _make_factor( + [7.0, 7.0, 7.0, 7.0, 7.0, 7.0], + symbols=["A", "B", "C"], + ) + result = _get_values(f.cs_neutralize(const)) + # Constant regressor → var = 0 → null + assert all(v is None for v in result) + + +# ══════════════════════════════════════════════════════════ +# 5. Math Operations Safety +# ══════════════════════════════════════════════════════════ + + +class TestMathSafety: + """Test math operations with domain-safety guards.""" + + def test_log_positive(self): + f = _make_factor([1.0, np.e, 10.0]) + result = _get_values(f.log()) + assert result[0] == pytest.approx(0.0) + assert result[1] == pytest.approx(1.0) + + def test_log_negative(self): + f = _make_factor([-1.0, 0.0, 1.0]) + result = _get_values(f.log()) + assert result[0] is None # log of negative + assert result[1] is None # log of zero + assert result[2] == pytest.approx(0.0) + + def test_sqrt_negative(self): + f = _make_factor([-4.0, 0.0, 4.0]) + result = _get_values(f.sqrt()) + assert result[0] is None # sqrt of negative + assert result[1] is None # sqrt of zero (factor > 0 is strict) + assert result[2] == pytest.approx(2.0) + + def test_log_with_base(self): + f = _make_factor([1.0, 10.0, 100.0]) + result = _get_values(f.log(base=10)) + assert result[0] == pytest.approx(0.0) + assert result[1] == pytest.approx(1.0) + assert result[2] == pytest.approx(2.0) + + +# ══════════════════════════════════════════════════════════ +# 6. Multi-Symbol Consistency +# ══════════════════════════════════════════════════════════ + + +class TestMultiSymbolConsistency: + """Ensure operations are applied per-symbol for ts_* and per-time for cs_*.""" + + def test_ts_mean_per_symbol(self): + """Each symbol should have independent rolling mean.""" + f = _make_factor( + [1.0, 10.0, 2.0, 20.0, 3.0, 30.0], + symbols=["A", "B"], + ) + result = f.data.sort(["symbol", "end_time"]) + a_vals = result.filter(pl.col("symbol") == "A")["factor"].to_list() + b_vals = result.filter(pl.col("symbol") == "B")["factor"].to_list() + + result_mean = f.ts_mean(2).data.sort(["symbol", "end_time"]) + a_mean = result_mean.filter(pl.col("symbol") == "A")["factor"].to_list() + b_mean = result_mean.filter(pl.col("symbol") == "B")["factor"].to_list() + + assert a_mean[0] is None # incomplete + assert a_mean[1] == pytest.approx(1.5) # mean(1, 2) + assert a_mean[2] == pytest.approx(2.5) # mean(2, 3) + assert b_mean[0] is None + assert b_mean[1] == pytest.approx(15.0) # mean(10, 20) + assert b_mean[2] == pytest.approx(25.0) # mean(20, 30) + + def test_cs_rank_per_time(self): + """Cross-sectional rank is computed per time step.""" + f = _make_factor( + [10.0, 20.0, 30.0, 100.0, 50.0, 1.0], + symbols=["A", "B", "C"], + ) + result = f.cs_rank().data.sort(["end_time", "symbol"]) + vals = result["factor"].to_list() + + # t=0: A=10 (rank 1/3), B=20 (rank 2/3), C=30 (rank 3/3) + assert vals[0] == pytest.approx(1 / 3) + assert vals[1] == pytest.approx(2 / 3) + assert vals[2] == pytest.approx(3 / 3) + + +# ══════════════════════════════════════════════════════════ +# 7. EPSILON Threshold Tests +# ══════════════════════════════════════════════════════════ + + +class TestEpsilonThreshold: + """Test the EPSILON boundary precisely.""" + + def test_epsilon_value(self): + """EPSILON should be 1e-10.""" + assert EPSILON == 1e-10 + + def test_division_at_epsilon(self): + """Division by exactly EPSILON should return null.""" + f1 = _make_factor([1.0]) + f2 = _make_factor([EPSILON]) + result = _get_values(f1 / f2) + assert result[0] is None + + def test_division_just_above_epsilon(self): + """Division by value slightly above EPSILON should succeed.""" + f1 = _make_factor([1.0]) + f2 = _make_factor([EPSILON * 2]) + result = _get_values(f1 / f2) + assert result[0] is not None + assert result[0] == pytest.approx(1.0 / (EPSILON * 2)) + + def test_inverse_at_epsilon(self): + f = _make_factor([EPSILON]) + result = _get_values(f.inverse()) + assert result[0] is None + + def test_inverse_just_above_epsilon(self): + f = _make_factor([EPSILON * 2]) + result = _get_values(f.inverse()) + assert result[0] is not None From 4f44bcd39208a84674602b447ec8b975bb38183f Mon Sep 17 00:00:00 2001 From: novis10813 Date: Fri, 13 Feb 2026 17:37:01 +0800 Subject: [PATCH 05/25] fix: align daily processing boundaries to UTC midnight When _calculate_date_range used datetime.now() directly (with arbitrary time component like 09:32:22 UTC), the day-by-day processing loop in load_aggbar would split the same 1-minute bucket across two adjacent daily DuckDB queries. This produced duplicate bars with partial OHLCV data at every daily boundary (e.g., every day at 09:32 UTC). Fix: snap start/end to UTC midnight boundaries when no explicit dates are provided. This ensures daily query boundaries always align with bar interval bucket boundaries, eliminating duplicate bars. Changes: - loader.py: _calculate_date_range now uses today_midnight + 1 day as end and end - days as start - downloader.py: same alignment for consistency - tests: update freeze_time assertions to expect midnight-aligned dates, add test_midnight_alignment_regardless_of_time --- src/factorium/data/downloader.py | 14 ++++++++++---- src/factorium/data/loader.py | 18 ++++++++++++++---- tests/test_data_loader.py | 30 ++++++++++++++++++++++++------ 3 files changed, 48 insertions(+), 14 deletions(-) diff --git a/src/factorium/data/downloader.py b/src/factorium/data/downloader.py index 7e29078..930799f 100644 --- a/src/factorium/data/downloader.py +++ b/src/factorium/data/downloader.py @@ -211,7 +211,11 @@ def _validate_params(self, data_type: str, market_type: str, futures_type: str) def _calculate_date_range( self, start_date: str | None, end_date: str | None, days: int | None ) -> tuple[datetime, datetime]: - """Calculate date range.""" + """Calculate date range aligned to UTC midnight. + + All returned datetimes are snapped to midnight (00:00:00) to ensure + consistency with the loader's daily processing boundaries. + """ try: if start_date and end_date: try: @@ -226,13 +230,15 @@ def _calculate_date_range( self.logger.error(f"Invalid date format: {str(e)}") raise - end = datetime.now() + # Snap to UTC midnight for consistent daily boundaries + today_midnight = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) + end = today_midnight + timedelta(days=1) if days: if days < 1: raise ValueError("Days must be greater than 0") - start = end - timedelta(days=days - 1) + start = end - timedelta(days=days) else: - start = end - timedelta(days=6) + start = end - timedelta(days=7) return start, end diff --git a/src/factorium/data/loader.py b/src/factorium/data/loader.py index c88ace9..d87f483 100644 --- a/src/factorium/data/loader.py +++ b/src/factorium/data/loader.py @@ -517,7 +517,14 @@ def load_aggbar( def _calculate_date_range( self, start_date: str | None, end_date: str | None, days: int | None ) -> tuple[datetime, datetime]: - """Calculate date range.""" + """Calculate date range aligned to UTC midnight. + + All returned datetimes are snapped to midnight (00:00:00) to ensure + daily processing boundaries align with bar interval buckets. + Without this alignment, a boundary like 09:32:22 UTC would split + the 09:32 minute-bucket across two adjacent daily queries, + producing duplicate bars with partial OHLCV data. + """ if start_date and end_date: return (datetime.strptime(start_date, "%Y-%m-%d"), datetime.strptime(end_date, "%Y-%m-%d")) @@ -527,11 +534,14 @@ def _calculate_date_range( datetime.strptime(start_date, "%Y-%m-%d") + timedelta(days=days), ) - end = datetime.now() + # Snap to UTC midnight to align daily processing boundaries + today_midnight = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) + # end = start of tomorrow (exclusive) to include today's full data + end = today_midnight + timedelta(days=1) if days: - start = end - timedelta(days=days - 1) + start = end - timedelta(days=days) else: - start = end - timedelta(days=6) + start = end - timedelta(days=7) return start, end diff --git a/tests/test_data_loader.py b/tests/test_data_loader.py index 1a5c49c..b726062 100644 --- a/tests/test_data_loader.py +++ b/tests/test_data_loader.py @@ -147,19 +147,37 @@ def test_with_start_date_and_days(self, loader): @freeze_time("2024-06-15 12:00:00") def test_with_only_days(self, loader): - """Test with only days specified (should use today as end).""" + """Test with only days specified — aligned to UTC midnight.""" start_dt, end_dt = loader._calculate_date_range(start_date=None, end_date=None, days=7) - assert end_dt == datetime(2024, 6, 15, 12, 0, 0) - assert start_dt == end_dt - timedelta(days=6) # days-1 = 6 + # end = start of tomorrow (exclusive), start = end - days + assert end_dt == datetime(2024, 6, 16, 0, 0, 0) + assert start_dt == datetime(2024, 6, 9, 0, 0, 0) + # Both must be midnight-aligned + assert start_dt.hour == 0 and start_dt.minute == 0 and start_dt.second == 0 + assert end_dt.hour == 0 and end_dt.minute == 0 and end_dt.second == 0 @freeze_time("2024-06-15 12:00:00") def test_default_7_days(self, loader): - """Test default behavior (no params = 7 days ending today).""" + """Test default behavior (no params = 7 days ending tomorrow midnight).""" start_dt, end_dt = loader._calculate_date_range(start_date=None, end_date=None, days=None) - assert end_dt == datetime(2024, 6, 15, 12, 0, 0) - assert start_dt == end_dt - timedelta(days=6) + assert end_dt == datetime(2024, 6, 16, 0, 0, 0) + assert start_dt == datetime(2024, 6, 9, 0, 0, 0) + # Both must be midnight-aligned + assert start_dt.hour == 0 and start_dt.minute == 0 and start_dt.second == 0 + assert end_dt.hour == 0 and end_dt.minute == 0 and end_dt.second == 0 + + @freeze_time("2024-06-15 23:59:59") + def test_midnight_alignment_regardless_of_time(self, loader): + """Test that date range is always midnight-aligned, even when called at 23:59:59.""" + start_dt, end_dt = loader._calculate_date_range(start_date=None, end_date=None, days=3) + + # Should snap to midnight boundaries + assert start_dt == datetime(2024, 6, 13, 0, 0, 0) + assert end_dt == datetime(2024, 6, 16, 0, 0, 0) + assert start_dt.microsecond == 0 + assert end_dt.microsecond == 0 def test_cross_month_range(self, loader): """Test date range crossing month boundary.""" From ef7ddc3792fa17ef028caae8cb67c07e3ecbc72b Mon Sep 17 00:00:00 2001 From: novis10813 Date: Fri, 13 Feb 2026 17:55:54 +0800 Subject: [PATCH 06/25] refactor(data): extract date range calculation to shared utility function - Create calculate_date_range() in data/utils.py for reusable date logic - Remove duplicate _calculate_date_range() from BinanceDataLoader and BinanceDataDownloader - Use shared get_market_string() from parquet.py instead of local implementation - Update tests to call utility function directly instead of private methods - Export calculate_date_range from data module public API This addresses PR review feedback to consolidate duplicated time calculation logic. --- src/factorium/data/__init__.py | 2 + src/factorium/data/downloader.py | 40 +----------------- src/factorium/data/loader.py | 42 ++----------------- src/factorium/data/utils.py | 70 ++++++++++++++++++++++++++++++++ tests/test_data_loader.py | 41 ++++++++++--------- 5 files changed, 99 insertions(+), 96 deletions(-) create mode 100644 src/factorium/data/utils.py diff --git a/src/factorium/data/__init__.py b/src/factorium/data/__init__.py index e7987fd..63ba728 100644 --- a/src/factorium/data/__init__.py +++ b/src/factorium/data/__init__.py @@ -21,6 +21,7 @@ get_market_string, read_hive_parquet, ) +from .utils import calculate_date_range __all__ = [ "BinanceDataLoader", @@ -33,4 +34,5 @@ "build_hive_path", "get_market_string", "BINANCE_COLUMNS", + "calculate_date_range", ] diff --git a/src/factorium/data/downloader.py b/src/factorium/data/downloader.py index 930799f..356270a 100644 --- a/src/factorium/data/downloader.py +++ b/src/factorium/data/downloader.py @@ -17,6 +17,7 @@ import aiohttp from .parquet import build_hive_path, csv_to_parquet, get_market_string +from .utils import calculate_date_range class BinanceDataDownloader: @@ -82,7 +83,7 @@ async def download_data( days: Number of days to download """ self._validate_params(data_type, market_type, futures_type) - start_date_dt, end_date_dt = self._calculate_date_range(start_date, end_date, days) + start_date_dt, end_date_dt = calculate_date_range(start_date, end_date, days) download_dir = self._setup_download_dir(symbol, data_type, market_type, futures_type) dates = self._generate_date_list(start_date_dt, end_date_dt) @@ -208,43 +209,6 @@ def _validate_params(self, data_type: str, market_type: str, futures_type: str) if market_type == "futures" and futures_type not in ["cm", "um"]: raise ValueError("Invalid futures type") - def _calculate_date_range( - self, start_date: str | None, end_date: str | None, days: int | None - ) -> tuple[datetime, datetime]: - """Calculate date range aligned to UTC midnight. - - All returned datetimes are snapped to midnight (00:00:00) to ensure - consistency with the loader's daily processing boundaries. - """ - try: - if start_date and end_date: - try: - start = datetime.strptime(start_date, "%Y-%m-%d") - end = datetime.strptime(end_date, "%Y-%m-%d") - - if start > end: - raise ValueError("Start date must be earlier than or equal to end date") - - return start, end - except ValueError as e: - self.logger.error(f"Invalid date format: {str(e)}") - raise - - # Snap to UTC midnight for consistent daily boundaries - today_midnight = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) - end = today_midnight + timedelta(days=1) - if days: - if days < 1: - raise ValueError("Days must be greater than 0") - start = end - timedelta(days=days) - else: - start = end - timedelta(days=7) - - return start, end - - except Exception as e: - self.logger.error(f"Error calculating date range: {str(e)}") - raise def _setup_download_dir(self, symbol: str, data_type: str, market_type: str, futures_type: str = "cm") -> Path: """Setup download directory (kept for backward compatibility, now uses temp dir).""" diff --git a/src/factorium/data/loader.py b/src/factorium/data/loader.py index d87f483..fb93aa4 100644 --- a/src/factorium/data/loader.py +++ b/src/factorium/data/loader.py @@ -128,6 +128,7 @@ def _normalize_timestamps_to_ms(df: pl.DataFrame, ts_unit: str) -> pl.DataFrame: from .downloader import BinanceDataDownloader # noqa: E402 from .metadata import AggBarMetadata # noqa: E402 from .parquet import get_market_string # noqa: E402 +from .utils import calculate_date_range # noqa: E402 class BinanceDataLoader: @@ -336,7 +337,7 @@ def load_aggbar( ) # Calculate date range - start_dt, end_dt = self._calculate_date_range(start_date, end_date, days) + start_dt, end_dt = calculate_date_range(start_date, end_date, days) # Download missing data if force_download: @@ -368,7 +369,7 @@ def load_aggbar( aggregator = BarAggregator(duckdb_configurator=duckdb_configurator) cache = BarCache(storage=self.storage) if (use_cache and bar_type == "time") else None - market_str = self._get_market_string(market_type, futures_type) + market_str = get_market_string(market_type, futures_type) # Collect aggregated data all_dfs: list[pl.DataFrame] = [] @@ -514,36 +515,6 @@ def load_aggbar( return AggBar(result_df, combined_meta) - def _calculate_date_range( - self, start_date: str | None, end_date: str | None, days: int | None - ) -> tuple[datetime, datetime]: - """Calculate date range aligned to UTC midnight. - - All returned datetimes are snapped to midnight (00:00:00) to ensure - daily processing boundaries align with bar interval buckets. - Without this alignment, a boundary like 09:32:22 UTC would split - the 09:32 minute-bucket across two adjacent daily queries, - producing duplicate bars with partial OHLCV data. - """ - if start_date and end_date: - return (datetime.strptime(start_date, "%Y-%m-%d"), datetime.strptime(end_date, "%Y-%m-%d")) - - if start_date and not end_date and days: - return ( - datetime.strptime(start_date, "%Y-%m-%d"), - datetime.strptime(start_date, "%Y-%m-%d") + timedelta(days=days), - ) - - # Snap to UTC midnight to align daily processing boundaries - today_midnight = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) - # end = start of tomorrow (exclusive) to include today's full data - end = today_midnight + timedelta(days=1) - if days: - start = end - timedelta(days=days) - else: - start = end - timedelta(days=7) - - return start, end def load_aggbar_fast( self, @@ -599,11 +570,6 @@ def load_aggbar_fast( use_cache=use_cache, ) - def _get_market_string(self, market_type: str, futures_type: str) -> str: - """Get market string for cache key.""" - if market_type == "futures": - return f"futures_{futures_type}" - return market_type def _check_all_symbols_exist( self, @@ -777,7 +743,7 @@ def _load_klines_direct( """ adapter = BinanceAdapter() - market_str = self._get_market_string(market_type, futures_type) + market_str = get_market_string(market_type, futures_type) # Build parquet glob pattern parquet_pattern = adapter.build_parquet_glob( diff --git a/src/factorium/data/utils.py b/src/factorium/data/utils.py new file mode 100644 index 0000000..2bd70a3 --- /dev/null +++ b/src/factorium/data/utils.py @@ -0,0 +1,70 @@ +""" +Shared utilities for data loading and processing. +""" + +import logging +from datetime import datetime, timedelta + +logger = logging.getLogger(__name__) + + +def calculate_date_range( + start_date: str | None = None, + end_date: str | None = None, + days: int | None = None, + default_days: int = 7, +) -> tuple[datetime, datetime]: + """Calculate date range aligned to UTC midnight. + + All returned datetimes are snapped to midnight (00:00:00) to ensure + daily processing boundaries align with bar interval buckets. + Without this alignment, a boundary like 09:32:22 UTC would split + the 09:32 minute-bucket across two adjacent daily queries, + producing duplicate bars with partial OHLCV data. + + Priority: + 1. If both start_date and end_date are provided: [start, end] + 2. If start_date and days are provided: [start, start + days] + 3. If neither: [today_midnight - default_days, today_midnight + 1] + 4. If only days: [today_midnight - days, today_midnight + 1] + + Args: + start_date: Start date string (YYYY-MM-DD) + end_date: End date string (YYYY-MM-DD) + days: Number of days + default_days: Default number of days if none specified (default: 7) + + Returns: + tuple[datetime, datetime]: (start_dt, end_dt) snapped to midnight. + """ + try: + if start_date and end_date: + start = datetime.strptime(start_date, "%Y-%m-%d") + end = datetime.strptime(end_date, "%Y-%m-%d") + if start > end: + raise ValueError("Start date must be earlier than or equal to end date") + return start, end + + if start_date and days: + if days < 1: + raise ValueError("Days must be greater than 0") + start = datetime.strptime(start_date, "%Y-%m-%d") + return start, start + timedelta(days=days) + + # Snap to UTC midnight for consistent daily boundaries + today_midnight = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) + # end = start of tomorrow (exclusive) to include today's full data + end = today_midnight + timedelta(days=1) + + if days: + if days < 1: + raise ValueError("Days must be greater than 0") + start = end - timedelta(days=days) + else: + start = end - timedelta(days=default_days) + + return start, end + + except Exception as e: + logger.error(f"Error calculating date range: {str(e)}") + raise diff --git a/tests/test_data_loader.py b/tests/test_data_loader.py index b726062..5dc0f8d 100644 --- a/tests/test_data_loader.py +++ b/tests/test_data_loader.py @@ -13,7 +13,7 @@ from freezegun import freeze_time from factorium import BinanceDataLoader, AggBar -from factorium.data import build_hive_path +from factorium.data import build_hive_path, calculate_date_range # ============================================================================= @@ -129,26 +129,26 @@ def create_parquet_file(base_path: Path, market: str, data_type: str, symbol: st class TestCalculateDateRange: - """Tests for BinanceDataLoader._calculate_date_range method.""" + """Tests for calculate_date_range utility function.""" - def test_with_start_and_end_date(self, loader): + def test_with_start_and_end_date(self): """Test with both start_date and end_date specified.""" - start_dt, end_dt = loader._calculate_date_range(start_date="2024-01-01", end_date="2024-01-07", days=None) + start_dt, end_dt = calculate_date_range(start_date="2024-01-01", end_date="2024-01-07", days=None) assert start_dt == datetime(2024, 1, 1) assert end_dt == datetime(2024, 1, 7) - def test_with_start_date_and_days(self, loader): + def test_with_start_date_and_days(self): """Test with start_date and days specified.""" - start_dt, end_dt = loader._calculate_date_range(start_date="2024-01-01", end_date=None, days=7) + start_dt, end_dt = calculate_date_range(start_date="2024-01-01", end_date=None, days=7) assert start_dt == datetime(2024, 1, 1) assert end_dt == datetime(2024, 1, 8) # 7 days after start @freeze_time("2024-06-15 12:00:00") - def test_with_only_days(self, loader): + def test_with_only_days(self): """Test with only days specified — aligned to UTC midnight.""" - start_dt, end_dt = loader._calculate_date_range(start_date=None, end_date=None, days=7) + start_dt, end_dt = calculate_date_range(start_date=None, end_date=None, days=7) # end = start of tomorrow (exclusive), start = end - days assert end_dt == datetime(2024, 6, 16, 0, 0, 0) @@ -158,9 +158,9 @@ def test_with_only_days(self, loader): assert end_dt.hour == 0 and end_dt.minute == 0 and end_dt.second == 0 @freeze_time("2024-06-15 12:00:00") - def test_default_7_days(self, loader): + def test_default_7_days(self): """Test default behavior (no params = 7 days ending tomorrow midnight).""" - start_dt, end_dt = loader._calculate_date_range(start_date=None, end_date=None, days=None) + start_dt, end_dt = calculate_date_range(start_date=None, end_date=None, days=None) assert end_dt == datetime(2024, 6, 16, 0, 0, 0) assert start_dt == datetime(2024, 6, 9, 0, 0, 0) @@ -169,9 +169,9 @@ def test_default_7_days(self, loader): assert end_dt.hour == 0 and end_dt.minute == 0 and end_dt.second == 0 @freeze_time("2024-06-15 23:59:59") - def test_midnight_alignment_regardless_of_time(self, loader): + def test_midnight_alignment_regardless_of_time(self): """Test that date range is always midnight-aligned, even when called at 23:59:59.""" - start_dt, end_dt = loader._calculate_date_range(start_date=None, end_date=None, days=3) + start_dt, end_dt = calculate_date_range(start_date=None, end_date=None, days=3) # Should snap to midnight boundaries assert start_dt == datetime(2024, 6, 13, 0, 0, 0) @@ -179,35 +179,36 @@ def test_midnight_alignment_regardless_of_time(self, loader): assert start_dt.microsecond == 0 assert end_dt.microsecond == 0 - def test_cross_month_range(self, loader): + def test_cross_month_range(self): """Test date range crossing month boundary.""" - start_dt, end_dt = loader._calculate_date_range(start_date="2024-01-28", end_date=None, days=10) + start_dt, end_dt = calculate_date_range(start_date="2024-01-28", end_date=None, days=10) assert start_dt == datetime(2024, 1, 28) assert end_dt == datetime(2024, 2, 7) # Crosses into February - def test_cross_year_range(self, loader): + def test_cross_year_range(self): """Test date range crossing year boundary.""" - start_dt, end_dt = loader._calculate_date_range(start_date="2023-12-28", end_date="2024-01-05", days=None) + start_dt, end_dt = calculate_date_range(start_date="2023-12-28", end_date="2024-01-05", days=None) assert start_dt == datetime(2023, 12, 28) assert end_dt == datetime(2024, 1, 5) - def test_single_day_range(self, loader): + def test_single_day_range(self): """Test single day range (start == end).""" - start_dt, end_dt = loader._calculate_date_range(start_date="2024-01-01", end_date="2024-01-01", days=None) + start_dt, end_dt = calculate_date_range(start_date="2024-01-01", end_date="2024-01-01", days=None) assert start_dt == datetime(2024, 1, 1) assert end_dt == datetime(2024, 1, 1) - def test_start_date_with_one_day(self, loader): + def test_start_date_with_one_day(self): """Test start_date with days=1.""" - start_dt, end_dt = loader._calculate_date_range(start_date="2024-01-01", end_date=None, days=1) + start_dt, end_dt = calculate_date_range(start_date="2024-01-01", end_date=None, days=1) assert start_dt == datetime(2024, 1, 1) assert end_dt == datetime(2024, 1, 2) + # ============================================================================= # TestBuildDateFilter - 日期過濾條件測試 # ============================================================================= From 35756a2cebe50054de449f5eb2f320191ada30f9 Mon Sep 17 00:00:00 2001 From: novis10813 Date: Fri, 13 Feb 2026 19:13:40 +0800 Subject: [PATCH 07/25] restore(universe): re-apply checklist pipeline after temporary revert --- src/factorium/__init__.py | 27 +++++ src/factorium/aggbar.py | 18 ++++ src/factorium/backtest/vectorized.py | 30 +++++- src/factorium/factors/analyzer.py | 13 ++- src/factorium/factors/core.py | 3 +- src/factorium/universe/__init__.py | 27 +++++ src/factorium/universe/checklist.py | 25 +++++ src/factorium/universe/filters.py | 79 +++++++++++++++ src/factorium/universe/metadata.py | 99 ++++++++++++++++++ src/factorium/universe/rules.py | 122 +++++++++++++++++++++++ src/factorium/universe/tags.py | 115 +++++++++++++++++++++ src/factorium/universe/universe.py | 25 +++++ tests/universe/__init__.py | 0 tests/universe/test_aggbar_mask.py | 111 +++++++++++++++++++++ tests/universe/test_backtest_mask.py | 102 +++++++++++++++++++ tests/universe/test_checklist_filters.py | 97 ++++++++++++++++++ tests/universe/test_factor_mask.py | 70 +++++++++++++ tests/universe/test_integration.py | 108 ++++++++++++++++++++ tests/universe/test_metadata.py | 92 +++++++++++++++++ tests/universe/test_rules.py | 24 +++++ tests/universe/test_tags.py | 62 ++++++++++++ tests/universe/test_universe_rules.py | 93 +++++++++++++++++ 22 files changed, 1336 insertions(+), 6 deletions(-) create mode 100644 src/factorium/universe/__init__.py create mode 100644 src/factorium/universe/checklist.py create mode 100644 src/factorium/universe/filters.py create mode 100644 src/factorium/universe/metadata.py create mode 100644 src/factorium/universe/rules.py create mode 100644 src/factorium/universe/tags.py create mode 100644 src/factorium/universe/universe.py create mode 100644 tests/universe/__init__.py create mode 100644 tests/universe/test_aggbar_mask.py create mode 100644 tests/universe/test_backtest_mask.py create mode 100644 tests/universe/test_checklist_filters.py create mode 100644 tests/universe/test_factor_mask.py create mode 100644 tests/universe/test_integration.py create mode 100644 tests/universe/test_metadata.py create mode 100644 tests/universe/test_rules.py create mode 100644 tests/universe/test_tags.py create mode 100644 tests/universe/test_universe_rules.py diff --git a/src/factorium/__init__.py b/src/factorium/__init__.py index c91681f..bfed669 100644 --- a/src/factorium/__init__.py +++ b/src/factorium/__init__.py @@ -34,6 +34,20 @@ from .factors.base import BaseFactor from .factors.core import Factor from .research import ResearchSession +from .universe import ( + Checklist, + ExcludeLeveragedTokens, + ExcludeStablecoins, + FilterRule, + MetadataProvider, + MinLiquidity, + MinListingAge, + MinVolume, + SymbolMetadata, + TagFilter, + TagProvider, + Universe, +) __version__ = "0.2.1" @@ -45,4 +59,17 @@ "ResearchSession", # Data loading "BinanceDataLoader", + # Universe and checklist + "FilterRule", + "SymbolMetadata", + "Universe", + "Checklist", + "ExcludeStablecoins", + "ExcludeLeveragedTokens", + "MinListingAge", + "TagFilter", + "MinVolume", + "MinLiquidity", + "MetadataProvider", + "TagProvider", ] diff --git a/src/factorium/aggbar.py b/src/factorium/aggbar.py index ed47db6..e0a6241 100644 --- a/src/factorium/aggbar.py +++ b/src/factorium/aggbar.py @@ -17,6 +17,8 @@ from .bar import BaseBar from .data.metadata import AggBarMetadata from .factors.core import Factor + from .universe.checklist import Checklist + from .universe.universe import Universe class AggBar: @@ -211,6 +213,22 @@ def convert_timestamp(value: datetime | int | str | None) -> int | None: return AggBar(self._data.filter(cond)) + def with_mask( + self, + name: str, + mask_source: "Universe | Checklist", + metadata: dict, + tags: dict[str, list[str]] | None = None, + ) -> "AggBar": + """Add a boolean mask column and return a new AggBar.""" + protected_cols = {"start_time", "end_time", "symbol", "open", "high", "low", "close", "volume"} + if name in protected_cols: + raise ValueError(f"Cannot use protected column name: {name}") + + mask_expr = mask_source.apply(self._data.lazy(), metadata, tags) + new_data = self._data.with_columns(mask_expr.alias(name)) + return AggBar(new_data, metadata=self._metadata) + @property def cols(self) -> list[str]: """Return list of column names.""" diff --git a/src/factorium/backtest/vectorized.py b/src/factorium/backtest/vectorized.py index bf8ca0a..743b12a 100644 --- a/src/factorium/backtest/vectorized.py +++ b/src/factorium/backtest/vectorized.py @@ -59,6 +59,7 @@ def __init__( neutralization: Literal["market", "none"] = "market", frequency: str = "1h", constraints: list | None = None, + mask: str | None = None, ): """ Initialize the vectorized backtester. @@ -87,16 +88,21 @@ def __init__( self.periods_per_year = frequency_to_periods_per_year(frequency) self._periods_per_year = self.periods_per_year # Alias for backward compatibility self.constraints = constraints or [] + self._mask = mask # Convert inputs to Polars DataFrames if isinstance(prices, AggBar): if entry_price not in prices.cols: raise ValueError(f"entry_price '{entry_price}' not found in prices") + if mask is not None and mask not in prices.cols: + raise ValueError(f"mask '{mask}' not found in prices") self.prices_df = prices.to_polars() else: self.prices_df = prices if entry_price not in prices.columns: raise ValueError(f"entry_price '{entry_price}' not found in prices") + if mask is not None and mask not in prices.columns: + raise ValueError(f"mask '{mask}' not found in prices") if isinstance(signal, Factor): self.signal_df = signal.lazy.collect() @@ -145,7 +151,10 @@ def summary(self) -> dict[str, Any]: def _prepare_data(self) -> pl.DataFrame: """Merge prices and signals, shift signals to avoid lookahead bias.""" # Get the entry price column - prices_df = self.prices_df.select(["end_time", "symbol", self.entry_price]).rename({self.entry_price: "price"}) + price_cols = ["end_time", "symbol", self.entry_price] + if self._mask is not None: + price_cols.append(self._mask) + prices_df = self.prices_df.select(price_cols).rename({self.entry_price: "price"}) # Prepare signal data signal_df = self.signal_df.select(["end_time", "symbol", "factor"]).rename({"factor": "signal"}) @@ -163,18 +172,33 @@ def _prepare_data(self) -> pl.DataFrame: def _calculate_weights(self, df: pl.DataFrame) -> pl.DataFrame: """Calculate portfolio weights (cross-sectional).""" + signal_col = "prev_signal" + if self._mask is not None: + signal_col = "_masked_signal" + df = df.with_columns( + pl.when(pl.col(self._mask).fill_null(False)) + .then(pl.col("prev_signal")) + .otherwise(None) + .alias(signal_col) + ) + if self.neutralization == "market": # Market neutral: (signal - mean) / sum(|signal - mean|) from .utils import neutralize_weights_polars - df = neutralize_weights_polars(df, "prev_signal", "end_time") + df = neutralize_weights_polars(df, signal_col, "end_time") else: # long-only # Normalize positive signals to sum to 1 - positive_only = pl.when(pl.col("prev_signal") > 0).then(pl.col("prev_signal")).otherwise(0.0) + positive_only = pl.when(pl.col(signal_col) > 0).then(pl.col(signal_col)).otherwise(0.0) df = df.with_columns( [(positive_only / positive_only.sum().over("end_time")).fill_nan(0.0).fill_null(0.0).alias("weight")] ) + if self._mask is not None: + df = df.with_columns( + pl.when(pl.col(self._mask).fill_null(False)).then(pl.col("weight")).otherwise(0.0).alias("weight") + ).drop("_masked_signal") + # Apply constraints for constraint in self.constraints: df = constraint.apply(df) diff --git a/src/factorium/factors/analyzer.py b/src/factorium/factors/analyzer.py index 7c6c152..b90efc4 100644 --- a/src/factorium/factors/analyzer.py +++ b/src/factorium/factors/analyzer.py @@ -222,10 +222,11 @@ class FactorAnalyzer: prices: Factor | None # Type annotation for prices attribute - def __init__(self, factor: Factor, prices: AggBar | Factor, quantiles: int = 5): + def __init__(self, factor: Factor, prices: AggBar | Factor, quantiles: int = 5, mask: str | None = None): self.factor = factor self.quantiles = quantiles self._raw_prices = prices + self._mask = mask if isinstance(prices, AggBar): try: close_col = prices["close"] @@ -340,7 +341,12 @@ def prepare_data(self, periods: list[int] | None = None, price_col: str | None = if price_col not in self._raw_prices._data.columns: available_cols = ", ".join(sorted(self._raw_prices._data.columns)) raise ValueError(f"Price column '{price_col}' not found in AggBar. Available columns: {available_cols}") - prices_lf = self._raw_prices.to_polars().lazy().select(["start_time", "end_time", "symbol", price_col]) + select_cols = ["start_time", "end_time", "symbol", price_col] + if self._mask is not None: + if self._mask not in self._raw_prices._data.columns: + raise ValueError(f"Mask column '{self._mask}' not found in AggBar") + select_cols.append(self._mask) + prices_lf = self._raw_prices.to_polars().lazy().select(select_cols) price_col_name = price_col elif self.prices is not None: # self.prices is a Factor (we've narrowed the type above) @@ -357,6 +363,9 @@ def prepare_data(self, periods: list[int] | None = None, price_col: str | None = how="inner", ) + if self._mask is not None and isinstance(self._raw_prices, AggBar): + df_lf = df_lf.filter(pl.col(self._mask).fill_null(False)) + # Calculate forward returns for each period # return = (price.shift(-p) / price) - 1.0 return_exprs = [] diff --git a/src/factorium/factors/core.py b/src/factorium/factors/core.py index 871b1ea..2168056 100644 --- a/src/factorium/factors/core.py +++ b/src/factorium/factors/core.py @@ -68,6 +68,7 @@ def eval( quantiles: int = 5, output_dir: str | None = None, price_col: str = "close", + mask: str | None = None, **kwargs, ) -> "FactorAnalysisResult": """ @@ -92,7 +93,7 @@ def eval( """ from .analyzer import FactorAnalyzer - analyzer = FactorAnalyzer(factor=self, prices=prices, quantiles=quantiles) + analyzer = FactorAnalyzer(factor=self, prices=prices, quantiles=quantiles, mask=mask) result = analyzer.analyze(price_col=price_col, periods=periods) diff --git a/src/factorium/universe/__init__.py b/src/factorium/universe/__init__.py new file mode 100644 index 0000000..ffe3b20 --- /dev/null +++ b/src/factorium/universe/__init__.py @@ -0,0 +1,27 @@ +from .checklist import Checklist +from .filters import MinLiquidity, MinVolume, TagFilter +from .metadata import MetadataProvider +from .rules import ( + ExcludeLeveragedTokens, + ExcludeStablecoins, + FilterRule, + MinListingAge, + SymbolMetadata, +) +from .tags import TagProvider +from .universe import Universe + +__all__ = [ + "Checklist", + "ExcludeLeveragedTokens", + "ExcludeStablecoins", + "FilterRule", + "MinLiquidity", + "MinListingAge", + "MinVolume", + "MetadataProvider", + "SymbolMetadata", + "TagFilter", + "TagProvider", + "Universe", +] diff --git a/src/factorium/universe/checklist.py b/src/factorium/universe/checklist.py new file mode 100644 index 0000000..dc0319b --- /dev/null +++ b/src/factorium/universe/checklist.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +import polars as pl + +from .rules import FilterRule, SymbolMetadata + + +class Checklist: + """Research checklist built from inclusion filters.""" + + def __init__(self, filters: list[FilterRule]) -> None: + if not filters: + raise ValueError("Checklist must have at least one filter") + self.filters = filters + + def apply( + self, + df: pl.LazyFrame, + metadata: dict[str, SymbolMetadata], + tags: dict[str, list[str]] | None = None, + ) -> pl.Expr: + combined = self.filters[0].apply(df, metadata, tags) + for flt in self.filters[1:]: + combined = combined & flt.apply(df, metadata, tags) + return combined diff --git a/src/factorium/universe/filters.py b/src/factorium/universe/filters.py new file mode 100644 index 0000000..5f8b7c6 --- /dev/null +++ b/src/factorium/universe/filters.py @@ -0,0 +1,79 @@ +from __future__ import annotations + +import polars as pl + +from .rules import SymbolMetadata + + +class TagFilter: + """Filter symbols by include/exclude tag sets.""" + + def __init__(self, include: list[str] | None = None, exclude: list[str] | None = None) -> None: + if include is None and exclude is None: + raise ValueError("At least one of include or exclude must be specified") + self._include = set(include) if include else None + self._exclude = set(exclude) if exclude else None + + def apply( + self, + df: pl.LazyFrame, + metadata: dict[str, SymbolMetadata], + tags: dict[str, list[str]] | None = None, + ) -> pl.Expr: + del df + if tags is None: + raise ValueError("TagFilter requires tags, but tags=None was provided") + + included_symbols: set[str] = set() + for sym, meta in metadata.items(): + base_asset = meta.get("base_asset", "") + symbol_tags = set(tags.get(base_asset, [])) + + if self._include is not None and not (symbol_tags & self._include): + continue + + if self._exclude is not None and (symbol_tags & self._exclude): + continue + + included_symbols.add(sym) + + return pl.col("symbol").is_in(included_symbols) + + +class MinVolume: + """Filter symbols by rolling mean traded volume.""" + + def __init__(self, window: int = 20, threshold: float = 1_000_000) -> None: + self._window = window + self._threshold = threshold + + def apply( + self, + df: pl.LazyFrame, + metadata: dict[str, SymbolMetadata], + tags: dict[str, list[str]] | None = None, + ) -> pl.Expr: + del df, metadata, tags + rolling_volume = ( + pl.col("volume").rolling_mean(window_size=self._window, min_samples=self._window).over("symbol") + ) + return (rolling_volume >= self._threshold).fill_null(False) + + +class MinLiquidity: + """Filter symbols by rolling mean notional turnover.""" + + def __init__(self, window: int = 20, threshold: float = 100_000) -> None: + self._window = window + self._threshold = threshold + + def apply( + self, + df: pl.LazyFrame, + metadata: dict[str, SymbolMetadata], + tags: dict[str, list[str]] | None = None, + ) -> pl.Expr: + del df, metadata, tags + liquidity = pl.col("volume") * pl.col("close") + rolling_liquidity = liquidity.rolling_mean(window_size=self._window, min_samples=self._window).over("symbol") + return (rolling_liquidity >= self._threshold).fill_null(False) diff --git a/src/factorium/universe/metadata.py b/src/factorium/universe/metadata.py new file mode 100644 index 0000000..09ca6cd --- /dev/null +++ b/src/factorium/universe/metadata.py @@ -0,0 +1,99 @@ +from __future__ import annotations + +import asyncio +import json +import time +from pathlib import Path + +import aiohttp + +from .rules import KNOWN_STABLECOINS, LEVERAGED_PATTERNS, SymbolMetadata + + +ENDPOINTS = { + "spot": "https://api.binance.com/api/v3/exchangeInfo", + "um": "https://fapi.binance.com/fapi/v1/exchangeInfo", + "cm": "https://dapi.binance.com/dapi/v1/exchangeInfo", +} + + +class MetadataProvider: + """Fetch and cache symbol metadata from Binance exchangeInfo.""" + + def __init__(self, market: str = "um", cache_dir: str | Path = "./Data/metadata", cache_ttl: int = 86400) -> None: + if market not in ENDPOINTS: + raise ValueError(f"Unsupported market: {market}") + self.market = market + self.cache_dir = Path(cache_dir) + self.cache_ttl = cache_ttl + self._cache_path = self.cache_dir / f"{self.market}_exchange_info.json" + + async def _request_json(self, session: aiohttp.ClientSession, url: str) -> dict: + async with session.get(url) as response: + response.raise_for_status() + return await response.json() + + async def fetch_async(self) -> dict[str, SymbolMetadata]: + cached = self._load_cache() + if cached is not None: + return cached + + endpoint = ENDPOINTS[self.market] + async with aiohttp.ClientSession() as session: + payload = await self._request_json(session, endpoint) + + parsed = self._parse_exchange_info(payload) + self._save_cache(parsed) + return parsed + + def fetch(self) -> dict[str, SymbolMetadata]: + return asyncio.run(self.fetch_async()) + + def _parse_exchange_info(self, data: dict) -> dict[str, SymbolMetadata]: + output: dict[str, SymbolMetadata] = {} + for item in data.get("symbols", []): + symbol = item.get("symbol") + if not symbol: + continue + + base_asset = item.get("baseAsset", "") + onboard_date = item.get("onboardDate") + listing_date = int(onboard_date) if isinstance(onboard_date, (int, float)) else None + + output[symbol] = { + "symbol": symbol, + "base_asset": base_asset, + "quote_asset": item.get("quoteAsset", ""), + "status": item.get("status", ""), + "listing_date": listing_date, + "is_leveraged": bool(LEVERAGED_PATTERNS.search(base_asset)), + "is_stablecoin_pair": base_asset in KNOWN_STABLECOINS, + } + + return output + + def _load_cache(self) -> dict[str, SymbolMetadata] | None: + if not self._cache_path.exists(): + return None + + try: + payload = json.loads(self._cache_path.read_text(encoding="utf-8")) + except (json.JSONDecodeError, OSError): + return None + + saved_at = payload.get("saved_at") + if not isinstance(saved_at, (int, float)): + return None + + if time.time() - float(saved_at) > self.cache_ttl: + return None + + data = payload.get("data") + if not isinstance(data, dict): + return None + return data + + def _save_cache(self, data: dict[str, SymbolMetadata]) -> None: + self.cache_dir.mkdir(parents=True, exist_ok=True) + payload = {"saved_at": time.time(), "data": data} + self._cache_path.write_text(json.dumps(payload), encoding="utf-8") diff --git a/src/factorium/universe/rules.py b/src/factorium/universe/rules.py new file mode 100644 index 0000000..440a172 --- /dev/null +++ b/src/factorium/universe/rules.py @@ -0,0 +1,122 @@ +from __future__ import annotations + +import re +from typing import Protocol, TypedDict, runtime_checkable + +import polars as pl + + +class SymbolMetadata(TypedDict, total=False): + """Metadata for a single trading symbol.""" + + symbol: str + base_asset: str + quote_asset: str + status: str + listing_date: int + is_leveraged: bool + is_stablecoin_pair: bool + + +@runtime_checkable +class FilterRule(Protocol): + """Shared interface for Universe rules and Checklist filters.""" + + def apply( + self, + df: pl.LazyFrame, + metadata: dict[str, SymbolMetadata], + tags: dict[str, list[str]] | None = None, + ) -> pl.Expr: ... + + +KNOWN_STABLECOINS = frozenset( + { + "USDT", + "USDC", + "BUSD", + "DAI", + "TUSD", + "FDUSD", + "USDP", + "USDD", + "UST", + "FRAX", + "LUSD", + "SUSD", + "GUSD", + "USDJ", + "EUR", + "GBP", + "AEUR", + } +) + + +LEVERAGED_PATTERNS = re.compile(r"(UP|DOWN|BEAR|BULL|[0-9]+[LS])$", re.IGNORECASE) + + +class ExcludeStablecoins: + """Exclude symbols where base asset is a stablecoin.""" + + def __init__(self, extra_stablecoins: set[str] | None = None) -> None: + self._extra = extra_stablecoins or set() + + def apply( + self, + df: pl.LazyFrame, + metadata: dict[str, SymbolMetadata], + tags: dict[str, list[str]] | None = None, + ) -> pl.Expr: + del df, tags + all_stables = KNOWN_STABLECOINS | self._extra + excluded_symbols = [ + sym + for sym, meta in metadata.items() + if meta.get("base_asset", "") in all_stables or bool(meta.get("is_stablecoin_pair", False)) + ] + return ~pl.col("symbol").is_in(excluded_symbols) + + +class ExcludeLeveragedTokens: + """Exclude leveraged-token symbols.""" + + def apply( + self, + df: pl.LazyFrame, + metadata: dict[str, SymbolMetadata], + tags: dict[str, list[str]] | None = None, + ) -> pl.Expr: + del df, tags + excluded_symbols = [ + sym + for sym, meta in metadata.items() + if bool(meta.get("is_leveraged", False)) or bool(LEVERAGED_PATTERNS.search(meta.get("base_asset", ""))) + ] + return ~pl.col("symbol").is_in(excluded_symbols) + + +class MinListingAge: + """Exclude symbols younger than configured listing days.""" + + def __init__(self, days: int = 90) -> None: + self._min_ms = days * 86_400_000 + + def apply( + self, + df: pl.LazyFrame, + metadata: dict[str, SymbolMetadata], + tags: dict[str, list[str]] | None = None, + ) -> pl.Expr: + del df, tags + listing_map: dict[str, int] = {} + for sym, meta in metadata.items(): + listing_date = meta.get("listing_date") + if listing_date is not None: + listing_map[sym] = int(listing_date) + + if not listing_map: + return pl.lit(True) + + listing_expr = pl.col("symbol").replace_strict(listing_map, default=None).cast(pl.Int64, strict=False) + return ((pl.col("start_time") - listing_expr) >= self._min_ms) | listing_expr.is_null() diff --git a/src/factorium/universe/tags.py b/src/factorium/universe/tags.py new file mode 100644 index 0000000..d55dfb1 --- /dev/null +++ b/src/factorium/universe/tags.py @@ -0,0 +1,115 @@ +from __future__ import annotations + +import asyncio +import json +import time +from pathlib import Path + +import aiohttp + + +COINGECKO_BASE_URL = "https://api.coingecko.com/api/v3" + + +class TagProvider: + """Fetch and cache token categories from CoinGecko.""" + + def __init__( + self, + cache_dir: str | Path = "./Data/metadata", + cache_ttl: int = 604800, + api_key: str | None = None, + ) -> None: + self.cache_dir = Path(cache_dir) + self.cache_ttl = cache_ttl + self.api_key = api_key + self._cache_path = self.cache_dir / "coingecko_tags.json" + + async def _request_json( + self, + session: aiohttp.ClientSession, + url: str, + params: dict | None = None, + headers: dict[str, str] | None = None, + ) -> dict | list: + async with session.get(url, params=params, headers=headers) as response: + response.raise_for_status() + return await response.json() + + async def fetch_async(self, symbols: list[str] | None = None) -> dict[str, list[str]]: + requested = [s.upper() for s in symbols] if symbols is not None else None + cached = self._load_cache() + + if cached is not None: + if requested is None: + return cached + if all(sym in cached for sym in requested): + return {sym: cached[sym] for sym in requested} + + headers: dict[str, str] | None = None + if self.api_key: + headers = {"x-cg-pro-api-key": self.api_key} + + async with aiohttp.ClientSession() as session: + raw_list = await self._request_json(session, f"{COINGECKO_BASE_URL}/coins/list", headers=headers) + symbol_to_id: dict[str, str] = {} + for item in raw_list if isinstance(raw_list, list) else []: + symbol = str(item.get("symbol", "")).upper() + coin_id = item.get("id") + if symbol and coin_id and symbol not in symbol_to_id: + symbol_to_id[symbol] = str(coin_id) + + targets = requested or sorted(symbol_to_id.keys()) + result: dict[str, list[str]] = {} if cached is None else dict(cached) + + for symbol in targets: + if symbol in result: + continue + + coin_id = symbol_to_id.get(symbol) + if not coin_id: + continue + + detail = await self._request_json(session, f"{COINGECKO_BASE_URL}/coins/{coin_id}", headers=headers) + categories = detail.get("categories", []) if isinstance(detail, dict) else [] + result[symbol] = [str(tag) for tag in categories] + await asyncio.sleep(0.12) + + self._save_cache(result) + if requested is None: + return result + return {sym: result.get(sym, []) for sym in requested if sym in result} + + def fetch(self, symbols: list[str] | None = None) -> dict[str, list[str]]: + return asyncio.run(self.fetch_async(symbols=symbols)) + + def _load_cache(self) -> dict[str, list[str]] | None: + if not self._cache_path.exists(): + return None + + try: + payload = json.loads(self._cache_path.read_text(encoding="utf-8")) + except (json.JSONDecodeError, OSError): + return None + + saved_at = payload.get("saved_at") + if not isinstance(saved_at, (int, float)): + return None + + if time.time() - float(saved_at) > self.cache_ttl: + return None + + data = payload.get("data") + if not isinstance(data, dict): + return None + + out: dict[str, list[str]] = {} + for key, value in data.items(): + if isinstance(value, list): + out[str(key)] = [str(v) for v in value] + return out + + def _save_cache(self, data: dict[str, list[str]]) -> None: + self.cache_dir.mkdir(parents=True, exist_ok=True) + payload = {"saved_at": time.time(), "data": data} + self._cache_path.write_text(json.dumps(payload), encoding="utf-8") diff --git a/src/factorium/universe/universe.py b/src/factorium/universe/universe.py new file mode 100644 index 0000000..ea57c58 --- /dev/null +++ b/src/factorium/universe/universe.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +import polars as pl + +from .rules import FilterRule, SymbolMetadata + + +class Universe: + """Trading universe built from exclusion rules.""" + + def __init__(self, rules: list[FilterRule]) -> None: + if not rules: + raise ValueError("Universe must have at least one rule") + self.rules = rules + + def apply( + self, + df: pl.LazyFrame, + metadata: dict[str, SymbolMetadata], + tags: dict[str, list[str]] | None = None, + ) -> pl.Expr: + combined = self.rules[0].apply(df, metadata, tags) + for rule in self.rules[1:]: + combined = combined & rule.apply(df, metadata, tags) + return combined diff --git a/tests/universe/__init__.py b/tests/universe/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/universe/test_aggbar_mask.py b/tests/universe/test_aggbar_mask.py new file mode 100644 index 0000000..47d7c83 --- /dev/null +++ b/tests/universe/test_aggbar_mask.py @@ -0,0 +1,111 @@ +import polars as pl +import pytest + +from factorium import AggBar +from factorium.universe import ( + Checklist, + ExcludeStablecoins, + MinListingAge, + TagFilter, + Universe, +) + + +DAY_MS = 86_400_000 +BASE_TS = 1_700_000_000_000 + + +class SymbolOnlyRule: + def __init__(self, allowed: set[str]) -> None: + self.allowed = allowed + + def apply(self, df: pl.LazyFrame, metadata: dict, tags: dict[str, list[str]] | None = None) -> pl.Expr: + del df, metadata, tags + return pl.col("symbol").is_in(self.allowed) + + +def _sample_aggbar() -> AggBar: + rows = [] + for i in range(2): + ts = BASE_TS + i * DAY_MS + for symbol, close in [("BTCUSDT", 100.0), ("USDCUSDT", 1.0), ("NEWUSDT", 10.0)]: + rows.append( + { + "start_time": ts, + "end_time": ts + 3_600_000, + "symbol": symbol, + "open": close, + "high": close, + "low": close, + "close": close, + "volume": 1_000.0, + } + ) + return AggBar(pl.DataFrame(rows)) + + +def _metadata() -> dict[str, dict]: + return { + "BTCUSDT": { + "symbol": "BTCUSDT", + "base_asset": "BTC", + "quote_asset": "USDT", + "listing_date": BASE_TS - 500 * DAY_MS, + }, + "USDCUSDT": { + "symbol": "USDCUSDT", + "base_asset": "USDC", + "quote_asset": "USDT", + "is_stablecoin_pair": True, + "listing_date": BASE_TS - 500 * DAY_MS, + }, + "NEWUSDT": {"symbol": "NEWUSDT", "base_asset": "NEW", "quote_asset": "USDT", "listing_date": BASE_TS - DAY_MS}, + } + + +def test_with_mask_returns_new_aggbar_without_mutating_original() -> None: + agg = _sample_aggbar() + masked = agg.with_mask("in_universe", SymbolOnlyRule({"BTCUSDT"}), _metadata()) + + assert "in_universe" not in agg.cols + assert "in_universe" in masked.cols + values = masked.to_polars().filter(pl.col("symbol") == "BTCUSDT")["in_universe"].to_list() + assert values == [True, True] + + +def test_with_mask_rejects_protected_column_name() -> None: + agg = _sample_aggbar() + with pytest.raises(ValueError, match="protected column name"): + agg.with_mask("close", SymbolOnlyRule({"BTCUSDT"}), _metadata()) + + +def test_getitem_factor_does_not_include_mask_column() -> None: + agg = _sample_aggbar().with_mask("in_universe", SymbolOnlyRule({"BTCUSDT"}), _metadata()) + + factor = agg["close"] + assert factor.lazy.collect().columns == ["start_time", "end_time", "symbol", "factor"] + + +def test_universe_and_checklist_masks_can_be_composed() -> None: + agg = _sample_aggbar() + metadata = _metadata() + tags = {"BTC": ["layer1"], "USDC": ["stablecoin"], "NEW": ["meme"]} + + universe = Universe([ExcludeStablecoins(), MinListingAge(days=2)]) + checklist = Checklist([TagFilter(include=["layer1"])]) + + with_universe = agg.with_mask("in_universe", universe, metadata) + with_checklist = with_universe.with_mask("in_checklist", checklist, metadata, tags) + + df = with_checklist.to_polars() + kept = set(df.filter(pl.col("in_universe") & pl.col("in_checklist"))["symbol"].to_list()) + assert kept == {"BTCUSDT"} + + +def test_with_mask_supports_time_varying_rule() -> None: + agg = _sample_aggbar() + metadata = _metadata() + masked = agg.with_mask("old_enough", Universe([MinListingAge(days=2)]), metadata) + + new_values = masked.to_polars().filter(pl.col("symbol") == "NEWUSDT").sort("start_time")["old_enough"].to_list() + assert new_values == [False, True] diff --git a/tests/universe/test_backtest_mask.py b/tests/universe/test_backtest_mask.py new file mode 100644 index 0000000..707a94c --- /dev/null +++ b/tests/universe/test_backtest_mask.py @@ -0,0 +1,102 @@ +import polars as pl + +from factorium import AggBar +from factorium.backtest.vectorized import VectorizedBacktester + + +def _make_prices() -> AggBar: + rows = [] + base_ts = 1_700_000_000_000 + for i in range(6): + ts = base_ts + i * 3_600_000 + rows.extend( + [ + { + "start_time": ts, + "end_time": ts + 3_600_000, + "symbol": "A", + "open": 100.0 + i, + "high": 101.0 + i, + "low": 99.0 + i, + "close": 100.0 + i, + "volume": 1_000.0, + "in_universe": True, + }, + { + "start_time": ts, + "end_time": ts + 3_600_000, + "symbol": "B", + "open": 50.0 + i, + "high": 51.0 + i, + "low": 49.0 + i, + "close": 50.0 + i, + "volume": 1_000.0, + "in_universe": True, + }, + { + "start_time": ts, + "end_time": ts + 3_600_000, + "symbol": "C", + "open": 10.0 + i, + "high": 11.0 + i, + "low": 9.0 + i, + "close": 10.0 + i, + "volume": 1_000.0, + "in_universe": False, + }, + ] + ) + return AggBar(pl.DataFrame(rows)) + + +def _make_signal() -> pl.DataFrame: + rows = [] + base_ts = 1_700_000_000_000 + for i in range(6): + ts = base_ts + i * 3_600_000 + rows.extend( + [ + {"start_time": ts, "end_time": ts + 3_600_000, "symbol": "A", "factor": 3.0 + i}, + {"start_time": ts, "end_time": ts + 3_600_000, "symbol": "B", "factor": 2.0 + i}, + {"start_time": ts, "end_time": ts + 3_600_000, "symbol": "C", "factor": 1.0 + i}, + ] + ) + return pl.DataFrame(rows) + + +def test_backtester_mask_sets_outside_symbols_weight_to_zero() -> None: + prices = _make_prices() + signal = _make_signal() + + bt = VectorizedBacktester(prices=prices, signal=signal, mask="in_universe", neutralization="none") + combined = bt._prepare_data() + weighted = bt._calculate_weights(combined) + + c_weights = weighted.filter((pl.col("symbol") == "C") & (pl.col("prev_signal").is_not_null()))["weight"].to_list() + assert set(c_weights) == {0.0} + + +def test_backtester_mask_applies_before_market_neutralization() -> None: + prices = _make_prices() + signal = _make_signal() + + bt = VectorizedBacktester(prices=prices, signal=signal, mask="in_universe", neutralization="market") + weighted = bt._calculate_weights(bt._prepare_data()) + + sample_time = sorted(set(weighted["end_time"].to_list()))[1] + sample = weighted.filter(pl.col("end_time") == sample_time) + + b_weight = sample.filter(pl.col("symbol") == "B")["weight"].to_list()[0] + c_weight = sample.filter(pl.col("symbol") == "C")["weight"].to_list()[0] + assert b_weight < 0 + assert c_weight == 0.0 + + +def test_backtester_without_mask_keeps_backward_compatible_flow() -> None: + prices = _make_prices() + signal = _make_signal() + + bt = VectorizedBacktester(prices=prices, signal=signal) + result = bt.run() + + assert len(result.equity_curve) > 0 diff --git a/tests/universe/test_checklist_filters.py b/tests/universe/test_checklist_filters.py new file mode 100644 index 0000000..8134446 --- /dev/null +++ b/tests/universe/test_checklist_filters.py @@ -0,0 +1,97 @@ +import polars as pl +import pytest + +from factorium.universe import Checklist, MinLiquidity, MinVolume, TagFilter + + +def _make_panel() -> pl.DataFrame: + rows = [] + base_ts = 1_700_000_000_000 + for i in range(5): + ts = base_ts + i * 3_600_000 + rows.extend( + [ + { + "start_time": ts, + "end_time": ts + 3_600_000, + "symbol": "BTCUSDT", + "close": 100.0, + "volume": 20_000.0, + }, + { + "start_time": ts, + "end_time": ts + 3_600_000, + "symbol": "DOGEUSDT", + "close": 1.0, + "volume": 100.0, + }, + ] + ) + return pl.DataFrame(rows) + + +def _metadata() -> dict[str, dict]: + return { + "BTCUSDT": {"symbol": "BTCUSDT", "base_asset": "BTC", "quote_asset": "USDT"}, + "DOGEUSDT": {"symbol": "DOGEUSDT", "base_asset": "DOGE", "quote_asset": "USDT"}, + } + + +def _tags() -> dict[str, list[str]]: + return { + "BTC": ["store-of-value", "layer1"], + "DOGE": ["meme"], + } + + +def test_tag_filter_include_mode() -> None: + df = _make_panel().lazy() + expr = TagFilter(include=["store-of-value"]).apply(df, _metadata(), _tags()) + out = _make_panel().lazy().with_columns(expr.alias("keep")).collect() + assert set(out.filter(pl.col("keep"))["symbol"].to_list()) == {"BTCUSDT"} + + +def test_tag_filter_exclude_mode() -> None: + df = _make_panel().lazy() + expr = TagFilter(exclude=["meme"]).apply(df, _metadata(), _tags()) + out = _make_panel().lazy().with_columns(expr.alias("keep")).collect() + assert set(out.filter(pl.col("keep"))["symbol"].to_list()) == {"BTCUSDT"} + + +def test_tag_filter_raises_when_tags_missing() -> None: + with pytest.raises(ValueError, match="requires tags"): + TagFilter(include=["layer1"]).apply(_make_panel().lazy(), _metadata(), tags=None) + + +def test_min_volume_uses_rolling_threshold() -> None: + df = _make_panel().lazy() + expr = MinVolume(window=3, threshold=10_000).apply(df, _metadata()) + out = _make_panel().lazy().with_columns(expr.alias("keep")).collect() + + btc = out.filter(pl.col("symbol") == "BTCUSDT").sort("start_time")["keep"].to_list() + doge = out.filter(pl.col("symbol") == "DOGEUSDT").sort("start_time")["keep"].to_list() + assert btc == [False, False, True, True, True] + assert doge == [False, False, False, False, False] + + +def test_min_liquidity_uses_volume_times_close() -> None: + df = _make_panel().lazy() + expr = MinLiquidity(window=3, threshold=500_000).apply(df, _metadata()) + out = _make_panel().lazy().with_columns(expr.alias("keep")).collect() + + btc = out.filter(pl.col("symbol") == "BTCUSDT").sort("start_time")["keep"].to_list() + doge = out.filter(pl.col("symbol") == "DOGEUSDT").sort("start_time")["keep"].to_list() + assert btc == [False, False, True, True, True] + assert doge == [False, False, False, False, False] + + +def test_checklist_combines_filters_with_and_logic() -> None: + checklist = Checklist([TagFilter(exclude=["meme"]), MinVolume(window=3, threshold=10_000)]) + out = ( + _make_panel() + .lazy() + .with_columns(checklist.apply(_make_panel().lazy(), _metadata(), _tags()).alias("ok")) + .collect() + ) + symbols = set(out.filter(pl.col("ok"))["symbol"].to_list()) + assert symbols == {"BTCUSDT"} diff --git a/tests/universe/test_factor_mask.py b/tests/universe/test_factor_mask.py new file mode 100644 index 0000000..3d3cdcc --- /dev/null +++ b/tests/universe/test_factor_mask.py @@ -0,0 +1,70 @@ +import polars as pl + +from factorium import AggBar +from factorium.factors.analyzer import FactorAnalyzer + + +def _make_aggbar() -> AggBar: + rows = [] + base_ts = 1_700_000_000_000 + for i in range(8): + ts = base_ts + i * 3_600_000 + rows.extend( + [ + { + "start_time": ts, + "end_time": ts + 3_600_000, + "symbol": "BTCUSDT", + "open": 100 + i, + "high": 101 + i, + "low": 99 + i, + "close": 100 + i, + "volume": 1_000.0, + "alpha": float(i + 1), + "in_checklist": True, + }, + { + "start_time": ts, + "end_time": ts + 3_600_000, + "symbol": "DOGEUSDT", + "open": 10 + i, + "high": 11 + i, + "low": 9 + i, + "close": 10 + i, + "volume": 1_000.0, + "alpha": float(100 - i), + "in_checklist": False, + }, + ] + ) + return AggBar(pl.DataFrame(rows)) + + +def test_factor_eval_mask_is_backward_compatible_when_none() -> None: + agg = _make_aggbar() + factor = agg["alpha"] + + with_mask_none = factor.eval(prices=agg, periods=1, quantiles=2, mask=None) + without_mask = factor.eval(prices=agg, periods=1, quantiles=2) + + assert with_mask_none.ic_series.equals(without_mask.ic_series) + + +def test_factor_analyzer_applies_mask_from_aggbar() -> None: + agg = _make_aggbar() + factor = agg["alpha"] + + analyzer = FactorAnalyzer(factor=factor, prices=agg, quantiles=2, mask="in_checklist") + prepared = analyzer.prepare_data(periods=[1], price_col="close") + + assert set(prepared["symbol"].to_list()) == {"BTCUSDT"} + + +def test_factor_eval_with_mask_changes_universe_used_for_analysis() -> None: + agg = _make_aggbar() + factor = agg["alpha"] + + unmasked = factor.eval(prices=agg, periods=1, quantiles=2) + masked = factor.eval(prices=agg, periods=1, quantiles=2, mask="in_checklist") + + assert len(masked.ic_series) <= len(unmasked.ic_series) diff --git a/tests/universe/test_integration.py b/tests/universe/test_integration.py new file mode 100644 index 0000000..2165dc8 --- /dev/null +++ b/tests/universe/test_integration.py @@ -0,0 +1,108 @@ +import polars as pl + +from factorium import AggBar +from factorium.factors.analyzer import FactorAnalyzer +from factorium.universe import ( + Checklist, + ExcludeStablecoins, + MinListingAge, + MinVolume, + TagFilter, + Universe, +) + + +DAY_MS = 86_400_000 +BASE_TS = 1_700_000_000_000 + + +def _make_aggbar() -> AggBar: + rows = [] + for i in range(10): + ts = BASE_TS + i * DAY_MS + rows.extend( + [ + { + "start_time": ts, + "end_time": ts + 3_600_000, + "symbol": "BTCUSDT", + "open": 100 + i, + "high": 101 + i, + "low": 99 + i, + "close": 100 + i, + "volume": 20_000 + i, + "alpha": float(i + 1), + }, + { + "start_time": ts, + "end_time": ts + 3_600_000, + "symbol": "USDCUSDT", + "open": 1.0, + "high": 1.0, + "low": 1.0, + "close": 1.0, + "volume": 50_000, + "alpha": float(50 - i), + }, + { + "start_time": ts, + "end_time": ts + 3_600_000, + "symbol": "NEWUSDT", + "open": 10 + i, + "high": 11 + i, + "low": 9 + i, + "close": 10 + i, + "volume": 100 + i, + "alpha": float(100 + i), + }, + ] + ) + return AggBar(pl.DataFrame(rows)) + + +def _metadata() -> dict[str, dict]: + return { + "BTCUSDT": { + "symbol": "BTCUSDT", + "base_asset": "BTC", + "quote_asset": "USDT", + "status": "TRADING", + "listing_date": BASE_TS - 365 * DAY_MS, + }, + "USDCUSDT": { + "symbol": "USDCUSDT", + "base_asset": "USDC", + "quote_asset": "USDT", + "status": "TRADING", + "listing_date": BASE_TS - 365 * DAY_MS, + "is_stablecoin_pair": True, + }, + "NEWUSDT": { + "symbol": "NEWUSDT", + "base_asset": "NEW", + "quote_asset": "USDT", + "status": "TRADING", + "listing_date": BASE_TS - 2 * DAY_MS, + }, + } + + +def test_full_pipeline_universe_checklist_factor_eval() -> None: + bar = _make_aggbar() + metadata = _metadata() + tags = {"BTC": ["layer1"], "USDC": ["stablecoin"], "NEW": ["meme"]} + + universe = Universe([ExcludeStablecoins(), MinListingAge(days=5)]) + checklist = Checklist([TagFilter(include=["layer1"]), MinVolume(window=3, threshold=10_000)]) + + bar = bar.with_mask("in_universe", universe, metadata) + bar = bar.with_mask("in_checklist", checklist, metadata, tags) + + factor = bar["alpha"] + result = factor.eval(prices=bar, periods=1, quantiles=2, mask="in_checklist") + + analyzer = FactorAnalyzer(factor=factor, prices=bar, quantiles=2, mask="in_checklist") + prepared = analyzer.prepare_data(periods=[1], price_col="close") + + assert set(prepared["symbol"].to_list()) == {"BTCUSDT"} + assert result.factor_name == "alpha" diff --git a/tests/universe/test_metadata.py b/tests/universe/test_metadata.py new file mode 100644 index 0000000..4b6e510 --- /dev/null +++ b/tests/universe/test_metadata.py @@ -0,0 +1,92 @@ +import json +from pathlib import Path + +import pytest + +from factorium.universe.metadata import MetadataProvider + + +def test_parse_exchange_info_extracts_symbol_metadata() -> None: + provider = MetadataProvider(market="um") + payload = { + "symbols": [ + { + "symbol": "BTCUSDT", + "baseAsset": "BTC", + "quoteAsset": "USDT", + "status": "TRADING", + "onboardDate": 1_700_000_000_000, + }, + { + "symbol": "BTCUPUSDT", + "baseAsset": "BTCUP", + "quoteAsset": "USDT", + "status": "TRADING", + "onboardDate": 1_700_000_000_000, + }, + { + "symbol": "USDCUSDT", + "baseAsset": "USDC", + "quoteAsset": "USDT", + "status": "TRADING", + }, + ] + } + + out = provider._parse_exchange_info(payload) + + assert out["BTCUSDT"]["listing_date"] == 1_700_000_000_000 + assert out["BTCUSDT"]["is_leveraged"] is False + assert out["BTCUSDT"]["is_stablecoin_pair"] is False + assert out["BTCUPUSDT"]["is_leveraged"] is True + assert out["USDCUSDT"]["is_stablecoin_pair"] is True + + +def test_cache_load_save_and_ttl(tmp_path: Path) -> None: + provider = MetadataProvider(market="um", cache_dir=tmp_path, cache_ttl=60) + sample = { + "BTCUSDT": { + "symbol": "BTCUSDT", + "base_asset": "BTC", + "quote_asset": "USDT", + "status": "TRADING", + "listing_date": 1_700_000_000_000, + "is_leveraged": False, + "is_stablecoin_pair": False, + } + } + + provider._save_cache(sample) + loaded = provider._load_cache() + assert loaded == sample + + cache_path = tmp_path / "um_exchange_info.json" + blob = json.loads(cache_path.read_text(encoding="utf-8")) + blob["saved_at"] = 0 + cache_path.write_text(json.dumps(blob), encoding="utf-8") + + assert provider._load_cache() is None + + +def test_fetch_prefers_cache_without_network(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + provider = MetadataProvider(market="um", cache_dir=tmp_path, cache_ttl=3600) + cached = { + "ETHUSDT": { + "symbol": "ETHUSDT", + "base_asset": "ETH", + "quote_asset": "USDT", + "status": "TRADING", + "listing_date": 1_700_000_000_000, + "is_leveraged": False, + "is_stablecoin_pair": False, + } + } + provider._save_cache(cached) + + async def should_not_call(*args, **kwargs): + raise AssertionError("network should not be called when cache is valid") + + monkeypatch.setattr(provider, "_request_json", should_not_call) + + out = provider.fetch() + assert out == cached diff --git a/tests/universe/test_rules.py b/tests/universe/test_rules.py new file mode 100644 index 0000000..ff347dc --- /dev/null +++ b/tests/universe/test_rules.py @@ -0,0 +1,24 @@ +import polars as pl +import pytest + +from factorium.universe import Checklist, FilterRule, Universe + + +class DummyRule: + def apply(self, df: pl.LazyFrame, metadata: dict, tags: dict[str, list[str]] | None = None) -> pl.Expr: + return pl.lit(True) + + +def test_filter_rule_runtime_protocol() -> None: + rule = DummyRule() + assert isinstance(rule, FilterRule) + + +def test_universe_requires_at_least_one_rule() -> None: + with pytest.raises(ValueError, match="at least one rule"): + Universe([]) + + +def test_checklist_requires_at_least_one_filter() -> None: + with pytest.raises(ValueError, match="at least one filter"): + Checklist([]) diff --git a/tests/universe/test_tags.py b/tests/universe/test_tags.py new file mode 100644 index 0000000..1827602 --- /dev/null +++ b/tests/universe/test_tags.py @@ -0,0 +1,62 @@ +import json +from pathlib import Path + +import pytest + +from factorium.universe.tags import TagProvider + + +def test_fetch_maps_symbols_to_categories(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + provider = TagProvider(cache_dir=tmp_path, cache_ttl=0) + + async def fake_request_json(session, url, params=None, headers=None): + del session, params, headers + if url.endswith("/coins/list"): + return [ + {"id": "bitcoin", "symbol": "btc", "name": "Bitcoin"}, + {"id": "ethereum", "symbol": "eth", "name": "Ethereum"}, + ] + if url.endswith("/coins/bitcoin"): + return {"categories": ["Layer 1", "Store Of Value"]} + if url.endswith("/coins/ethereum"): + return {"categories": ["Layer 1", "Smart Contract Platform"]} + raise AssertionError(f"unexpected url: {url}") + + async def no_sleep(seconds: float) -> None: + del seconds + + monkeypatch.setattr(provider, "_request_json", fake_request_json) + monkeypatch.setattr("factorium.universe.tags.asyncio.sleep", no_sleep) + + out = provider.fetch(symbols=["BTC", "ETH"]) + assert out["BTC"] == ["Layer 1", "Store Of Value"] + assert out["ETH"] == ["Layer 1", "Smart Contract Platform"] + + +def test_cache_load_save_and_ttl(tmp_path: Path) -> None: + provider = TagProvider(cache_dir=tmp_path, cache_ttl=10) + sample = {"BTC": ["Layer 1"], "ETH": ["Layer 1", "Smart Contract Platform"]} + + provider._save_cache(sample) + loaded = provider._load_cache() + assert loaded == sample + + cache_path = tmp_path / "coingecko_tags.json" + blob = json.loads(cache_path.read_text(encoding="utf-8")) + blob["saved_at"] = 0 + cache_path.write_text(json.dumps(blob), encoding="utf-8") + + assert provider._load_cache() is None + + +def test_fetch_uses_cached_subset(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + provider = TagProvider(cache_dir=tmp_path, cache_ttl=3600) + provider._save_cache({"BTC": ["Layer 1"], "ETH": ["Layer 1"]}) + + async def should_not_call(*args, **kwargs): + raise AssertionError("network should not be called for cached subset") + + monkeypatch.setattr(provider, "_request_json", should_not_call) + + out = provider.fetch(symbols=["BTC"]) + assert out == {"BTC": ["Layer 1"]} diff --git a/tests/universe/test_universe_rules.py b/tests/universe/test_universe_rules.py new file mode 100644 index 0000000..110f79f --- /dev/null +++ b/tests/universe/test_universe_rules.py @@ -0,0 +1,93 @@ +import polars as pl + +from factorium.universe import ExcludeLeveragedTokens, ExcludeStablecoins, MinListingAge, Universe + + +NOW_MS = 1_700_000_000_000 +DAY_MS = 86_400_000 + + +def _sample_df() -> pl.DataFrame: + rows = [ + {"start_time": NOW_MS - 5 * DAY_MS, "end_time": NOW_MS - 5 * DAY_MS + 3_600_000, "symbol": "BTCUSDT"}, + {"start_time": NOW_MS - 5 * DAY_MS, "end_time": NOW_MS - 5 * DAY_MS + 3_600_000, "symbol": "USDCUSDT"}, + {"start_time": NOW_MS - 5 * DAY_MS, "end_time": NOW_MS - 5 * DAY_MS + 3_600_000, "symbol": "BTCUPUSDT"}, + {"start_time": NOW_MS - 5 * DAY_MS, "end_time": NOW_MS - 5 * DAY_MS + 3_600_000, "symbol": "NEWUSDT"}, + {"start_time": NOW_MS + 100 * DAY_MS, "end_time": NOW_MS + 100 * DAY_MS + 3_600_000, "symbol": "NEWUSDT"}, + ] + return pl.DataFrame(rows) + + +def _sample_metadata() -> dict[str, dict]: + return { + "BTCUSDT": { + "symbol": "BTCUSDT", + "base_asset": "BTC", + "quote_asset": "USDT", + "status": "TRADING", + "listing_date": NOW_MS - 365 * DAY_MS, + }, + "USDCUSDT": { + "symbol": "USDCUSDT", + "base_asset": "USDC", + "quote_asset": "USDT", + "status": "TRADING", + "is_stablecoin_pair": True, + "listing_date": NOW_MS - 365 * DAY_MS, + }, + "BTCUPUSDT": { + "symbol": "BTCUPUSDT", + "base_asset": "BTCUP", + "quote_asset": "USDT", + "status": "TRADING", + "is_leveraged": True, + "listing_date": NOW_MS - 365 * DAY_MS, + }, + "NEWUSDT": { + "symbol": "NEWUSDT", + "base_asset": "NEW", + "quote_asset": "USDT", + "status": "TRADING", + "listing_date": NOW_MS - 10 * DAY_MS, + }, + } + + +def test_exclude_stablecoins_filters_stable_base_assets() -> None: + df = _sample_df().lazy() + metadata = _sample_metadata() + expr = ExcludeStablecoins().apply(df, metadata) + out = _sample_df().lazy().with_columns(expr.alias("keep")).collect() + + stable_keep = out.filter(pl.col("symbol") == "USDCUSDT")["keep"].to_list() + assert stable_keep == [False] + + +def test_exclude_leveraged_tokens_filters_leveraged_symbols() -> None: + df = _sample_df().lazy() + metadata = _sample_metadata() + expr = ExcludeLeveragedTokens().apply(df, metadata) + out = _sample_df().lazy().with_columns(expr.alias("keep")).collect() + + leveraged_keep = out.filter(pl.col("symbol") == "BTCUPUSDT")["keep"].to_list() + assert leveraged_keep == [False] + + +def test_min_listing_age_is_time_varying() -> None: + metadata = _sample_metadata() + df = _sample_df().lazy() + expr = MinListingAge(days=90).apply(df, metadata) + out = _sample_df().lazy().with_columns(expr.alias("keep")).collect() + + new_rows = out.filter(pl.col("symbol") == "NEWUSDT").sort("start_time") + assert new_rows["keep"].to_list() == [False, True] + + +def test_universe_combines_rules_with_and_logic() -> None: + metadata = _sample_metadata() + rules = [ExcludeStablecoins(), ExcludeLeveragedTokens(), MinListingAge(days=90)] + universe = Universe(rules) + + out = _sample_df().lazy().with_columns(universe.apply(_sample_df().lazy(), metadata).alias("in_universe")).collect() + kept_symbols = set(out.filter(pl.col("in_universe"))["symbol"].to_list()) + assert kept_symbols == {"BTCUSDT", "NEWUSDT"} From 58591623ca09d8bbe2aeb99f3ab34ac293d333f7 Mon Sep 17 00:00:00 2001 From: novis10813 Date: Sat, 14 Feb 2026 00:13:40 +0800 Subject: [PATCH 08/25] docs(universe): add user guide and integrate mask workflow --- docs/index.md | 1 + .../plans/2026-02-13-universe-docs-pr-plan.md | 199 ++++++++++++++++++ docs/user-guide/backtest.md | 20 +- docs/user-guide/bar.md | 23 ++ docs/user-guide/factor.md | 17 +- docs/user-guide/universe.md | 102 +++++++++ mkdocs.yml | 1 + tests/docs/test_universe_user_docs.py | 52 +++++ 8 files changed, 413 insertions(+), 2 deletions(-) create mode 100644 docs/plans/2026-02-13-universe-docs-pr-plan.md create mode 100644 docs/user-guide/universe.md create mode 100644 tests/docs/test_universe_user_docs.py diff --git a/docs/index.md b/docs/index.md index 5f78859..c0d9cf3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -78,6 +78,7 @@ uv add factorium | [快速開始](getting-started/quickstart.md) | 五分鐘上手教學 | | [資料獲取](getting-started/data-acquisition.md) | 下載與載入市場數據 | | [Bar 聚合](user-guide/bar.md) | 不同類型的 K 線聚合 | +| [Universe 與 Checklist](user-guide/universe.md) | 建立資產池遮罩(Universe / Checklist)並串接因子與回測 | | [Factor 因子](user-guide/factor.md) | 因子計算與運算子 | | [因子分析](user-guide/analyzer.md) | IC / 分層收益等分析工具 | | [策略回測](user-guide/backtest.md) | 向量化回測與權重約束 | diff --git a/docs/plans/2026-02-13-universe-docs-pr-plan.md b/docs/plans/2026-02-13-universe-docs-pr-plan.md new file mode 100644 index 0000000..087ef80 --- /dev/null +++ b/docs/plans/2026-02-13-universe-docs-pr-plan.md @@ -0,0 +1,199 @@ +# Universe User Documentation Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** 補齊 universe/checklist 的官方使用文件,讓使用者能從文件直接完成「建立資產池 -> 套 mask -> 因子計算 -> 回測」流程。 + +**Architecture:** 以 `docs/user-guide/universe.md` 作為主入口,並在既有 `bar/factor/backtest/index` 做最小必要串接。先用文件驗收測試定義必要章節與關鍵 API,再逐步補內容,最後以 MkDocs build 驗證可發布品質。 + +**Tech Stack:** MkDocs + Material、Markdown、pytest(文件驗收測試) + +--- + +### Task 1: 建立文件驗收測試骨架 + +**Files:** +- Create: `tests/docs/test_universe_user_docs.py` +- Test: `tests/docs/test_universe_user_docs.py` + +**Step 1: Write the failing test** + +```python +from pathlib import Path + + +def test_universe_guide_file_exists(): + assert Path("docs/user-guide/universe.md").exists() + + +def test_universe_guide_has_required_sections(): + text = Path("docs/user-guide/universe.md").read_text(encoding="utf-8") + required = [ + "# Universe 與 Checklist", + "## 快速流程", + "## 與 AggBar 整合", + "## 與 Factor 整合", + "## 與 Backtest 整合", + "## 常見錯誤", + ] + for section in required: + assert section in text +``` + +**Step 2: Run test to verify it fails** + +Run: `uv run pytest tests/docs/test_universe_user_docs.py -v` +Expected: FAIL(`docs/user-guide/universe.md` 尚不存在) + +**Step 3: Write minimal implementation** + +建立 `docs/user-guide/universe.md`,先放最小標題骨架以滿足測試。 + +**Step 4: Run test to verify it passes** + +Run: `uv run pytest tests/docs/test_universe_user_docs.py -v` +Expected: PASS + +**Step 5: Commit** + +```bash +git add tests/docs/test_universe_user_docs.py docs/user-guide/universe.md +git commit -m "test(docs): add universe user-guide acceptance checks" +``` + +### Task 2: 完成 Universe 主文件內容 + +**Files:** +- Modify: `docs/user-guide/universe.md` +- Test: `tests/docs/test_universe_user_docs.py` + +**Step 1: Write the failing test** + +在 `tests/docs/test_universe_user_docs.py` 增加關鍵片段驗證: +- `AggBar.with_mask(` +- `Factor.eval(..., mask=` +- `Backtester(..., mask=`(以專案實際 API 命名為準) + +**Step 2: Run test to verify it fails** + +Run: `uv run pytest tests/docs/test_universe_user_docs.py -v` +Expected: FAIL(關鍵片段尚未補齊) + +**Step 3: Write minimal implementation** + +在 `docs/user-guide/universe.md` 補齊: +- 概念:universe 與 checklist 的差異 +- 快速流程:資料 -> 遮罩 -> 因子 -> 回測 +- 可直接執行的最小程式片段 +- 常見錯誤(index 對齊、look-ahead、NaN/mask 傳遞) + +**Step 4: Run test to verify it passes** + +Run: `uv run pytest tests/docs/test_universe_user_docs.py -v` +Expected: PASS + +**Step 5: Commit** + +```bash +git add docs/user-guide/universe.md tests/docs/test_universe_user_docs.py +git commit -m "docs(universe): add complete universe/checklist user guide" +``` + +### Task 3: 串接既有文件與導覽 + +**Files:** +- Modify: `mkdocs.yml` +- Modify: `docs/index.md` +- Modify: `docs/user-guide/bar.md` +- Modify: `docs/user-guide/factor.md` +- Modify: `docs/user-guide/backtest.md` +- Test: `tests/docs/test_universe_user_docs.py` + +**Step 1: Write the failing test** + +在 `tests/docs/test_universe_user_docs.py` 新增: +- `mkdocs.yml` 導覽含 `user-guide/universe.md` +- `docs/index.md` 含 universe/checklist 入口描述 +- `bar/factor/backtest` 各至少一處連到 universe 使用方式 + +**Step 2: Run test to verify it fails** + +Run: `uv run pytest tests/docs/test_universe_user_docs.py -v` +Expected: FAIL + +**Step 3: Write minimal implementation** + +更新 `mkdocs.yml` 導覽,並在上述文件加入最小必要交叉連結與示例段落。 + +**Step 4: Run test to verify it passes** + +Run: `uv run pytest tests/docs/test_universe_user_docs.py -v` +Expected: PASS + +**Step 5: Commit** + +```bash +git add mkdocs.yml docs/index.md docs/user-guide/bar.md docs/user-guide/factor.md docs/user-guide/backtest.md tests/docs/test_universe_user_docs.py +git commit -m "docs: wire universe guide into nav and related user guides" +``` + +### Task 4: 發布前驗證 + +**Files:** +- Modify: `docs/user-guide/universe.md`(若 build 警告需微調) + +**Step 1: Write the failing test** + +無新增程式測試;以文件建置作為驗收門檻。 + +**Step 2: Run test to verify it fails** + +Run: `uv run mkdocs build` +Expected: 若有 broken links 或 nav 問題則 FAIL + +**Step 3: Write minimal implementation** + +修正連結、標題層級、程式片段語法標記,直到 build 無錯。 + +**Step 4: Run test to verify it passes** + +Run: `uv run mkdocs build` +Expected: PASS + +**Step 5: Commit** + +```bash +git add docs/ mkdocs.yml +git commit -m "chore(docs): pass mkdocs build for universe documentation" +``` + +### Task 5: PR 自我檢查 + +**Files:** +- Modify: `docs/plans/2026-02-13-universe-docs-pr-plan.md`(若需補充實際執行備註) + +**Step 1: Write the failing test** + +建立自我檢查清單(人工): +- [ ] 新頁面在導覽可見 +- [ ] 主流程程式片段可讀且命名一致 +- [ ] 交叉連結完整 + +**Step 2: Run test to verify it fails** + +人工審閱,任何一項未滿足即視為 FAIL。 + +**Step 3: Write minimal implementation** + +補齊缺漏內容。 + +**Step 4: Run test to verify it passes** + +再次人工審閱 + `uv run mkdocs build`。 + +**Step 5: Commit** + +```bash +git add docs/ +git commit -m "docs: finalize universe documentation PR checklist" +``` diff --git a/docs/user-guide/backtest.md b/docs/user-guide/backtest.md index 6f83fbc..2e82c2e 100644 --- a/docs/user-guide/backtest.md +++ b/docs/user-guide/backtest.md @@ -9,6 +9,7 @@ - **價格資料 (`prices`)**:使用 `AggBar` 表示的多標的 OHLCV 資料,欄位至少包含 `["start_time", "end_time", "symbol", "open", "high", "low", "close", "volume"]`。 - **信號因子 (`signal`)**:任何 `Factor` 物件,需與 `prices` 在 `end_time, symbol` 上對齊,通常為已做過橫截面處理的排名 / Z-score。 +- **資產池遮罩 (`mask`)**:可選欄位名稱,用來限制哪些標的在該時間點可以持倉。 - **避免前視偏差**:回測時會使用「前一根 bar 的信號」在當前 bar 交易。 - **向量化實作**:內部全部以 Polars 向量化計算完成,再轉成 pandas 計算績效指標。 @@ -83,6 +84,24 @@ print(pandas_result.metrics) --- +## 3.1 使用 Universe / Checklist mask 限制持倉 + +若你已經在 `AggBar` 內建立 mask 欄位(例如 `checklist_mask`),可以在回測時直接指定: + +```python +bt = Backtester( + prices=agg, + signal=momentum, + mask="checklist_mask", + neutralization="market", +) +result = bt.run() +``` + +當 `mask` 為 `False` 或 `null`,該標的在該期權重會被設為 0。這能讓回測與因子分析維持同一套 Universe/Checklist 約束。 + +--- + ## 4. 市場中性與 Long-only 權重 ### 4.1 `neutralization="market"`(市場中性) @@ -175,4 +194,3 @@ print(result.metrics) - 直接使用 `Backtester(prices=agg, signal=signal)`;或 - 透過 `ResearchSession.backtest(signal)`。 5. **查看結果**:讀取 `BacktestResult.metrics`、`equity_curve`、`trades` 等欄位,或將結果轉成 pandas 作進一步分析。 - diff --git a/docs/user-guide/bar.md b/docs/user-guide/bar.md index 2ffbdb3..da91a08 100644 --- a/docs/user-guide/bar.md +++ b/docs/user-guide/bar.md @@ -318,3 +318,26 @@ Bar 聚合使用 DuckDB 的 SQL 引擎,效能特點: 2. **最後一個 Bar**:Volume/Dollar Bar 的最後一個 bar 可能不完整(未達閾值) 3. **空資料處理**:若指定日期無資料,會回傳空的 DataFrame 和零值 metadata 4. **多標的聚合**:每個標的獨立聚合,最後合併 + +--- + +## 與 Universe / Checklist 整合 + +`AggBar` 可透過 `with_mask` 直接掛入資產池遮罩欄位,後續因子與回測可共用同一個欄位名稱: + +```python +from factorium import Universe, ExcludeStablecoins + +universe = Universe(rules=[ExcludeStablecoins()]) +metadata = {} +tags = {} + +agg = agg.with_mask( + name="universe_mask", + mask_source=universe, + metadata=metadata, + tags=tags, +) +``` + +若你要進一步做因子與回測,請參考 [Universe 與 Checklist](universe.md)。 diff --git a/docs/user-guide/factor.md b/docs/user-guide/factor.md index eb48105..d6c95fd 100644 --- a/docs/user-guide/factor.md +++ b/docs/user-guide/factor.md @@ -262,10 +262,26 @@ result = factor.eval( quantiles: int = 5, # 分層數量(per-day cross-sectional quantiles) output_dir: str | None = None, # 若提供路徑,會輸出評估結果到時間戳目錄 price_col: str = "close", # 價格欄位名稱(當 prices 為 AggBar 時使用) + mask: str | None = None, # 選填:限制分析樣本的遮罩欄位名稱 **kwargs, ) -> FactorAnalysisResult ``` +### 與 Universe / Checklist 遮罩整合 + +若你在 `AggBar` 上已建立 Universe/Checklist 遮罩欄位,可以直接傳入 `mask=`: + +```python +result = momentum_20.eval( + prices=agg, + periods=1, + quantiles=5, + mask="checklist_mask", +) +``` + +這可確保分位分組與後續評估都只在可交易資產池上進行,避免把不在策略範圍內的標的納入統計。 + `result` 回傳一個 `FactorAnalysisResult` dataclass,主要包含: - **`factor_name`**: 因子名稱 @@ -358,4 +374,3 @@ if result.ic_summary['ic_ir'] > 1.0: 搭配 `BinanceDataLoader` + `Bar` + `AggBar`,可以很方便地從原始交易資料一路建構到完整的因子研究流程。 - diff --git a/docs/user-guide/universe.md b/docs/user-guide/universe.md new file mode 100644 index 0000000..63d3c77 --- /dev/null +++ b/docs/user-guide/universe.md @@ -0,0 +1,102 @@ +# Universe 與 Checklist + +Universe / Checklist 用來建立資產池遮罩(mask),把因子與回測限制在符合條件的標的集合。 + +- `Universe`:偏向交易規則與市場結構條件(例如排除穩定幣、上市天數)。 +- `Checklist`:偏向研究條件與流動性門檻(例如成交量、流動性、標籤條件)。 + +## 快速流程 + +以下流程示範「建立資產池 -> 套 mask -> 因子計算 -> 回測」。 + +```python +from factorium import ( + Backtester, + Checklist, + Factor, + MinLiquidity, + MinVolume, + Universe, + ExcludeStablecoins, + load_aggbar, +) + +# 1) 載入資料 +bar = load_aggbar("BTCUSDT", start="2023-01-01", end="2024-01-01", timeframe="1d") + +# 2) 建立 Universe / Checklist +universe = Universe(rules=[ExcludeStablecoins()]) +checklist = Checklist(filters=[MinVolume(window=20, threshold=1_000_000), MinLiquidity(window=20, threshold=100_000)]) + +# metadata/tags 可由 MetadataProvider / TagProvider 產生 +metadata = {} +tags = {} + +# 3) 產生 mask 欄位 +bar = bar.with_mask(name="universe_mask", mask_source=universe, metadata=metadata, tags=tags) +bar = bar.with_mask(name="checklist_mask", mask_source=checklist, metadata=metadata, tags=tags) + +# 4) 因子計算使用同一個 mask +factor = Factor("(close / shift(close, 5)) - 1") +result = factor.eval(bar, periods=5, quantiles=5, mask="checklist_mask") + +# 5) 回測使用同一個 mask +bt = Backtester(bar.data, signal=result.data, mask="checklist_mask") +stats = bt.run() +``` + +## 與 AggBar 整合 + +`AggBar.with_mask()` 會把 `Universe` 或 `Checklist` 的條件計算成布林欄位,直接寫回 `AggBar.data`。 + +```python +bar = bar.with_mask( + name="universe_mask", + mask_source=universe, + metadata=metadata, + tags=tags, +) +``` + +注意: + +- `name` 不能覆蓋保留欄位(如 `open`, `close`, `volume`, `symbol`)。 +- `metadata` 需包含與 `symbol` 對應的資訊;若規則依賴 `tags`,必須提供 `tags`。 + +## 與 Factor 整合 + +`Factor.eval` 可透過 `mask` 參數限制有效樣本: + +```python +result = factor.eval( + bar, + periods=5, + quantiles=5, + mask="checklist_mask", +) +``` + +這代表因子分位分組與後續分析只在 `checklist_mask == True` 的資料上進行。 + +## 與 Backtest 整合 + +`Backtester(...)` 支援 `mask=`,用來限制可持倉資產: + +```python +bt = Backtester( + prices=bar.data, + signal=result.data, + holding_period=5, + mask="checklist_mask", +) +stats = bt.run() +``` + +當 mask 為 `False` 或 `null` 時,回測會把該資產權重歸零。 + +## 常見錯誤 + +- `mask` 欄位名稱拼錯,導致 `Factor.eval(..., mask=...)` 或 `Backtester(..., mask=...)` 找不到欄位。 +- `metadata` / `tags` 與 `symbol` 不一致,造成遮罩結果異常。 +- 先用完整資料算 signal,再事後套 mask,可能引入 look-ahead;請在計算流程中一致傳入同一個 mask。 +- 忽略空值處理,讓 `null` 直接進入決策。建議把 mask 欄位維持明確布林語義(`True` 可交易,其他視為不可交易)。 diff --git a/mkdocs.yml b/mkdocs.yml index 9003bf0..c7c5b6e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -54,6 +54,7 @@ nav: - 五分鐘教學: getting-started/quickstart.md - 使用指南: - Bar 聚合: user-guide/bar.md + - Universe 與 Checklist: user-guide/universe.md - Factor 因子: user-guide/factor.md - 表達式語法: user-guide/parser.md - 因子分析: user-guide/analyzer.md diff --git a/tests/docs/test_universe_user_docs.py b/tests/docs/test_universe_user_docs.py new file mode 100644 index 0000000..58b215c --- /dev/null +++ b/tests/docs/test_universe_user_docs.py @@ -0,0 +1,52 @@ +from pathlib import Path + + +def test_universe_guide_file_exists() -> None: + assert Path("docs/user-guide/universe.md").exists() + + +def test_universe_guide_has_required_sections() -> None: + text = Path("docs/user-guide/universe.md").read_text(encoding="utf-8") + required = [ + "# Universe 與 Checklist", + "## 快速流程", + "## 與 AggBar 整合", + "## 與 Factor 整合", + "## 與 Backtest 整合", + "## 常見錯誤", + ] + for section in required: + assert section in text + + +def test_universe_guide_has_core_api_snippets() -> None: + text = Path("docs/user-guide/universe.md").read_text(encoding="utf-8") + required_snippets = [ + "AggBar.with_mask(", + "Factor.eval", + "mask=", + "Backtester(", + ] + for snippet in required_snippets: + assert snippet in text + + +def test_mkdocs_nav_includes_universe_page() -> None: + text = Path("mkdocs.yml").read_text(encoding="utf-8") + assert "user-guide/universe.md" in text + + +def test_index_mentions_universe_capability() -> None: + text = Path("docs/index.md").read_text(encoding="utf-8") + assert "universe" in text.lower() + assert "checklist" in text.lower() + + +def test_related_guides_reference_universe_workflow() -> None: + bar_text = Path("docs/user-guide/bar.md").read_text(encoding="utf-8") + factor_text = Path("docs/user-guide/factor.md").read_text(encoding="utf-8") + backtest_text = Path("docs/user-guide/backtest.md").read_text(encoding="utf-8") + + assert "with_mask" in bar_text + assert "mask" in factor_text.lower() + assert "mask" in backtest_text.lower() From d5faaaef1b1e48c90471ce6112d1ff8ee1cab3f7 Mon Sep 17 00:00:00 2001 From: novis10813 Date: Sat, 14 Feb 2026 08:38:47 +0800 Subject: [PATCH 09/25] docs(examples): add universe checklist workflow notebook --- .../2026-02-13-universe-examples-pr-plan.md | 197 ++++++++++++++++++ examples/05_universe_checklist_workflow.ipynb | 156 ++++++++++++++ examples/README.md | 2 + tests/examples/test_universe_notebook_docs.py | 49 +++++ 4 files changed, 404 insertions(+) create mode 100644 docs/plans/2026-02-13-universe-examples-pr-plan.md create mode 100644 examples/05_universe_checklist_workflow.ipynb create mode 100644 tests/examples/test_universe_notebook_docs.py diff --git a/docs/plans/2026-02-13-universe-examples-pr-plan.md b/docs/plans/2026-02-13-universe-examples-pr-plan.md new file mode 100644 index 0000000..7e4566d --- /dev/null +++ b/docs/plans/2026-02-13-universe-examples-pr-plan.md @@ -0,0 +1,197 @@ +# Universe Example Notebook Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** 在 `examples/` 新增 universe/checklist 實戰 notebook,並更新 examples 導覽,讓使用者能照著範例重現遮罩化研究流程。 + +**Architecture:** 以新 notebook `examples/05_universe_checklist_workflow.ipynb` 作為單一教學入口,README 只保留高層導引。先建立結構驗收測試(檔案存在、README 收錄、必要章節標題),再逐步填入可執行 cell,最後用 nbconvert 執行驗證。 + +**Tech Stack:** Jupyter Notebook、Python、pytest、nbformat/nbconvert + +--- + +### Task 1: 建立範例驗收測試骨架 + +**Files:** +- Create: `tests/examples/test_universe_notebook_docs.py` +- Test: `tests/examples/test_universe_notebook_docs.py` + +**Step 1: Write the failing test** + +```python +from pathlib import Path + + +def test_universe_notebook_exists(): + assert Path("examples/05_universe_checklist_workflow.ipynb").exists() + + +def test_examples_readme_mentions_universe_notebook(): + text = Path("examples/README.md").read_text(encoding="utf-8") + assert "05_universe_checklist_workflow.ipynb" in text +``` + +**Step 2: Run test to verify it fails** + +Run: `uv run pytest tests/examples/test_universe_notebook_docs.py -v` +Expected: FAIL(新 notebook 尚未建立,README 尚未更新) + +**Step 3: Write minimal implementation** + +建立空白 notebook 檔與 README 最小條目。 + +**Step 4: Run test to verify it passes** + +Run: `uv run pytest tests/examples/test_universe_notebook_docs.py -v` +Expected: PASS + +**Step 5: Commit** + +```bash +git add tests/examples/test_universe_notebook_docs.py examples/05_universe_checklist_workflow.ipynb examples/README.md +git commit -m "test(examples): add acceptance checks for universe notebook" +``` + +### Task 2: 定義 notebook 結構與教學章節 + +**Files:** +- Modify: `examples/05_universe_checklist_workflow.ipynb` +- Modify: `tests/examples/test_universe_notebook_docs.py` + +**Step 1: Write the failing test** + +在測試中用 `nbformat` 檢查 notebook markdown 標題至少包含: +- `# Universe Checklist Workflow` +- `## Build Universe and Checklist` +- `## Apply Mask to AggBar` +- `## Evaluate Factor with Mask` +- `## Run Backtest with Mask` + +**Step 2: Run test to verify it fails** + +Run: `uv run pytest tests/examples/test_universe_notebook_docs.py -v` +Expected: FAIL + +**Step 3: Write minimal implementation** + +在 notebook 加入對應章節 markdown cell(先不放完整程式碼)。 + +**Step 4: Run test to verify it passes** + +Run: `uv run pytest tests/examples/test_universe_notebook_docs.py -v` +Expected: PASS + +**Step 5: Commit** + +```bash +git add examples/05_universe_checklist_workflow.ipynb tests/examples/test_universe_notebook_docs.py +git commit -m "docs(examples): scaffold universe workflow notebook sections" +``` + +### Task 3: 填入可執行程式流程(最小可重現) + +**Files:** +- Modify: `examples/05_universe_checklist_workflow.ipynb` +- Modify: `tests/examples/test_universe_notebook_docs.py` + +**Step 1: Write the failing test** + +測試新增關鍵 API 片段檢查(以 cell 內容字串比對): +- `with_mask(` +- `mask=` +- `universe` 或 `checklist` 建立呼叫 + +**Step 2: Run test to verify it fails** + +Run: `uv run pytest tests/examples/test_universe_notebook_docs.py -v` +Expected: FAIL + +**Step 3: Write minimal implementation** + +補齊 notebook code cell: +- 載入資料與必要欄位 +- 建立 universe/checklist +- 產生並套用 mask 到 `AggBar` +- 執行 `Factor.eval(..., mask=...)` +- 執行回測(含 mask 版本) +- 增加與未套 mask 的最小比較表格 + +**Step 4: Run test to verify it passes** + +Run: `uv run pytest tests/examples/test_universe_notebook_docs.py -v` +Expected: PASS + +**Step 5: Commit** + +```bash +git add examples/05_universe_checklist_workflow.ipynb tests/examples/test_universe_notebook_docs.py +git commit -m "feat(examples): add executable universe checklist workflow notebook" +``` + +### Task 4: 更新 examples 導覽與執行驗證 + +**Files:** +- Modify: `examples/README.md` +- Modify: `examples/05_universe_checklist_workflow.ipynb`(若執行驗證需修正) + +**Step 1: Write the failing test** + +在 `tests/examples/test_universe_notebook_docs.py` 增加 README 驗收: +- notebook 目的說明 +- 前置條件 +- 建議執行順序(01 -> ... -> 05) + +**Step 2: Run test to verify it fails** + +Run: `uv run pytest tests/examples/test_universe_notebook_docs.py -v` +Expected: FAIL + +**Step 3: Write minimal implementation** + +更新 `examples/README.md`,補齊第 5 本 notebook 的說明與順序。 + +**Step 4: Run test to verify it passes** + +Run: +- `uv run pytest tests/examples/test_universe_notebook_docs.py -v` +- `uv run python -m jupyter nbconvert --to notebook --execute examples/05_universe_checklist_workflow.ipynb --output /tmp/05_universe_checklist_workflow.executed.ipynb` + +Expected: PASS(測試通過,notebook 可從頭執行) + +**Step 5: Commit** + +```bash +git add examples/README.md examples/05_universe_checklist_workflow.ipynb tests/examples/test_universe_notebook_docs.py +git commit -m "docs(examples): document universe notebook and verify execution" +``` + +### Task 5: PR 自我檢查 + +**Files:** +- Modify: `docs/plans/2026-02-13-universe-examples-pr-plan.md`(若需補充執行備註) + +**Step 1: Write the failing test** + +建立人工清單: +- [ ] Notebook 每節都有文字解說與輸出解讀 +- [ ] 程式碼無 look-ahead 寫法 +- [ ] README 與 notebook API 名稱一致 + +**Step 2: Run test to verify it fails** + +人工審閱,任一項不滿足即 FAIL。 + +**Step 3: Write minimal implementation** + +修正文案、變數命名與示例片段。 + +**Step 4: Run test to verify it passes** + +再次人工審閱 + 重新執行測試與 nbconvert。 + +**Step 5: Commit** + +```bash +git add examples/ tests/examples/ +git commit -m "chore(examples): finalize universe notebook PR checklist" +``` diff --git a/examples/05_universe_checklist_workflow.ipynb b/examples/05_universe_checklist_workflow.ipynb new file mode 100644 index 0000000..62896fb --- /dev/null +++ b/examples/05_universe_checklist_workflow.ipynb @@ -0,0 +1,156 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Universe Checklist Workflow\n", + "\n", + "This notebook shows how to apply universe/checklist masks consistently in factor analysis and backtesting." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Build Universe and Checklist\n", + "\n", + "We first create a small synthetic multi-symbol dataset, then define metadata/tags and mask rules." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import polars as pl\n", + "\n", + "from factorium import AggBar, Checklist, Universe\n", + "from factorium.backtest import Backtester\n", + "from factorium.universe import ExcludeStablecoins, MinVolume, TagFilter\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "DAY_MS = 86_400_000\n", + "BASE_TS = 1_700_000_000_000\n", + "\n", + "rows = []\n", + "for i in range(30):\n", + " ts = BASE_TS + i * DAY_MS\n", + " rows.extend(\n", + " [\n", + " {\"start_time\": ts, \"end_time\": ts + 3_600_000, \"symbol\": \"BTCUSDT\", \"open\": 100 + i, \"high\": 101 + i, \"low\": 99 + i, \"close\": 100 + i, \"volume\": 20_000 + i, \"alpha\": float(i + 1)},\n", + " {\"start_time\": ts, \"end_time\": ts + 3_600_000, \"symbol\": \"USDCUSDT\", \"open\": 1.0, \"high\": 1.0, \"low\": 1.0, \"close\": 1.0, \"volume\": 50_000, \"alpha\": float(50 - i)},\n", + " {\"start_time\": ts, \"end_time\": ts + 3_600_000, \"symbol\": \"NEWUSDT\", \"open\": 10 + i, \"high\": 11 + i, \"low\": 9 + i, \"close\": 10 + i, \"volume\": 500 + i, \"alpha\": float(100 + i)},\n", + " ]\n", + " )\n", + "\n", + "agg = AggBar(pl.DataFrame(rows))\n", + "\n", + "metadata = {\n", + " \"BTCUSDT\": {\"symbol\": \"BTCUSDT\", \"base_asset\": \"BTC\", \"quote_asset\": \"USDT\", \"status\": \"TRADING\", \"listing_date\": BASE_TS - 365 * DAY_MS},\n", + " \"USDCUSDT\": {\"symbol\": \"USDCUSDT\", \"base_asset\": \"USDC\", \"quote_asset\": \"USDT\", \"status\": \"TRADING\", \"listing_date\": BASE_TS - 365 * DAY_MS, \"is_stablecoin_pair\": True},\n", + " \"NEWUSDT\": {\"symbol\": \"NEWUSDT\", \"base_asset\": \"NEW\", \"quote_asset\": \"USDT\", \"status\": \"TRADING\", \"listing_date\": BASE_TS - 30 * DAY_MS},\n", + "}\n", + "tags = {\"BTC\": [\"layer1\"], \"USDC\": [\"stablecoin\"], \"NEW\": [\"meme\"]}\n", + "\n", + "universe = Universe([ExcludeStablecoins()])\n", + "checklist = Checklist([TagFilter(include=[\"layer1\", \"meme\"]), MinVolume(window=5, threshold=1_000)])\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Apply Mask to AggBar\n", + "\n", + "Use `AggBar.with_mask(...)` to add boolean columns that can be reused downstream." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "agg = agg.with_mask(name=\"in_universe\", mask_source=universe, metadata=metadata, tags=tags)\n", + "agg = agg.with_mask(name=\"in_checklist\", mask_source=checklist, metadata=metadata, tags=tags)\n", + "\n", + "agg.data[[\"symbol\", \"in_universe\", \"in_checklist\"]].head(9)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Evaluate Factor with Mask\n", + "\n", + "Use the same mask in `Factor.eval(..., mask=...)` so ranking/evaluation happens only inside your tradable universe." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "factor = agg[\"alpha\"]\n", + "masked_result = factor.eval(prices=agg, periods=1, quantiles=2, mask=\"in_checklist\")\n", + "unmasked_result = factor.eval(prices=agg, periods=1, quantiles=2)\n", + "\n", + "print(\"factor:\", masked_result.factor_name)\n", + "masked_rows = sum(len(df) for df in masked_result.quantile_returns.values())\n", + "unmasked_rows = sum(len(df) for df in unmasked_result.quantile_returns.values())\n", + "print(\"masked quantile rows:\", masked_rows)\n", + "print(\"unmasked quantile rows:\", unmasked_rows)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Run Backtest with Mask\n", + "\n", + "Finally, pass `mask=` into `Backtester(...)` to keep positions inside the same universe constraints." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " bt = Backtester(\n", + " prices=agg,\n", + " signal=factor,\n", + " holding_period=3,\n", + " neutralization=\"market\",\n", + " mask=\"in_universe\",\n", + " )\n", + " bt_result = bt.run()\n", + " bt_result.metrics\n", + "except Exception as exc:\n", + " print(\"Backtest run needs enough cross-sectional signals in each bar:\", exc)\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/README.md b/examples/README.md index 5cd600b..768ab56 100644 --- a/examples/README.md +++ b/examples/README.md @@ -10,6 +10,7 @@ Interactive Jupyter notebooks demonstrating factor research workflows with Facto | [02 — Mean Reversion Factor](02_mean_reversion_factor.ipynb) | Mean reversion with cross-sectional processing | Z-score distance, volatility normalization, `cs_rank`, `cs_zscore`, `cs_winsorize`, market-neutral vs. long-only backtest, advanced operators (`ts_autocorr`, `ts_kurtosis`, `ts_skewness`) | | [03 — Data Loading & Exploration](03_data_loading_and_exploration.ipynb) | Deep dive into data handling | `BinanceDataLoader`, `AggBar` methods, time-bar intervals (1min/5min/1h), slicing, CSV/Parquet export, `ResearchSession` from files | | [04 — Multi-Factor Combination](04_multi_factor_combination.ipynb) | Combine and select factors | Factor correlations, `ts_corr`, `cs_neutralize`, `CompositeFactor` (equal/custom/z-score), single vs. composite backtest, factor selection workflow | +| [05 — Universe & Checklist Workflow](05_universe_checklist_workflow.ipynb) | Constrain research and backtest to a tradable asset universe | `Universe`, `Checklist`, `AggBar.with_mask`, `Factor.eval(..., mask=...)`, `Backtester(..., mask=...)` | ## Getting Started @@ -44,3 +45,4 @@ If you're new to Factorium, we recommend starting with: 2. **Notebook 01** — Walk through a full factor research workflow 3. **Notebook 02** — Learn about signal processing and cross-sectional transforms 4. **Notebook 04** — Combine multiple factors into a composite signal +5. **Notebook 05** — Apply universe/checklist masks consistently in analysis and backtests diff --git a/tests/examples/test_universe_notebook_docs.py b/tests/examples/test_universe_notebook_docs.py new file mode 100644 index 0000000..477b8c6 --- /dev/null +++ b/tests/examples/test_universe_notebook_docs.py @@ -0,0 +1,49 @@ +from pathlib import Path +import json + + +def test_universe_notebook_exists() -> None: + assert Path("examples/05_universe_checklist_workflow.ipynb").exists() + + +def test_examples_readme_mentions_universe_notebook() -> None: + text = Path("examples/README.md").read_text(encoding="utf-8") + assert "05_universe_checklist_workflow.ipynb" in text + + +def test_universe_notebook_has_required_sections() -> None: + notebook = json.loads(Path("examples/05_universe_checklist_workflow.ipynb").read_text(encoding="utf-8")) + markdown_text = "\n".join( + "".join(cell.get("source", [])) for cell in notebook.get("cells", []) if cell.get("cell_type") == "markdown" + ) + + required = [ + "# Universe Checklist Workflow", + "## Build Universe and Checklist", + "## Apply Mask to AggBar", + "## Evaluate Factor with Mask", + "## Run Backtest with Mask", + ] + for section in required: + assert section in markdown_text + + +def test_universe_notebook_has_core_api_snippets() -> None: + text = Path("examples/05_universe_checklist_workflow.ipynb").read_text(encoding="utf-8") + required_snippets = [ + "with_mask(", + "mask=", + "Universe(", + "Checklist(", + "Backtester(", + ] + for snippet in required_snippets: + assert snippet in text + + +def test_examples_readme_has_universe_guidance() -> None: + text = Path("examples/README.md").read_text(encoding="utf-8") + assert "Universe & Checklist Workflow" in text + assert "Prerequisites" in text + assert "Recommended Reading Order" in text + assert "Notebook 05" in text From 45382048e91e6a0be7ad75b924e16567730d8251 Mon Sep 17 00:00:00 2001 From: novis10813 Date: Sat, 14 Feb 2026 13:31:57 +0800 Subject: [PATCH 10/25] test(data): migrate cache tests to storage backend API --- tests/data/test_cache.py | 45 +++++++++++++-------------------- tests/data/test_cache_polars.py | 27 +++++++++----------- 2 files changed, 29 insertions(+), 43 deletions(-) diff --git a/tests/data/test_cache.py b/tests/data/test_cache.py index 9aff2d9..a10302e 100644 --- a/tests/data/test_cache.py +++ b/tests/data/test_cache.py @@ -9,7 +9,6 @@ import pytest from factorium.data.cache import BarCache - from factorium.storage import LocalStorageBackend @@ -20,6 +19,13 @@ def temp_cache_dir(): yield Path(tmpdir) +@pytest.fixture +def cache(temp_cache_dir): + """Create cache using non-deprecated storage API.""" + storage = LocalStorageBackend(str(temp_cache_dir)) + return BarCache(storage=storage, cache_prefix="") + + @pytest.fixture def sample_bar_df(): """Create sample bar DataFrame in Polars format.""" @@ -41,16 +47,13 @@ def sample_bar_df(): class TestBarCache: """Tests for BarCache.""" - def test_cache_initialization(self, temp_cache_dir): + def test_cache_initialization(self, temp_cache_dir, cache): """Test cache initializes correctly.""" - cache = BarCache(storage=LocalStorageBackend(str(temp_cache_dir))) - assert isinstance(cache.storage, LocalStorageBackend) + assert cache.cache_dir is None assert temp_cache_dir.exists() - def test_cache_miss_returns_none(self, temp_cache_dir): + def test_cache_miss_returns_none(self, cache): """Test that cache miss returns None.""" - cache = BarCache(storage=LocalStorageBackend(str(temp_cache_dir))) - result = cache.get( exchange="binance", symbols=["BTCUSDT"], @@ -62,10 +65,8 @@ def test_cache_miss_returns_none(self, temp_cache_dir): assert result is None - def test_cache_put_and_get(self, temp_cache_dir, sample_bar_df): + def test_cache_put_and_get(self, cache, sample_bar_df): """Test putting and getting from cache.""" - cache = BarCache(storage=LocalStorageBackend(str(temp_cache_dir))) - cache.put( df=sample_bar_df, exchange="binance", @@ -88,10 +89,8 @@ def test_cache_put_and_get(self, temp_cache_dir, sample_bar_df): assert result is not None assert len(result) == len(sample_bar_df) - def test_cache_key_different_symbols(self, temp_cache_dir, sample_bar_df): + def test_cache_key_different_symbols(self, cache, sample_bar_df): """Test that different symbols produce different cache keys.""" - cache = BarCache(storage=LocalStorageBackend(str(temp_cache_dir))) - cache.put( df=sample_bar_df, exchange="binance", @@ -113,10 +112,8 @@ def test_cache_key_different_symbols(self, temp_cache_dir, sample_bar_df): assert result is None - def test_cache_key_different_interval(self, temp_cache_dir, sample_bar_df): + def test_cache_key_different_interval(self, cache, sample_bar_df): """Test that different intervals produce different cache keys.""" - cache = BarCache(storage=LocalStorageBackend(str(temp_cache_dir))) - cache.put( df=sample_bar_df, exchange="binance", @@ -138,10 +135,8 @@ def test_cache_key_different_interval(self, temp_cache_dir, sample_bar_df): assert result is None - def test_cache_daily_files(self, temp_cache_dir, sample_bar_df): + def test_cache_daily_files(self, temp_cache_dir, cache, sample_bar_df): """Test that cache creates daily files.""" - cache = BarCache(storage=LocalStorageBackend(str(temp_cache_dir))) - cache.put( df=sample_bar_df, exchange="binance", @@ -156,10 +151,8 @@ def test_cache_daily_files(self, temp_cache_dir, sample_bar_df): assert len(cache_files) == 1 assert "2024-01-15" in cache_files[0].name - def test_get_date_range(self, temp_cache_dir, sample_bar_df): + def test_get_date_range(self, cache, sample_bar_df): """Test getting data for a date range.""" - cache = BarCache(storage=LocalStorageBackend(str(temp_cache_dir))) - for day in range(1, 4): cache.put( df=sample_bar_df, @@ -184,10 +177,8 @@ def test_get_date_range(self, temp_cache_dir, sample_bar_df): assert result is not None assert len(result) == len(sample_bar_df) * 3 - def test_get_date_range_partial_miss(self, temp_cache_dir, sample_bar_df): + def test_get_date_range_partial_miss(self, cache, sample_bar_df): """Test that partial cache miss returns None for range.""" - cache = BarCache(storage=LocalStorageBackend(str(temp_cache_dir))) - for day in [1, 3]: cache.put( df=sample_bar_df, @@ -211,10 +202,8 @@ def test_get_date_range_partial_miss(self, temp_cache_dir, sample_bar_df): assert result is None - def test_clear_cache(self, temp_cache_dir, sample_bar_df): + def test_clear_cache(self, cache, sample_bar_df): """Test clearing the cache.""" - cache = BarCache(storage=LocalStorageBackend(str(temp_cache_dir))) - cache.put( df=sample_bar_df, exchange="binance", diff --git a/tests/data/test_cache_polars.py b/tests/data/test_cache_polars.py index 28b17fc..0706349 100644 --- a/tests/data/test_cache_polars.py +++ b/tests/data/test_cache_polars.py @@ -19,6 +19,13 @@ def temp_cache_dir(): yield Path(tmpdir) +@pytest.fixture +def cache(temp_cache_dir): + """Create cache using non-deprecated storage API.""" + storage = LocalStorageBackend(str(temp_cache_dir)) + return BarCache(storage=storage, cache_prefix="") + + @pytest.fixture def sample_bar_df_polars(): """Create sample bar DataFrame in Polars format.""" @@ -40,10 +47,8 @@ def sample_bar_df_polars(): class TestBarCachePolars: """Tests for BarCache with Polars DataFrames.""" - def test_put_and_get_polars(self, temp_cache_dir, sample_bar_df_polars): + def test_put_and_get_polars(self, cache, sample_bar_df_polars): """Test storing and retrieving Polars DataFrame from cache.""" - cache = BarCache(storage=LocalStorageBackend(str(temp_cache_dir))) - cache.put( df=sample_bar_df_polars, exchange="binance", @@ -68,10 +73,8 @@ def test_put_and_get_polars(self, temp_cache_dir, sample_bar_df_polars): assert len(result) == len(sample_bar_df_polars) assert result.shape == sample_bar_df_polars.shape - def test_get_returns_none_when_not_cached(self, temp_cache_dir): + def test_get_returns_none_when_not_cached(self, cache): """Test that get returns None if data not in cache.""" - cache = BarCache(storage=LocalStorageBackend(str(temp_cache_dir))) - result = cache.get( exchange="binance", symbols=["BTCUSDT"], @@ -83,10 +86,8 @@ def test_get_returns_none_when_not_cached(self, temp_cache_dir): assert result is None - def test_get_range_returns_polars(self, temp_cache_dir, sample_bar_df_polars): + def test_get_range_returns_polars(self, cache, sample_bar_df_polars): """Test get_range returns concatenated Polars DataFrame.""" - cache = BarCache(storage=LocalStorageBackend(str(temp_cache_dir))) - # Store data for 3 consecutive days for day in range(1, 4): cache.put( @@ -113,10 +114,8 @@ def test_get_range_returns_polars(self, temp_cache_dir, sample_bar_df_polars): assert isinstance(result, pl.DataFrame) assert len(result) == len(sample_bar_df_polars) * 3 - def test_get_range_returns_none_if_any_missing(self, temp_cache_dir, sample_bar_df_polars): + def test_get_range_returns_none_if_any_missing(self, cache, sample_bar_df_polars): """Test get_range returns None if any day missing from range.""" - cache = BarCache(storage=LocalStorageBackend(str(temp_cache_dir))) - # Store data for days 1 and 3, but skip day 2 for day in [1, 3]: cache.put( @@ -142,10 +141,8 @@ def test_get_range_returns_none_if_any_missing(self, temp_cache_dir, sample_bar_ # Should return None because day 2 is missing assert result is None - def test_put_and_get_preserves_data_types(self, temp_cache_dir): + def test_put_and_get_preserves_data_types(self, cache): """Test that data types are preserved through cache round-trip.""" - cache = BarCache(storage=LocalStorageBackend(str(temp_cache_dir))) - df = pl.DataFrame( { "symbol": ["BTCUSDT", "ETHUSDT"], From 22fe3d2130b83f2ac6108e9785719f4b8f85ab7e Mon Sep 17 00:00:00 2001 From: novis10813 Date: Sat, 14 Feb 2026 15:19:43 +0800 Subject: [PATCH 11/25] fix(data): align date range to UTC midnight --- src/factorium/data/utils.py | 4 ++-- tests/data/test_timestamp_utils.py | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/factorium/data/utils.py b/src/factorium/data/utils.py index 2bd70a3..5feb482 100644 --- a/src/factorium/data/utils.py +++ b/src/factorium/data/utils.py @@ -3,7 +3,7 @@ """ import logging -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone logger = logging.getLogger(__name__) @@ -52,7 +52,7 @@ def calculate_date_range( return start, start + timedelta(days=days) # Snap to UTC midnight for consistent daily boundaries - today_midnight = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) + today_midnight = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0) # end = start of tomorrow (exclusive) to include today's full data end = today_midnight + timedelta(days=1) diff --git a/tests/data/test_timestamp_utils.py b/tests/data/test_timestamp_utils.py index 6187d76..362cdfc 100644 --- a/tests/data/test_timestamp_utils.py +++ b/tests/data/test_timestamp_utils.py @@ -1,12 +1,16 @@ # tests/data/test_timestamp_utils.py +from datetime import datetime, timedelta, timezone + import polars as pl import pytest +from factorium.data import utils as data_utils from factorium.data.loader import ( _convert_to_target_unit, _detect_timestamp_unit, _normalize_timestamps_to_ms, ) +from factorium.data.utils import calculate_date_range def test_detect_timestamp_unit_seconds(): @@ -39,6 +43,23 @@ def test_convert_to_target_unit_invalid_unit(): _convert_to_target_unit(1704067200000, "invalid") +def test_calculate_date_range_uses_utc_midnight(monkeypatch): + class FakeDateTime(datetime): + @classmethod + def now(cls, tz=None): + if tz is None: + return cls(2026, 2, 14, 23, 30, tzinfo=timezone(timedelta(hours=8))) + return cls(2026, 2, 14, 15, 30, tzinfo=timezone.utc).astimezone(tz) + + monkeypatch.setattr(data_utils, "datetime", FakeDateTime) + + start, end = calculate_date_range(days=1) + + expected_today_midnight = datetime(2026, 2, 14, 0, 0, tzinfo=timezone.utc) + assert start == expected_today_midnight + assert end == expected_today_midnight + timedelta(days=1) + + class TestNormalizeTimestampsToMs: """Tests for _normalize_timestamps_to_ms function.""" From a02281e5db30e292dedd1168c482913432405ce0 Mon Sep 17 00:00:00 2001 From: novis10813 Date: Sat, 14 Feb 2026 15:19:43 +0800 Subject: [PATCH 12/25] fix(universe): use run_async wrappers and warn on full tag fetch --- src/factorium/universe/metadata.py | 5 +-- src/factorium/universe/tags.py | 16 ++++++++-- tests/universe/test_metadata.py | 33 ++++++++++++++++++++ tests/universe/test_tags.py | 50 ++++++++++++++++++++++++++++++ 4 files changed, 100 insertions(+), 4 deletions(-) diff --git a/src/factorium/universe/metadata.py b/src/factorium/universe/metadata.py index 09ca6cd..01e6b6a 100644 --- a/src/factorium/universe/metadata.py +++ b/src/factorium/universe/metadata.py @@ -1,6 +1,5 @@ from __future__ import annotations -import asyncio import json import time from pathlib import Path @@ -47,7 +46,9 @@ async def fetch_async(self) -> dict[str, SymbolMetadata]: return parsed def fetch(self) -> dict[str, SymbolMetadata]: - return asyncio.run(self.fetch_async()) + from ..data.loader import _run_async + + return _run_async(self.fetch_async()) def _parse_exchange_info(self, data: dict) -> dict[str, SymbolMetadata]: output: dict[str, SymbolMetadata] = {} diff --git a/src/factorium/universe/tags.py b/src/factorium/universe/tags.py index d55dfb1..67c6498 100644 --- a/src/factorium/universe/tags.py +++ b/src/factorium/universe/tags.py @@ -2,6 +2,7 @@ import asyncio import json +import logging import time from pathlib import Path @@ -9,10 +10,16 @@ COINGECKO_BASE_URL = "https://api.coingecko.com/api/v3" +logger = logging.getLogger(__name__) class TagProvider: - """Fetch and cache token categories from CoinGecko.""" + """Fetch and cache token categories from CoinGecko. + + Note: + Calling ``fetch``/``fetch_async`` with ``symbols=None`` performs + full-market category fetching and can be slow for large universes. + """ def __init__( self, @@ -46,6 +53,9 @@ async def fetch_async(self, symbols: list[str] | None = None) -> dict[str, list[ if all(sym in cached for sym in requested): return {sym: cached[sym] for sym in requested} + if requested is None: + logger.warning("Fetching tags for all symbols may take a long time; pass symbols to limit scope.") + headers: dict[str, str] | None = None if self.api_key: headers = {"x-cg-pro-api-key": self.api_key} @@ -81,7 +91,9 @@ async def fetch_async(self, symbols: list[str] | None = None) -> dict[str, list[ return {sym: result.get(sym, []) for sym in requested if sym in result} def fetch(self, symbols: list[str] | None = None) -> dict[str, list[str]]: - return asyncio.run(self.fetch_async(symbols=symbols)) + from ..data.loader import _run_async + + return _run_async(self.fetch_async(symbols=symbols)) def _load_cache(self) -> dict[str, list[str]] | None: if not self._cache_path.exists(): diff --git a/tests/universe/test_metadata.py b/tests/universe/test_metadata.py index 4b6e510..51f0e48 100644 --- a/tests/universe/test_metadata.py +++ b/tests/universe/test_metadata.py @@ -3,6 +3,7 @@ import pytest +import factorium.data.loader as data_loader from factorium.universe.metadata import MetadataProvider @@ -90,3 +91,35 @@ async def should_not_call(*args, **kwargs): out = provider.fetch() assert out == cached + + +def test_metadata_fetch_uses_run_async(monkeypatch: pytest.MonkeyPatch) -> None: + provider = MetadataProvider(market="um") + expected = { + "BTCUSDT": { + "symbol": "BTCUSDT", + "base_asset": "BTC", + "quote_asset": "USDT", + "status": "TRADING", + "listing_date": 1_700_000_000_000, + "is_leveraged": False, + "is_stablecoin_pair": False, + } + } + + async def fake_fetch_async() -> dict[str, dict[str, object]]: + return expected + + called = {"value": False} + + def fake_run_async(coro): + called["value"] = True + coro.close() + return expected + + monkeypatch.setattr(provider, "fetch_async", fake_fetch_async) + monkeypatch.setattr(data_loader, "_run_async", fake_run_async) + + out = provider.fetch() + assert out == expected + assert called["value"] is True diff --git a/tests/universe/test_tags.py b/tests/universe/test_tags.py index 1827602..309e477 100644 --- a/tests/universe/test_tags.py +++ b/tests/universe/test_tags.py @@ -3,6 +3,7 @@ import pytest +import factorium.data.loader as data_loader from factorium.universe.tags import TagProvider @@ -60,3 +61,52 @@ async def should_not_call(*args, **kwargs): out = provider.fetch(symbols=["BTC"]) assert out == {"BTC": ["Layer 1"]} + + +def test_tags_fetch_uses_run_async(monkeypatch: pytest.MonkeyPatch) -> None: + provider = TagProvider() + expected = {"BTC": ["Layer 1"]} + + async def fake_fetch_async(symbols=None): + del symbols + return expected + + called = {"value": False} + + def fake_run_async(coro): + called["value"] = True + coro.close() + return expected + + monkeypatch.setattr(provider, "fetch_async", fake_fetch_async) + monkeypatch.setattr(data_loader, "_run_async", fake_run_async) + + out = provider.fetch(symbols=["BTC"]) + assert out == expected + assert called["value"] is True + + +def test_fetch_warns_when_symbols_is_none( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + provider = TagProvider(cache_dir=tmp_path, cache_ttl=0) + + async def fake_request_json(session, url, params=None, headers=None): + del session, params, headers + if url.endswith("/coins/list"): + return [{"id": "bitcoin", "symbol": "btc", "name": "Bitcoin"}] + if url.endswith("/coins/bitcoin"): + return {"categories": ["Layer 1"]} + raise AssertionError(f"unexpected url: {url}") + + async def no_sleep(seconds: float) -> None: + del seconds + + monkeypatch.setattr(provider, "_request_json", fake_request_json) + monkeypatch.setattr("factorium.universe.tags.asyncio.sleep", no_sleep) + + caplog.set_level("WARNING") + out = provider.fetch(symbols=None) + + assert "BTC" in out + assert "may take a long time" in caplog.text From a8b82d3093fe586138181399956f3a80a921a918 Mon Sep 17 00:00:00 2001 From: novis10813 Date: Sat, 14 Feb 2026 15:19:43 +0800 Subject: [PATCH 13/25] fix(analyzer): re-prepare data when requested periods are missing --- src/factorium/factors/analyzer.py | 9 +++++++-- tests/factors/test_analyzer.py | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/factorium/factors/analyzer.py b/src/factorium/factors/analyzer.py index b90efc4..2488959 100644 --- a/src/factorium/factors/analyzer.py +++ b/src/factorium/factors/analyzer.py @@ -243,8 +243,13 @@ def __init__(self, factor: Factor, prices: AggBar | Factor, quantiles: int = 5, def _ensure_data_prepared(self, periods: list[int] | None = None, price_col: str | None = None) -> None: """Ensure data is prepared. Auto-calls prepare_data() if needed.""" - if not hasattr(self, "_clean_data"): - logger.info("Data not prepared. Auto-calling prepare_data()...") + has_missing_period = bool( + periods + and hasattr(self, "_clean_data") + and any(f"period_{p}" not in self._clean_data.columns for p in periods) + ) + if not hasattr(self, "_clean_data") or has_missing_period: + logger.info("Data not prepared or missing requested periods. Auto-calling prepare_data()...") self.prepare_data(periods=periods, price_col=price_col) def analyze(self, price_col: str = "close", periods: int | list[int] = 1) -> FactorAnalysisResult: diff --git a/tests/factors/test_analyzer.py b/tests/factors/test_analyzer.py index 3eab3c1..fba2582 100644 --- a/tests/factors/test_analyzer.py +++ b/tests/factors/test_analyzer.py @@ -475,3 +475,17 @@ def test_analyze_empty_periods_list_raises_error(sample_data): with pytest.raises(ValueError, match="Periods list cannot be empty"): analyzer.analyze(periods=[]) + + +def test_ensure_data_prepared_reprepare_when_period_missing(sample_data): + agg = AggBar(sample_data) + factor = agg["my_factor"] + prices = agg["close"] + analyzer = FactorAnalyzer(factor, prices) + + analyzer.prepare_data(periods=[1]) + assert "period_1" in analyzer._clean_data.columns + assert "period_5" not in analyzer._clean_data.columns + + analyzer._ensure_data_prepared(periods=[1, 5]) + assert "period_5" in analyzer._clean_data.columns From f14a06109d6bbcc8fca65a8923e4eb3ee128d17d Mon Sep 17 00:00:00 2001 From: novis10813 Date: Sat, 14 Feb 2026 15:19:43 +0800 Subject: [PATCH 14/25] refactor(backtest): remove redundant mask reapplication --- src/factorium/backtest/vectorized.py | 4 +--- tests/backtest/test_vectorized.py | 35 ++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/factorium/backtest/vectorized.py b/src/factorium/backtest/vectorized.py index 743b12a..52a30d7 100644 --- a/src/factorium/backtest/vectorized.py +++ b/src/factorium/backtest/vectorized.py @@ -195,9 +195,7 @@ def _calculate_weights(self, df: pl.DataFrame) -> pl.DataFrame: ) if self._mask is not None: - df = df.with_columns( - pl.when(pl.col(self._mask).fill_null(False)).then(pl.col("weight")).otherwise(0.0).alias("weight") - ).drop("_masked_signal") + df = df.drop("_masked_signal") # Apply constraints for constraint in self.constraints: diff --git a/tests/backtest/test_vectorized.py b/tests/backtest/test_vectorized.py index 1d69066..c8ae6ab 100644 --- a/tests/backtest/test_vectorized.py +++ b/tests/backtest/test_vectorized.py @@ -170,6 +170,41 @@ def test_long_only_weights_sum_to_one(self): if ws > 0: assert abs(ws - 1.0) < 1e-10 + def test_calculate_weights_masked_assets_remain_zero_after_neutralize(self): + timestamps = [1704067200000, 1704070800000, 1704074400000] + rows = [] + for i, ts in enumerate(timestamps): + for symbol, base_price, in_universe in [ + ("A", 100.0, True), + ("B", 80.0, True), + ("C", 60.0, False), + ]: + price = base_price * (1 + 0.01 * i) + rows.append( + { + "start_time": ts, + "end_time": ts + 3600000, + "symbol": symbol, + "open": price, + "high": price, + "low": price, + "close": price, + "volume": 1000.0, + "in_universe": in_universe, + } + ) + + prices = AggBar(pl.DataFrame(rows)) + signal = prices["close"].cs_rank() + bt = VectorizedBacktester(prices=prices, signal=signal, neutralization="market", mask="in_universe") + + combined = bt._prepare_data() + weighted = bt._calculate_weights(combined) + + masked = weighted.filter(~pl.col("in_universe").fill_null(False)) + assert masked["weight"].abs().max() == 0.0 + assert "_masked_signal" not in weighted.columns + class TestMetricsCalculation: """Tests for metrics calculation.""" From 5b5b2d28846519de1633183f39e6586f0e5da259 Mon Sep 17 00:00:00 2001 From: novis10813 Date: Sat, 14 Feb 2026 15:37:25 +0800 Subject: [PATCH 15/25] fix(data): make explicit end_date inclusive by day --- src/factorium/data/utils.py | 7 ++++--- tests/test_data_loader.py | 21 ++++++++++----------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/factorium/data/utils.py b/src/factorium/data/utils.py index 5feb482..147c2b9 100644 --- a/src/factorium/data/utils.py +++ b/src/factorium/data/utils.py @@ -23,7 +23,7 @@ def calculate_date_range( producing duplicate bars with partial OHLCV data. Priority: - 1. If both start_date and end_date are provided: [start, end] + 1. If both start_date and end_date are provided: [start, end + 1 day) 2. If start_date and days are provided: [start, start + days] 3. If neither: [today_midnight - default_days, today_midnight + 1] 4. If only days: [today_midnight - days, today_midnight + 1] @@ -40,9 +40,10 @@ def calculate_date_range( try: if start_date and end_date: start = datetime.strptime(start_date, "%Y-%m-%d") - end = datetime.strptime(end_date, "%Y-%m-%d") - if start > end: + end_inclusive = datetime.strptime(end_date, "%Y-%m-%d") + if start > end_inclusive: raise ValueError("Start date must be earlier than or equal to end date") + end = end_inclusive + timedelta(days=1) return start, end if start_date and days: diff --git a/tests/test_data_loader.py b/tests/test_data_loader.py index 5dc0f8d..e7a28a8 100644 --- a/tests/test_data_loader.py +++ b/tests/test_data_loader.py @@ -8,7 +8,7 @@ import pyarrow as pa import pyarrow.parquet as pq from pathlib import Path -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from unittest.mock import patch, MagicMock, AsyncMock from freezegun import freeze_time @@ -136,7 +136,7 @@ def test_with_start_and_end_date(self): start_dt, end_dt = calculate_date_range(start_date="2024-01-01", end_date="2024-01-07", days=None) assert start_dt == datetime(2024, 1, 1) - assert end_dt == datetime(2024, 1, 7) + assert end_dt == datetime(2024, 1, 8) def test_with_start_date_and_days(self): """Test with start_date and days specified.""" @@ -151,8 +151,8 @@ def test_with_only_days(self): start_dt, end_dt = calculate_date_range(start_date=None, end_date=None, days=7) # end = start of tomorrow (exclusive), start = end - days - assert end_dt == datetime(2024, 6, 16, 0, 0, 0) - assert start_dt == datetime(2024, 6, 9, 0, 0, 0) + assert end_dt == datetime(2024, 6, 16, 0, 0, 0, tzinfo=timezone.utc) + assert start_dt == datetime(2024, 6, 9, 0, 0, 0, tzinfo=timezone.utc) # Both must be midnight-aligned assert start_dt.hour == 0 and start_dt.minute == 0 and start_dt.second == 0 assert end_dt.hour == 0 and end_dt.minute == 0 and end_dt.second == 0 @@ -162,8 +162,8 @@ def test_default_7_days(self): """Test default behavior (no params = 7 days ending tomorrow midnight).""" start_dt, end_dt = calculate_date_range(start_date=None, end_date=None, days=None) - assert end_dt == datetime(2024, 6, 16, 0, 0, 0) - assert start_dt == datetime(2024, 6, 9, 0, 0, 0) + assert end_dt == datetime(2024, 6, 16, 0, 0, 0, tzinfo=timezone.utc) + assert start_dt == datetime(2024, 6, 9, 0, 0, 0, tzinfo=timezone.utc) # Both must be midnight-aligned assert start_dt.hour == 0 and start_dt.minute == 0 and start_dt.second == 0 assert end_dt.hour == 0 and end_dt.minute == 0 and end_dt.second == 0 @@ -174,8 +174,8 @@ def test_midnight_alignment_regardless_of_time(self): start_dt, end_dt = calculate_date_range(start_date=None, end_date=None, days=3) # Should snap to midnight boundaries - assert start_dt == datetime(2024, 6, 13, 0, 0, 0) - assert end_dt == datetime(2024, 6, 16, 0, 0, 0) + assert start_dt == datetime(2024, 6, 13, 0, 0, 0, tzinfo=timezone.utc) + assert end_dt == datetime(2024, 6, 16, 0, 0, 0, tzinfo=timezone.utc) assert start_dt.microsecond == 0 assert end_dt.microsecond == 0 @@ -191,14 +191,14 @@ def test_cross_year_range(self): start_dt, end_dt = calculate_date_range(start_date="2023-12-28", end_date="2024-01-05", days=None) assert start_dt == datetime(2023, 12, 28) - assert end_dt == datetime(2024, 1, 5) + assert end_dt == datetime(2024, 1, 6) def test_single_day_range(self): """Test single day range (start == end).""" start_dt, end_dt = calculate_date_range(start_date="2024-01-01", end_date="2024-01-01", days=None) assert start_dt == datetime(2024, 1, 1) - assert end_dt == datetime(2024, 1, 1) + assert end_dt == datetime(2024, 1, 2) def test_start_date_with_one_day(self): """Test start_date with days=1.""" @@ -208,7 +208,6 @@ def test_start_date_with_one_day(self): assert end_dt == datetime(2024, 1, 2) - # ============================================================================= # TestBuildDateFilter - 日期過濾條件測試 # ============================================================================= From df90c680a1c5e455600742927f2ff84a7ca8384f Mon Sep 17 00:00:00 2001 From: novis10813 Date: Sat, 14 Feb 2026 15:37:25 +0800 Subject: [PATCH 16/25] fix(universe): exclude missing listing dates in MinListingAge --- src/factorium/universe/rules.py | 4 ++-- tests/universe/test_universe_rules.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/factorium/universe/rules.py b/src/factorium/universe/rules.py index 440a172..cfcdac2 100644 --- a/src/factorium/universe/rules.py +++ b/src/factorium/universe/rules.py @@ -116,7 +116,7 @@ def apply( listing_map[sym] = int(listing_date) if not listing_map: - return pl.lit(True) + return pl.lit(False) listing_expr = pl.col("symbol").replace_strict(listing_map, default=None).cast(pl.Int64, strict=False) - return ((pl.col("start_time") - listing_expr) >= self._min_ms) | listing_expr.is_null() + return ((pl.col("start_time") - listing_expr) >= self._min_ms).fill_null(False) diff --git a/tests/universe/test_universe_rules.py b/tests/universe/test_universe_rules.py index 110f79f..bf60098 100644 --- a/tests/universe/test_universe_rules.py +++ b/tests/universe/test_universe_rules.py @@ -91,3 +91,17 @@ def test_universe_combines_rules_with_and_logic() -> None: out = _sample_df().lazy().with_columns(universe.apply(_sample_df().lazy(), metadata).alias("in_universe")).collect() kept_symbols = set(out.filter(pl.col("in_universe"))["symbol"].to_list()) assert kept_symbols == {"BTCUSDT", "NEWUSDT"} + + +def test_min_listing_age_excludes_symbol_when_listing_date_missing() -> None: + metadata = _sample_metadata() + metadata["NEWUSDT"].pop("listing_date") + + out = ( + _sample_df() + .lazy() + .with_columns(MinListingAge(days=90).apply(_sample_df().lazy(), metadata).alias("keep")) + .collect() + ) + new_rows = out.filter(pl.col("symbol") == "NEWUSDT").sort("start_time") + assert new_rows["keep"].to_list() == [False, False] From e37420b963c286f36f6423f07e456bdabe9e6a14 Mon Sep 17 00:00:00 2001 From: novis10813 Date: Sat, 14 Feb 2026 15:37:25 +0800 Subject: [PATCH 17/25] fix(universe): require symbols and handle CoinGecko symbol collisions --- src/factorium/universe/tags.py | 26 ++++++++--------- tests/universe/test_tags.py | 52 +++++++++++++++++++--------------- 2 files changed, 42 insertions(+), 36 deletions(-) diff --git a/src/factorium/universe/tags.py b/src/factorium/universe/tags.py index 67c6498..b00d9f8 100644 --- a/src/factorium/universe/tags.py +++ b/src/factorium/universe/tags.py @@ -17,8 +17,8 @@ class TagProvider: """Fetch and cache token categories from CoinGecko. Note: - Calling ``fetch``/``fetch_async`` with ``symbols=None`` performs - full-market category fetching and can be slow for large universes. + ``symbols`` must be explicitly provided to avoid full-market + category fetching from CoinGecko, which can be very slow. """ def __init__( @@ -44,18 +44,16 @@ async def _request_json( return await response.json() async def fetch_async(self, symbols: list[str] | None = None) -> dict[str, list[str]]: - requested = [s.upper() for s in symbols] if symbols is not None else None + if symbols is None: + raise ValueError("symbols must be provided to avoid fetching the entire CoinGecko database") + + requested = [s.upper() for s in symbols] cached = self._load_cache() if cached is not None: - if requested is None: - return cached if all(sym in cached for sym in requested): return {sym: cached[sym] for sym in requested} - if requested is None: - logger.warning("Fetching tags for all symbols may take a long time; pass symbols to limit scope.") - headers: dict[str, str] | None = None if self.api_key: headers = {"x-cg-pro-api-key": self.api_key} @@ -66,10 +64,14 @@ async def fetch_async(self, symbols: list[str] | None = None) -> dict[str, list[ for item in raw_list if isinstance(raw_list, list) else []: symbol = str(item.get("symbol", "")).upper() coin_id = item.get("id") - if symbol and coin_id and symbol not in symbol_to_id: - symbol_to_id[symbol] = str(coin_id) + if not symbol or not coin_id: + continue + + coin_id_str = str(coin_id) + if symbol not in symbol_to_id or coin_id_str == symbol.lower(): + symbol_to_id[symbol] = coin_id_str - targets = requested or sorted(symbol_to_id.keys()) + targets = requested result: dict[str, list[str]] = {} if cached is None else dict(cached) for symbol in targets: @@ -86,8 +88,6 @@ async def fetch_async(self, symbols: list[str] | None = None) -> dict[str, list[ await asyncio.sleep(0.12) self._save_cache(result) - if requested is None: - return result return {sym: result.get(sym, []) for sym in requested if sym in result} def fetch(self, symbols: list[str] | None = None) -> dict[str, list[str]]: diff --git a/tests/universe/test_tags.py b/tests/universe/test_tags.py index 309e477..20fd684 100644 --- a/tests/universe/test_tags.py +++ b/tests/universe/test_tags.py @@ -34,6 +34,32 @@ async def no_sleep(seconds: float) -> None: assert out["ETH"] == ["Layer 1", "Smart Contract Platform"] +def test_fetch_prefers_canonical_id_on_symbol_collision(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + provider = TagProvider(cache_dir=tmp_path, cache_ttl=0) + + async def fake_request_json(session, url, params=None, headers=None): + del session, params, headers + if url.endswith("/coins/list"): + return [ + {"id": "btc-token", "symbol": "btc", "name": "Some BTC Token"}, + {"id": "btc", "symbol": "btc", "name": "Canonical BTC"}, + ] + if url.endswith("/coins/btc"): + return {"categories": ["Store Of Value"]} + if url.endswith("/coins/btc-token"): + raise AssertionError("should use canonical id when collision exists") + raise AssertionError(f"unexpected url: {url}") + + async def no_sleep(seconds: float) -> None: + del seconds + + monkeypatch.setattr(provider, "_request_json", fake_request_json) + monkeypatch.setattr("factorium.universe.tags.asyncio.sleep", no_sleep) + + out = provider.fetch(symbols=["BTC"]) + assert out["BTC"] == ["Store Of Value"] + + def test_cache_load_save_and_ttl(tmp_path: Path) -> None: provider = TagProvider(cache_dir=tmp_path, cache_ttl=10) sample = {"BTC": ["Layer 1"], "ETH": ["Layer 1", "Smart Contract Platform"]} @@ -86,27 +112,7 @@ def fake_run_async(coro): assert called["value"] is True -def test_fetch_warns_when_symbols_is_none( - monkeypatch: pytest.MonkeyPatch, tmp_path: Path, caplog: pytest.LogCaptureFixture -) -> None: +def test_fetch_raises_when_symbols_is_none(tmp_path: Path) -> None: provider = TagProvider(cache_dir=tmp_path, cache_ttl=0) - - async def fake_request_json(session, url, params=None, headers=None): - del session, params, headers - if url.endswith("/coins/list"): - return [{"id": "bitcoin", "symbol": "btc", "name": "Bitcoin"}] - if url.endswith("/coins/bitcoin"): - return {"categories": ["Layer 1"]} - raise AssertionError(f"unexpected url: {url}") - - async def no_sleep(seconds: float) -> None: - del seconds - - monkeypatch.setattr(provider, "_request_json", fake_request_json) - monkeypatch.setattr("factorium.universe.tags.asyncio.sleep", no_sleep) - - caplog.set_level("WARNING") - out = provider.fetch(symbols=None) - - assert "BTC" in out - assert "may take a long time" in caplog.text + with pytest.raises(ValueError, match="symbols must be provided"): + provider.fetch(symbols=None) From a2555416772ee89d645259626e2ee273c71b8dc1 Mon Sep 17 00:00:00 2001 From: novis10813 Date: Sat, 14 Feb 2026 15:37:25 +0800 Subject: [PATCH 18/25] test(factors): normalize minute literal style in safe ops helper --- tests/factors/test_safe_operations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/factors/test_safe_operations.py b/tests/factors/test_safe_operations.py index c7cc9c3..3b78ad7 100644 --- a/tests/factors/test_safe_operations.py +++ b/tests/factors/test_safe_operations.py @@ -46,8 +46,8 @@ def _make_factor( for s_idx, sym in enumerate(symbols): rows.append( { - "start_time": t * 60000, - "end_time": (t + 1) * 60000, + "start_time": t * 60_000, + "end_time": (t + 1) * 60_000, "symbol": sym, "factor": values[t * n_symbols + s_idx], } From d759e39e4de6e906992841bb6ef88e8fdfb6bfde Mon Sep 17 00:00:00 2001 From: novis10813 Date: Mon, 16 Feb 2026 18:42:16 +0800 Subject: [PATCH 19/25] fix: put coingecko url into constants.py --- src/factorium/constants.py | 4 ++++ src/factorium/universe/tags.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/factorium/constants.py b/src/factorium/constants.py index 42e9115..b7120ec 100644 --- a/src/factorium/constants.py +++ b/src/factorium/constants.py @@ -15,9 +15,13 @@ MIN_PERIODS_PER_YEAR = 1.0 MAX_PERIODS_PER_YEAR = 365.25 * 24 * 60 # Minutes in a year +# External API URLs +COINGECKO_BASE_URL = "https://api.coingecko.com/api/v3" + __all__ = [ "EPSILON", "SECONDS_PER_YEAR", "MIN_PERIODS_PER_YEAR", "MAX_PERIODS_PER_YEAR", + "COINGECKO_BASE_URL", ] diff --git a/src/factorium/universe/tags.py b/src/factorium/universe/tags.py index b00d9f8..2edba28 100644 --- a/src/factorium/universe/tags.py +++ b/src/factorium/universe/tags.py @@ -8,8 +8,8 @@ import aiohttp +from ..constants import COINGECKO_BASE_URL -COINGECKO_BASE_URL = "https://api.coingecko.com/api/v3" logger = logging.getLogger(__name__) From 28a2244816ae2fe252c2d7aae544f67d7799a791 Mon Sep 17 00:00:00 2001 From: novis10813 Date: Mon, 16 Feb 2026 20:46:59 +0800 Subject: [PATCH 20/25] fix(test): correct mock target and missing imports in universe tests - Move _run_async import to module level in metadata.py and tags.py - Patch local module reference in tests to correctly mock _run_async - Fix NameError in test_metadata.py due to missing import --- src/factorium/universe/metadata.py | 3 +-- src/factorium/universe/tags.py | 4 ++-- tests/universe/test_metadata.py | 4 ++-- tests/universe/test_tags.py | 3 ++- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/factorium/universe/metadata.py b/src/factorium/universe/metadata.py index 01e6b6a..df37ca2 100644 --- a/src/factorium/universe/metadata.py +++ b/src/factorium/universe/metadata.py @@ -6,6 +6,7 @@ import aiohttp +from ..data.loader import _run_async from .rules import KNOWN_STABLECOINS, LEVERAGED_PATTERNS, SymbolMetadata @@ -46,8 +47,6 @@ async def fetch_async(self) -> dict[str, SymbolMetadata]: return parsed def fetch(self) -> dict[str, SymbolMetadata]: - from ..data.loader import _run_async - return _run_async(self.fetch_async()) def _parse_exchange_info(self, data: dict) -> dict[str, SymbolMetadata]: diff --git a/src/factorium/universe/tags.py b/src/factorium/universe/tags.py index 2edba28..2992677 100644 --- a/src/factorium/universe/tags.py +++ b/src/factorium/universe/tags.py @@ -9,6 +9,8 @@ import aiohttp from ..constants import COINGECKO_BASE_URL +from ..data.loader import _run_async + logger = logging.getLogger(__name__) @@ -91,8 +93,6 @@ async def fetch_async(self, symbols: list[str] | None = None) -> dict[str, list[ return {sym: result.get(sym, []) for sym in requested if sym in result} def fetch(self, symbols: list[str] | None = None) -> dict[str, list[str]]: - from ..data.loader import _run_async - return _run_async(self.fetch_async(symbols=symbols)) def _load_cache(self) -> dict[str, list[str]] | None: diff --git a/tests/universe/test_metadata.py b/tests/universe/test_metadata.py index 51f0e48..375b710 100644 --- a/tests/universe/test_metadata.py +++ b/tests/universe/test_metadata.py @@ -3,7 +3,7 @@ import pytest -import factorium.data.loader as data_loader +from factorium.universe import metadata as metadata_module from factorium.universe.metadata import MetadataProvider @@ -118,7 +118,7 @@ def fake_run_async(coro): return expected monkeypatch.setattr(provider, "fetch_async", fake_fetch_async) - monkeypatch.setattr(data_loader, "_run_async", fake_run_async) + monkeypatch.setattr(metadata_module, "_run_async", fake_run_async) out = provider.fetch() assert out == expected diff --git a/tests/universe/test_tags.py b/tests/universe/test_tags.py index 20fd684..8122f00 100644 --- a/tests/universe/test_tags.py +++ b/tests/universe/test_tags.py @@ -4,6 +4,7 @@ import pytest import factorium.data.loader as data_loader +from factorium.universe import tags as tags_module from factorium.universe.tags import TagProvider @@ -105,7 +106,7 @@ def fake_run_async(coro): return expected monkeypatch.setattr(provider, "fetch_async", fake_fetch_async) - monkeypatch.setattr(data_loader, "_run_async", fake_run_async) + monkeypatch.setattr(tags_module, "_run_async", fake_run_async) out = provider.fetch(symbols=["BTC"]) assert out == expected From 1ed34108439bda1a5f513dc7f84424326339baf0 Mon Sep 17 00:00:00 2001 From: novis10813 Date: Tue, 17 Feb 2026 21:32:34 +0800 Subject: [PATCH 21/25] chore(release): bump version to 0.4.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9e959bb..93958d0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "factorium" -version = "0.3.2" +version = "0.4.0" description = "A quantitative factor analysis library for financial research" readme = "README.md" requires-python = ">=3.11" From b9aace45cdd4d1be4662b9c87bde1ce348da5cec Mon Sep 17 00:00:00 2001 From: novis10813 Date: Tue, 17 Feb 2026 22:05:48 +0800 Subject: [PATCH 22/25] chore(release): add CHANGELOG for v0.4.0 --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..fc98014 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,27 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [0.4.0] - 2026-02-17 +### Added +- VectorizedBacktester: standardized `signal -> exposure -> weight` pipeline and portfolio schemes (market‑neutral, long‑only, top‑N patterns). (see #5) +- Factor analysis: multi‑horizon IC decay and flexible targets for `FactorAnalyzer` / reports. (see #4) +- Factor correlation utilities and clustering analysis (correlation matrix + visualizations). (see #6) +- Factor orthogonalization utilities (`cs_neutralize` / residual‑based orthogonalization). (see #7) +- Additional backtest metrics: Sortino ratio, Calmar ratio, win rate and improved metrics handling. +- New example notebooks demonstrating multi‑factor workflows and orthogonalization (`examples/04_multi_factor_combination.ipynb`). +- Extensive unit and integration tests for backtest, factor ops, and Polars paths. + +### Changed +- `Backtester` is now an alias for `VectorizedBacktester` (Polars‑based implementation). +- Internal refactors and Polars migration improvements for TS/CS operators and analyzer. + +### Fixed +- Various bug fixes and test stabilizations across data loading and backtest path. + +### Notes +- Backward compatibility: No breaking API changes expected for typical user workflows. See `docs/dev/migration-guide.md` for migration notes if you rely on internal/edge APIs. + +--- + +(Full changelog & commit list available in the release PR.) From 8b1c00a5f1eff34c6efd1de1164b890ed5118c3b Mon Sep 17 00:00:00 2001 From: novis10813 Date: Tue, 17 Feb 2026 22:06:29 +0800 Subject: [PATCH 23/25] docs(release): add 0.4.0 release notes --- docs/release-notes/0.4.0.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 docs/release-notes/0.4.0.md diff --git a/docs/release-notes/0.4.0.md b/docs/release-notes/0.4.0.md new file mode 100644 index 0000000..d6e45d5 --- /dev/null +++ b/docs/release-notes/0.4.0.md @@ -0,0 +1,15 @@ +# Release notes — v0.4.0 + +Released: 2026-02-17 (pending merge) + +## Highlights +- Vectorized backtester: clearer `signal → exposure → weights` pipeline and new portfolio schemes (market‑neutral, long‑only, top‑N). +- Multi‑horizon IC decay, factor correlation matrix + clustering, and orthogonalization utilities. +- New backtest metrics: Sortino, Calmar, win rate. + +## Migration notes +- No breaking changes expected for standard user APIs. If you rely on internal or edge-case APIs, please review the detailed CHANGELOG and tests. + +## Examples & docs +- See `examples/04_multi_factor_combination.ipynb` for correlation and orthogonalization workflows. +- Full changelog: `CHANGELOG.md`. From a25e6b4cd83b456d723fc6dacba8d3a85de76b4a Mon Sep 17 00:00:00 2001 From: novis10813 Date: Tue, 17 Feb 2026 22:16:07 +0800 Subject: [PATCH 24/25] chore(lock): regenerate uv.lock for Python 3.13 (fix s3transfer parse) --- uv.lock | 1547 +++++++++++++++++++++++++++---------------------------- 1 file changed, 763 insertions(+), 784 deletions(-) diff --git a/uv.lock b/uv.lock index 3d563a9..c6df8b1 100644 --- a/uv.lock +++ b/uv.lock @@ -2,9 +2,12 @@ version = 1 revision = 3 requires-python = ">=3.11" resolution-markers = [ - "python_full_version >= '3.14'", - "python_full_version >= '3.12' and python_full_version < '3.14'", - "python_full_version < '3.12'", + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] [[package]] @@ -27,7 +30,7 @@ wheels = [ [[package]] name = "aiohttp" -version = "3.13.2" +version = "3.13.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -38,93 +41,93 @@ dependencies = [ { name = "propcache" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1c/ce/3b83ebba6b3207a7135e5fcaba49706f8a4b6008153b4e30540c982fae26/aiohttp-3.13.2.tar.gz", hash = "sha256:40176a52c186aefef6eb3cad2cdd30cd06e3afbe88fe8ab2af9c0b90f228daca", size = 7837994, upload-time = "2025-10-28T20:59:39.937Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/35/74/b321e7d7ca762638cdf8cdeceb39755d9c745aff7a64c8789be96ddf6e96/aiohttp-3.13.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4647d02df098f6434bafd7f32ad14942f05a9caa06c7016fdcc816f343997dd0", size = 743409, upload-time = "2025-10-28T20:56:00.354Z" }, - { url = "https://files.pythonhosted.org/packages/99/3d/91524b905ec473beaf35158d17f82ef5a38033e5809fe8742e3657cdbb97/aiohttp-3.13.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e3403f24bcb9c3b29113611c3c16a2a447c3953ecf86b79775e7be06f7ae7ccb", size = 497006, upload-time = "2025-10-28T20:56:01.85Z" }, - { url = "https://files.pythonhosted.org/packages/eb/d3/7f68bc02a67716fe80f063e19adbd80a642e30682ce74071269e17d2dba1/aiohttp-3.13.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:43dff14e35aba17e3d6d5ba628858fb8cb51e30f44724a2d2f0c75be492c55e9", size = 493195, upload-time = "2025-10-28T20:56:03.314Z" }, - { url = "https://files.pythonhosted.org/packages/98/31/913f774a4708775433b7375c4f867d58ba58ead833af96c8af3621a0d243/aiohttp-3.13.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2a9ea08e8c58bb17655630198833109227dea914cd20be660f52215f6de5613", size = 1747759, upload-time = "2025-10-28T20:56:04.904Z" }, - { url = "https://files.pythonhosted.org/packages/e8/63/04efe156f4326f31c7c4a97144f82132c3bb21859b7bb84748d452ccc17c/aiohttp-3.13.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53b07472f235eb80e826ad038c9d106c2f653584753f3ddab907c83f49eedead", size = 1704456, upload-time = "2025-10-28T20:56:06.986Z" }, - { url = "https://files.pythonhosted.org/packages/8e/02/4e16154d8e0a9cf4ae76f692941fd52543bbb148f02f098ca73cab9b1c1b/aiohttp-3.13.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e736c93e9c274fce6419af4aac199984d866e55f8a4cec9114671d0ea9688780", size = 1807572, upload-time = "2025-10-28T20:56:08.558Z" }, - { url = "https://files.pythonhosted.org/packages/34/58/b0583defb38689e7f06798f0285b1ffb3a6fb371f38363ce5fd772112724/aiohttp-3.13.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ff5e771f5dcbc81c64898c597a434f7682f2259e0cd666932a913d53d1341d1a", size = 1895954, upload-time = "2025-10-28T20:56:10.545Z" }, - { url = "https://files.pythonhosted.org/packages/6b/f3/083907ee3437425b4e376aa58b2c915eb1a33703ec0dc30040f7ae3368c6/aiohttp-3.13.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3b6fb0c207cc661fa0bf8c66d8d9b657331ccc814f4719468af61034b478592", size = 1747092, upload-time = "2025-10-28T20:56:12.118Z" }, - { url = "https://files.pythonhosted.org/packages/ac/61/98a47319b4e425cc134e05e5f3fc512bf9a04bf65aafd9fdcda5d57ec693/aiohttp-3.13.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:97a0895a8e840ab3520e2288db7cace3a1981300d48babeb50e7425609e2e0ab", size = 1606815, upload-time = "2025-10-28T20:56:14.191Z" }, - { url = "https://files.pythonhosted.org/packages/97/4b/e78b854d82f66bb974189135d31fce265dee0f5344f64dd0d345158a5973/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9e8f8afb552297aca127c90cb840e9a1d4bfd6a10d7d8f2d9176e1acc69bad30", size = 1723789, upload-time = "2025-10-28T20:56:16.101Z" }, - { url = "https://files.pythonhosted.org/packages/ed/fc/9d2ccc794fc9b9acd1379d625c3a8c64a45508b5091c546dea273a41929e/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ed2f9c7216e53c3df02264f25d824b079cc5914f9e2deba94155190ef648ee40", size = 1718104, upload-time = "2025-10-28T20:56:17.655Z" }, - { url = "https://files.pythonhosted.org/packages/66/65/34564b8765ea5c7d79d23c9113135d1dd3609173da13084830f1507d56cf/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:99c5280a329d5fa18ef30fd10c793a190d996567667908bef8a7f81f8202b948", size = 1785584, upload-time = "2025-10-28T20:56:19.238Z" }, - { url = "https://files.pythonhosted.org/packages/30/be/f6a7a426e02fc82781afd62016417b3948e2207426d90a0e478790d1c8a4/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ca6ffef405fc9c09a746cb5d019c1672cd7f402542e379afc66b370833170cf", size = 1595126, upload-time = "2025-10-28T20:56:20.836Z" }, - { url = "https://files.pythonhosted.org/packages/e5/c7/8e22d5d28f94f67d2af496f14a83b3c155d915d1fe53d94b66d425ec5b42/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:47f438b1a28e926c37632bff3c44df7d27c9b57aaf4e34b1def3c07111fdb782", size = 1800665, upload-time = "2025-10-28T20:56:22.922Z" }, - { url = "https://files.pythonhosted.org/packages/d1/11/91133c8b68b1da9fc16555706aa7276fdf781ae2bb0876c838dd86b8116e/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9acda8604a57bb60544e4646a4615c1866ee6c04a8edef9b8ee6fd1d8fa2ddc8", size = 1739532, upload-time = "2025-10-28T20:56:25.924Z" }, - { url = "https://files.pythonhosted.org/packages/17/6b/3747644d26a998774b21a616016620293ddefa4d63af6286f389aedac844/aiohttp-3.13.2-cp311-cp311-win32.whl", hash = "sha256:868e195e39b24aaa930b063c08bb0c17924899c16c672a28a65afded9c46c6ec", size = 431876, upload-time = "2025-10-28T20:56:27.524Z" }, - { url = "https://files.pythonhosted.org/packages/c3/63/688462108c1a00eb9f05765331c107f95ae86f6b197b865d29e930b7e462/aiohttp-3.13.2-cp311-cp311-win_amd64.whl", hash = "sha256:7fd19df530c292542636c2a9a85854fab93474396a52f1695e799186bbd7f24c", size = 456205, upload-time = "2025-10-28T20:56:29.062Z" }, - { url = "https://files.pythonhosted.org/packages/29/9b/01f00e9856d0a73260e86dd8ed0c2234a466c5c1712ce1c281548df39777/aiohttp-3.13.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b1e56bab2e12b2b9ed300218c351ee2a3d8c8fdab5b1ec6193e11a817767e47b", size = 737623, upload-time = "2025-10-28T20:56:30.797Z" }, - { url = "https://files.pythonhosted.org/packages/5a/1b/4be39c445e2b2bd0aab4ba736deb649fabf14f6757f405f0c9685019b9e9/aiohttp-3.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:364e25edaabd3d37b1db1f0cbcee8c73c9a3727bfa262b83e5e4cf3489a2a9dc", size = 492664, upload-time = "2025-10-28T20:56:32.708Z" }, - { url = "https://files.pythonhosted.org/packages/28/66/d35dcfea8050e131cdd731dff36434390479b4045a8d0b9d7111b0a968f1/aiohttp-3.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c5c94825f744694c4b8db20b71dba9a257cd2ba8e010a803042123f3a25d50d7", size = 491808, upload-time = "2025-10-28T20:56:34.57Z" }, - { url = "https://files.pythonhosted.org/packages/00/29/8e4609b93e10a853b65f8291e64985de66d4f5848c5637cddc70e98f01f8/aiohttp-3.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba2715d842ffa787be87cbfce150d5e88c87a98e0b62e0f5aa489169a393dbbb", size = 1738863, upload-time = "2025-10-28T20:56:36.377Z" }, - { url = "https://files.pythonhosted.org/packages/9d/fa/4ebdf4adcc0def75ced1a0d2d227577cd7b1b85beb7edad85fcc87693c75/aiohttp-3.13.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:585542825c4bc662221fb257889e011a5aa00f1ae4d75d1d246a5225289183e3", size = 1700586, upload-time = "2025-10-28T20:56:38.034Z" }, - { url = "https://files.pythonhosted.org/packages/da/04/73f5f02ff348a3558763ff6abe99c223381b0bace05cd4530a0258e52597/aiohttp-3.13.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:39d02cb6025fe1aabca329c5632f48c9532a3dabccd859e7e2f110668972331f", size = 1768625, upload-time = "2025-10-28T20:56:39.75Z" }, - { url = "https://files.pythonhosted.org/packages/f8/49/a825b79ffec124317265ca7d2344a86bcffeb960743487cb11988ffb3494/aiohttp-3.13.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e67446b19e014d37342f7195f592a2a948141d15a312fe0e700c2fd2f03124f6", size = 1867281, upload-time = "2025-10-28T20:56:41.471Z" }, - { url = "https://files.pythonhosted.org/packages/b9/48/adf56e05f81eac31edcfae45c90928f4ad50ef2e3ea72cb8376162a368f8/aiohttp-3.13.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4356474ad6333e41ccefd39eae869ba15a6c5299c9c01dfdcfdd5c107be4363e", size = 1752431, upload-time = "2025-10-28T20:56:43.162Z" }, - { url = "https://files.pythonhosted.org/packages/30/ab/593855356eead019a74e862f21523db09c27f12fd24af72dbc3555b9bfd9/aiohttp-3.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeacf451c99b4525f700f078becff32c32ec327b10dcf31306a8a52d78166de7", size = 1562846, upload-time = "2025-10-28T20:56:44.85Z" }, - { url = "https://files.pythonhosted.org/packages/39/0f/9f3d32271aa8dc35036e9668e31870a9d3b9542dd6b3e2c8a30931cb27ae/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8a9b889aeabd7a4e9af0b7f4ab5ad94d42e7ff679aaec6d0db21e3b639ad58d", size = 1699606, upload-time = "2025-10-28T20:56:46.519Z" }, - { url = "https://files.pythonhosted.org/packages/2c/3c/52d2658c5699b6ef7692a3f7128b2d2d4d9775f2a68093f74bca06cf01e1/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:fa89cb11bc71a63b69568d5b8a25c3ca25b6d54c15f907ca1c130d72f320b76b", size = 1720663, upload-time = "2025-10-28T20:56:48.528Z" }, - { url = "https://files.pythonhosted.org/packages/9b/d4/8f8f3ff1fb7fb9e3f04fcad4e89d8a1cd8fc7d05de67e3de5b15b33008ff/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8aa7c807df234f693fed0ecd507192fc97692e61fee5702cdc11155d2e5cadc8", size = 1737939, upload-time = "2025-10-28T20:56:50.77Z" }, - { url = "https://files.pythonhosted.org/packages/03/d3/ddd348f8a27a634daae39a1b8e291ff19c77867af438af844bf8b7e3231b/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9eb3e33fdbe43f88c3c75fa608c25e7c47bbd80f48d012763cb67c47f39a7e16", size = 1555132, upload-time = "2025-10-28T20:56:52.568Z" }, - { url = "https://files.pythonhosted.org/packages/39/b8/46790692dc46218406f94374903ba47552f2f9f90dad554eed61bfb7b64c/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9434bc0d80076138ea986833156c5a48c9c7a8abb0c96039ddbb4afc93184169", size = 1764802, upload-time = "2025-10-28T20:56:54.292Z" }, - { url = "https://files.pythonhosted.org/packages/ba/e4/19ce547b58ab2a385e5f0b8aa3db38674785085abcf79b6e0edd1632b12f/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ff15c147b2ad66da1f2cbb0622313f2242d8e6e8f9b79b5206c84523a4473248", size = 1719512, upload-time = "2025-10-28T20:56:56.428Z" }, - { url = "https://files.pythonhosted.org/packages/70/30/6355a737fed29dcb6dfdd48682d5790cb5eab050f7b4e01f49b121d3acad/aiohttp-3.13.2-cp312-cp312-win32.whl", hash = "sha256:27e569eb9d9e95dbd55c0fc3ec3a9335defbf1d8bc1d20171a49f3c4c607b93e", size = 426690, upload-time = "2025-10-28T20:56:58.736Z" }, - { url = "https://files.pythonhosted.org/packages/0a/0d/b10ac09069973d112de6ef980c1f6bb31cb7dcd0bc363acbdad58f927873/aiohttp-3.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:8709a0f05d59a71f33fd05c17fc11fcb8c30140506e13c2f5e8ee1b8964e1b45", size = 453465, upload-time = "2025-10-28T20:57:00.795Z" }, - { url = "https://files.pythonhosted.org/packages/bf/78/7e90ca79e5aa39f9694dcfd74f4720782d3c6828113bb1f3197f7e7c4a56/aiohttp-3.13.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7519bdc7dfc1940d201651b52bf5e03f5503bda45ad6eacf64dda98be5b2b6be", size = 732139, upload-time = "2025-10-28T20:57:02.455Z" }, - { url = "https://files.pythonhosted.org/packages/db/ed/1f59215ab6853fbaa5c8495fa6cbc39edfc93553426152b75d82a5f32b76/aiohttp-3.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:088912a78b4d4f547a1f19c099d5a506df17eacec3c6f4375e2831ec1d995742", size = 490082, upload-time = "2025-10-28T20:57:04.784Z" }, - { url = "https://files.pythonhosted.org/packages/68/7b/fe0fe0f5e05e13629d893c760465173a15ad0039c0a5b0d0040995c8075e/aiohttp-3.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5276807b9de9092af38ed23ce120539ab0ac955547b38563a9ba4f5b07b95293", size = 489035, upload-time = "2025-10-28T20:57:06.894Z" }, - { url = "https://files.pythonhosted.org/packages/d2/04/db5279e38471b7ac801d7d36a57d1230feeee130bbe2a74f72731b23c2b1/aiohttp-3.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1237c1375eaef0db4dcd7c2559f42e8af7b87ea7d295b118c60c36a6e61cb811", size = 1720387, upload-time = "2025-10-28T20:57:08.685Z" }, - { url = "https://files.pythonhosted.org/packages/31/07/8ea4326bd7dae2bd59828f69d7fdc6e04523caa55e4a70f4a8725a7e4ed2/aiohttp-3.13.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:96581619c57419c3d7d78703d5b78c1e5e5fc0172d60f555bdebaced82ded19a", size = 1688314, upload-time = "2025-10-28T20:57:10.693Z" }, - { url = "https://files.pythonhosted.org/packages/48/ab/3d98007b5b87ffd519d065225438cc3b668b2f245572a8cb53da5dd2b1bc/aiohttp-3.13.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2713a95b47374169409d18103366de1050fe0ea73db358fc7a7acb2880422d4", size = 1756317, upload-time = "2025-10-28T20:57:12.563Z" }, - { url = "https://files.pythonhosted.org/packages/97/3d/801ca172b3d857fafb7b50c7c03f91b72b867a13abca982ed6b3081774ef/aiohttp-3.13.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:228a1cd556b3caca590e9511a89444925da87d35219a49ab5da0c36d2d943a6a", size = 1858539, upload-time = "2025-10-28T20:57:14.623Z" }, - { url = "https://files.pythonhosted.org/packages/f7/0d/4764669bdf47bd472899b3d3db91fffbe925c8e3038ec591a2fd2ad6a14d/aiohttp-3.13.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac6cde5fba8d7d8c6ac963dbb0256a9854e9fafff52fbcc58fdf819357892c3e", size = 1739597, upload-time = "2025-10-28T20:57:16.399Z" }, - { url = "https://files.pythonhosted.org/packages/c4/52/7bd3c6693da58ba16e657eb904a5b6decfc48ecd06e9ac098591653b1566/aiohttp-3.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2bef8237544f4e42878c61cef4e2839fee6346dc60f5739f876a9c50be7fcdb", size = 1555006, upload-time = "2025-10-28T20:57:18.288Z" }, - { url = "https://files.pythonhosted.org/packages/48/30/9586667acec5993b6f41d2ebcf96e97a1255a85f62f3c653110a5de4d346/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:16f15a4eac3bc2d76c45f7ebdd48a65d41b242eb6c31c2245463b40b34584ded", size = 1683220, upload-time = "2025-10-28T20:57:20.241Z" }, - { url = "https://files.pythonhosted.org/packages/71/01/3afe4c96854cfd7b30d78333852e8e851dceaec1c40fd00fec90c6402dd2/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:bb7fb776645af5cc58ab804c58d7eba545a97e047254a52ce89c157b5af6cd0b", size = 1712570, upload-time = "2025-10-28T20:57:22.253Z" }, - { url = "https://files.pythonhosted.org/packages/11/2c/22799d8e720f4697a9e66fd9c02479e40a49de3de2f0bbe7f9f78a987808/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e1b4951125ec10c70802f2cb09736c895861cd39fd9dcb35107b4dc8ae6220b8", size = 1733407, upload-time = "2025-10-28T20:57:24.37Z" }, - { url = "https://files.pythonhosted.org/packages/34/cb/90f15dd029f07cebbd91f8238a8b363978b530cd128488085b5703683594/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:550bf765101ae721ee1d37d8095f47b1f220650f85fe1af37a90ce75bab89d04", size = 1550093, upload-time = "2025-10-28T20:57:26.257Z" }, - { url = "https://files.pythonhosted.org/packages/69/46/12dce9be9d3303ecbf4d30ad45a7683dc63d90733c2d9fe512be6716cd40/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fe91b87fc295973096251e2d25a811388e7d8adf3bd2b97ef6ae78bc4ac6c476", size = 1758084, upload-time = "2025-10-28T20:57:28.349Z" }, - { url = "https://files.pythonhosted.org/packages/f9/c8/0932b558da0c302ffd639fc6362a313b98fdf235dc417bc2493da8394df7/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0c8e31cfcc4592cb200160344b2fb6ae0f9e4effe06c644b5a125d4ae5ebe23", size = 1716987, upload-time = "2025-10-28T20:57:30.233Z" }, - { url = "https://files.pythonhosted.org/packages/5d/8b/f5bd1a75003daed099baec373aed678f2e9b34f2ad40d85baa1368556396/aiohttp-3.13.2-cp313-cp313-win32.whl", hash = "sha256:0740f31a60848d6edb296a0df827473eede90c689b8f9f2a4cdde74889eb2254", size = 425859, upload-time = "2025-10-28T20:57:32.105Z" }, - { url = "https://files.pythonhosted.org/packages/5d/28/a8a9fc6957b2cee8902414e41816b5ab5536ecf43c3b1843c10e82c559b2/aiohttp-3.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:a88d13e7ca367394908f8a276b89d04a3652044612b9a408a0bb22a5ed976a1a", size = 452192, upload-time = "2025-10-28T20:57:34.166Z" }, - { url = "https://files.pythonhosted.org/packages/9b/36/e2abae1bd815f01c957cbf7be817b3043304e1c87bad526292a0410fdcf9/aiohttp-3.13.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2475391c29230e063ef53a66669b7b691c9bfc3f1426a0f7bcdf1216bdbac38b", size = 735234, upload-time = "2025-10-28T20:57:36.415Z" }, - { url = "https://files.pythonhosted.org/packages/ca/e3/1ee62dde9b335e4ed41db6bba02613295a0d5b41f74a783c142745a12763/aiohttp-3.13.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:f33c8748abef4d8717bb20e8fb1b3e07c6adacb7fd6beaae971a764cf5f30d61", size = 490733, upload-time = "2025-10-28T20:57:38.205Z" }, - { url = "https://files.pythonhosted.org/packages/1a/aa/7a451b1d6a04e8d15a362af3e9b897de71d86feac3babf8894545d08d537/aiohttp-3.13.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ae32f24bbfb7dbb485a24b30b1149e2f200be94777232aeadba3eecece4d0aa4", size = 491303, upload-time = "2025-10-28T20:57:40.122Z" }, - { url = "https://files.pythonhosted.org/packages/57/1e/209958dbb9b01174870f6a7538cd1f3f28274fdbc88a750c238e2c456295/aiohttp-3.13.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d7f02042c1f009ffb70067326ef183a047425bb2ff3bc434ead4dd4a4a66a2b", size = 1717965, upload-time = "2025-10-28T20:57:42.28Z" }, - { url = "https://files.pythonhosted.org/packages/08/aa/6a01848d6432f241416bc4866cae8dc03f05a5a884d2311280f6a09c73d6/aiohttp-3.13.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93655083005d71cd6c072cdab54c886e6570ad2c4592139c3fb967bfc19e4694", size = 1667221, upload-time = "2025-10-28T20:57:44.869Z" }, - { url = "https://files.pythonhosted.org/packages/87/4f/36c1992432d31bbc789fa0b93c768d2e9047ec8c7177e5cd84ea85155f36/aiohttp-3.13.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0db1e24b852f5f664cd728db140cf11ea0e82450471232a394b3d1a540b0f906", size = 1757178, upload-time = "2025-10-28T20:57:47.216Z" }, - { url = "https://files.pythonhosted.org/packages/ac/b4/8e940dfb03b7e0f68a82b88fd182b9be0a65cb3f35612fe38c038c3112cf/aiohttp-3.13.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b009194665bcd128e23eaddef362e745601afa4641930848af4c8559e88f18f9", size = 1838001, upload-time = "2025-10-28T20:57:49.337Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ef/39f3448795499c440ab66084a9db7d20ca7662e94305f175a80f5b7e0072/aiohttp-3.13.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c038a8fdc8103cd51dbd986ecdce141473ffd9775a7a8057a6ed9c3653478011", size = 1716325, upload-time = "2025-10-28T20:57:51.327Z" }, - { url = "https://files.pythonhosted.org/packages/d7/51/b311500ffc860b181c05d91c59a1313bdd05c82960fdd4035a15740d431e/aiohttp-3.13.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66bac29b95a00db411cd758fea0e4b9bdba6d549dfe333f9a945430f5f2cc5a6", size = 1547978, upload-time = "2025-10-28T20:57:53.554Z" }, - { url = "https://files.pythonhosted.org/packages/31/64/b9d733296ef79815226dab8c586ff9e3df41c6aff2e16c06697b2d2e6775/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4ebf9cfc9ba24a74cf0718f04aac2a3bbe745902cc7c5ebc55c0f3b5777ef213", size = 1682042, upload-time = "2025-10-28T20:57:55.617Z" }, - { url = "https://files.pythonhosted.org/packages/3f/30/43d3e0f9d6473a6db7d472104c4eff4417b1e9df01774cb930338806d36b/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a4b88ebe35ce54205c7074f7302bd08a4cb83256a3e0870c72d6f68a3aaf8e49", size = 1680085, upload-time = "2025-10-28T20:57:57.59Z" }, - { url = "https://files.pythonhosted.org/packages/16/51/c709f352c911b1864cfd1087577760ced64b3e5bee2aa88b8c0c8e2e4972/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:98c4fb90bb82b70a4ed79ca35f656f4281885be076f3f970ce315402b53099ae", size = 1728238, upload-time = "2025-10-28T20:57:59.525Z" }, - { url = "https://files.pythonhosted.org/packages/19/e2/19bd4c547092b773caeb48ff5ae4b1ae86756a0ee76c16727fcfd281404b/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:ec7534e63ae0f3759df3a1ed4fa6bc8f75082a924b590619c0dd2f76d7043caa", size = 1544395, upload-time = "2025-10-28T20:58:01.914Z" }, - { url = "https://files.pythonhosted.org/packages/cf/87/860f2803b27dfc5ed7be532832a3498e4919da61299b4a1f8eb89b8ff44d/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5b927cf9b935a13e33644cbed6c8c4b2d0f25b713d838743f8fe7191b33829c4", size = 1742965, upload-time = "2025-10-28T20:58:03.972Z" }, - { url = "https://files.pythonhosted.org/packages/67/7f/db2fc7618925e8c7a601094d5cbe539f732df4fb570740be88ed9e40e99a/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:88d6c017966a78c5265d996c19cdb79235be5e6412268d7e2ce7dee339471b7a", size = 1697585, upload-time = "2025-10-28T20:58:06.189Z" }, - { url = "https://files.pythonhosted.org/packages/0c/07/9127916cb09bb38284db5036036042b7b2c514c8ebaeee79da550c43a6d6/aiohttp-3.13.2-cp314-cp314-win32.whl", hash = "sha256:f7c183e786e299b5d6c49fb43a769f8eb8e04a2726a2bd5887b98b5cc2d67940", size = 431621, upload-time = "2025-10-28T20:58:08.636Z" }, - { url = "https://files.pythonhosted.org/packages/fb/41/554a8a380df6d3a2bba8a7726429a23f4ac62aaf38de43bb6d6cde7b4d4d/aiohttp-3.13.2-cp314-cp314-win_amd64.whl", hash = "sha256:fe242cd381e0fb65758faf5ad96c2e460df6ee5b2de1072fe97e4127927e00b4", size = 457627, upload-time = "2025-10-28T20:58:11Z" }, - { url = "https://files.pythonhosted.org/packages/c7/8e/3824ef98c039d3951cb65b9205a96dd2b20f22241ee17d89c5701557c826/aiohttp-3.13.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:f10d9c0b0188fe85398c61147bbd2a657d616c876863bfeff43376e0e3134673", size = 767360, upload-time = "2025-10-28T20:58:13.358Z" }, - { url = "https://files.pythonhosted.org/packages/a4/0f/6a03e3fc7595421274fa34122c973bde2d89344f8a881b728fa8c774e4f1/aiohttp-3.13.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e7c952aefdf2460f4ae55c5e9c3e80aa72f706a6317e06020f80e96253b1accd", size = 504616, upload-time = "2025-10-28T20:58:15.339Z" }, - { url = "https://files.pythonhosted.org/packages/c6/aa/ed341b670f1bc8a6f2c6a718353d13b9546e2cef3544f573c6a1ff0da711/aiohttp-3.13.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c20423ce14771d98353d2e25e83591fa75dfa90a3c1848f3d7c68243b4fbded3", size = 509131, upload-time = "2025-10-28T20:58:17.693Z" }, - { url = "https://files.pythonhosted.org/packages/7f/f0/c68dac234189dae5c4bbccc0f96ce0cc16b76632cfc3a08fff180045cfa4/aiohttp-3.13.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e96eb1a34396e9430c19d8338d2ec33015e4a87ef2b4449db94c22412e25ccdf", size = 1864168, upload-time = "2025-10-28T20:58:20.113Z" }, - { url = "https://files.pythonhosted.org/packages/8f/65/75a9a76db8364b5d0e52a0c20eabc5d52297385d9af9c35335b924fafdee/aiohttp-3.13.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:23fb0783bc1a33640036465019d3bba069942616a6a2353c6907d7fe1ccdaf4e", size = 1719200, upload-time = "2025-10-28T20:58:22.583Z" }, - { url = "https://files.pythonhosted.org/packages/f5/55/8df2ed78d7f41d232f6bd3ff866b6f617026551aa1d07e2f03458f964575/aiohttp-3.13.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e1a9bea6244a1d05a4e57c295d69e159a5c50d8ef16aa390948ee873478d9a5", size = 1843497, upload-time = "2025-10-28T20:58:24.672Z" }, - { url = "https://files.pythonhosted.org/packages/e9/e0/94d7215e405c5a02ccb6a35c7a3a6cfff242f457a00196496935f700cde5/aiohttp-3.13.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0a3d54e822688b56e9f6b5816fb3de3a3a64660efac64e4c2dc435230ad23bad", size = 1935703, upload-time = "2025-10-28T20:58:26.758Z" }, - { url = "https://files.pythonhosted.org/packages/0b/78/1eeb63c3f9b2d1015a4c02788fb543141aad0a03ae3f7a7b669b2483f8d4/aiohttp-3.13.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7a653d872afe9f33497215745da7a943d1dc15b728a9c8da1c3ac423af35178e", size = 1792738, upload-time = "2025-10-28T20:58:29.787Z" }, - { url = "https://files.pythonhosted.org/packages/41/75/aaf1eea4c188e51538c04cc568040e3082db263a57086ea74a7d38c39e42/aiohttp-3.13.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:56d36e80d2003fa3fc0207fac644216d8532e9504a785ef9a8fd013f84a42c61", size = 1624061, upload-time = "2025-10-28T20:58:32.529Z" }, - { url = "https://files.pythonhosted.org/packages/9b/c2/3b6034de81fbcc43de8aeb209073a2286dfb50b86e927b4efd81cf848197/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:78cd586d8331fb8e241c2dd6b2f4061778cc69e150514b39a9e28dd050475661", size = 1789201, upload-time = "2025-10-28T20:58:34.618Z" }, - { url = "https://files.pythonhosted.org/packages/c9/38/c15dcf6d4d890217dae79d7213988f4e5fe6183d43893a9cf2fe9e84ca8d/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:20b10bbfbff766294fe99987f7bb3b74fdd2f1a2905f2562132641ad434dcf98", size = 1776868, upload-time = "2025-10-28T20:58:38.835Z" }, - { url = "https://files.pythonhosted.org/packages/04/75/f74fd178ac81adf4f283a74847807ade5150e48feda6aef024403716c30c/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9ec49dff7e2b3c85cdeaa412e9d438f0ecd71676fde61ec57027dd392f00c693", size = 1790660, upload-time = "2025-10-28T20:58:41.507Z" }, - { url = "https://files.pythonhosted.org/packages/e7/80/7368bd0d06b16b3aba358c16b919e9c46cf11587dc572091031b0e9e3ef0/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:94f05348c4406450f9d73d38efb41d669ad6cd90c7ee194810d0eefbfa875a7a", size = 1617548, upload-time = "2025-10-28T20:58:43.674Z" }, - { url = "https://files.pythonhosted.org/packages/7d/4b/a6212790c50483cb3212e507378fbe26b5086d73941e1ec4b56a30439688/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:fa4dcb605c6f82a80c7f95713c2b11c3b8e9893b3ebd2bc9bde93165ed6107be", size = 1817240, upload-time = "2025-10-28T20:58:45.787Z" }, - { url = "https://files.pythonhosted.org/packages/ff/f7/ba5f0ba4ea8d8f3c32850912944532b933acbf0f3a75546b89269b9b7dde/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf00e5db968c3f67eccd2778574cf64d8b27d95b237770aa32400bd7a1ca4f6c", size = 1762334, upload-time = "2025-10-28T20:58:47.936Z" }, - { url = "https://files.pythonhosted.org/packages/7e/83/1a5a1856574588b1cad63609ea9ad75b32a8353ac995d830bf5da9357364/aiohttp-3.13.2-cp314-cp314t-win32.whl", hash = "sha256:d23b5fe492b0805a50d3371e8a728a9134d8de5447dce4c885f5587294750734", size = 464685, upload-time = "2025-10-28T20:58:50.642Z" }, - { url = "https://files.pythonhosted.org/packages/9f/4d/d22668674122c08f4d56972297c51a624e64b3ed1efaa40187607a7cb66e/aiohttp-3.13.2-cp314-cp314t-win_amd64.whl", hash = "sha256:ff0a7b0a82a7ab905cbda74006318d1b12e37c797eb1b0d4eb3e316cf47f658f", size = 498093, upload-time = "2025-10-28T20:58:52.782Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/4c/a164164834f03924d9a29dc3acd9e7ee58f95857e0b467f6d04298594ebb/aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b", size = 746051, upload-time = "2026-01-03T17:29:43.287Z" }, + { url = "https://files.pythonhosted.org/packages/82/71/d5c31390d18d4f58115037c432b7e0348c60f6f53b727cad33172144a112/aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64", size = 499234, upload-time = "2026-01-03T17:29:44.822Z" }, + { url = "https://files.pythonhosted.org/packages/0e/c9/741f8ac91e14b1d2e7100690425a5b2b919a87a5075406582991fb7de920/aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea", size = 494979, upload-time = "2026-01-03T17:29:46.405Z" }, + { url = "https://files.pythonhosted.org/packages/75/b5/31d4d2e802dfd59f74ed47eba48869c1c21552c586d5e81a9d0d5c2ad640/aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a", size = 1748297, upload-time = "2026-01-03T17:29:48.083Z" }, + { url = "https://files.pythonhosted.org/packages/1a/3e/eefad0ad42959f226bb79664826883f2687d602a9ae2941a18e0484a74d3/aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540", size = 1707172, upload-time = "2026-01-03T17:29:49.648Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3a/54a64299fac2891c346cdcf2aa6803f994a2e4beeaf2e5a09dcc54acc842/aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b", size = 1805405, upload-time = "2026-01-03T17:29:51.244Z" }, + { url = "https://files.pythonhosted.org/packages/6c/70/ddc1b7169cf64075e864f64595a14b147a895a868394a48f6a8031979038/aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3", size = 1899449, upload-time = "2026-01-03T17:29:53.938Z" }, + { url = "https://files.pythonhosted.org/packages/a1/7e/6815aab7d3a56610891c76ef79095677b8b5be6646aaf00f69b221765021/aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1", size = 1748444, upload-time = "2026-01-03T17:29:55.484Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f2/073b145c4100da5511f457dc0f7558e99b2987cf72600d42b559db856fbc/aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3", size = 1606038, upload-time = "2026-01-03T17:29:57.179Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c1/778d011920cae03ae01424ec202c513dc69243cf2db303965615b81deeea/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440", size = 1724156, upload-time = "2026-01-03T17:29:58.914Z" }, + { url = "https://files.pythonhosted.org/packages/0e/cb/3419eabf4ec1e9ec6f242c32b689248365a1cf621891f6f0386632525494/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7", size = 1722340, upload-time = "2026-01-03T17:30:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/7a/e5/76cf77bdbc435bf233c1f114edad39ed4177ccbfab7c329482b179cff4f4/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c", size = 1783041, upload-time = "2026-01-03T17:30:03.609Z" }, + { url = "https://files.pythonhosted.org/packages/9d/d4/dd1ca234c794fd29c057ce8c0566b8ef7fd6a51069de5f06fa84b9a1971c/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51", size = 1596024, upload-time = "2026-01-03T17:30:05.132Z" }, + { url = "https://files.pythonhosted.org/packages/55/58/4345b5f26661a6180afa686c473620c30a66afdf120ed3dd545bbc809e85/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4", size = 1804590, upload-time = "2026-01-03T17:30:07.135Z" }, + { url = "https://files.pythonhosted.org/packages/7b/06/05950619af6c2df7e0a431d889ba2813c9f0129cec76f663e547a5ad56f2/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29", size = 1740355, upload-time = "2026-01-03T17:30:09.083Z" }, + { url = "https://files.pythonhosted.org/packages/3e/80/958f16de79ba0422d7c1e284b2abd0c84bc03394fbe631d0a39ffa10e1eb/aiohttp-3.13.3-cp311-cp311-win32.whl", hash = "sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239", size = 433701, upload-time = "2026-01-03T17:30:10.869Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f2/27cdf04c9851712d6c1b99df6821a6623c3c9e55956d4b1e318c337b5a48/aiohttp-3.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f", size = 457678, upload-time = "2026-01-03T17:30:12.719Z" }, + { url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732, upload-time = "2026-01-03T17:30:14.23Z" }, + { url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293, upload-time = "2026-01-03T17:30:15.96Z" }, + { url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533, upload-time = "2026-01-03T17:30:17.431Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839, upload-time = "2026-01-03T17:30:19.422Z" }, + { url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932, upload-time = "2026-01-03T17:30:21.756Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906, upload-time = "2026-01-03T17:30:23.932Z" }, + { url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020, upload-time = "2026-01-03T17:30:26Z" }, + { url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181, upload-time = "2026-01-03T17:30:27.554Z" }, + { url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794, upload-time = "2026-01-03T17:30:29.254Z" }, + { url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900, upload-time = "2026-01-03T17:30:31.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239, upload-time = "2026-01-03T17:30:32.703Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527, upload-time = "2026-01-03T17:30:34.695Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489, upload-time = "2026-01-03T17:30:36.864Z" }, + { url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852, upload-time = "2026-01-03T17:30:39.433Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379, upload-time = "2026-01-03T17:30:41.081Z" }, + { url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253, upload-time = "2026-01-03T17:30:42.644Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407, upload-time = "2026-01-03T17:30:44.195Z" }, + { url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190, upload-time = "2026-01-03T17:30:45.832Z" }, + { url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783, upload-time = "2026-01-03T17:30:47.466Z" }, + { url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704, upload-time = "2026-01-03T17:30:49.373Z" }, + { url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652, upload-time = "2026-01-03T17:30:50.974Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014, upload-time = "2026-01-03T17:30:52.729Z" }, + { url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777, upload-time = "2026-01-03T17:30:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276, upload-time = "2026-01-03T17:30:56.512Z" }, + { url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131, upload-time = "2026-01-03T17:30:58.256Z" }, + { url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863, upload-time = "2026-01-03T17:31:00.445Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793, upload-time = "2026-01-03T17:31:03.024Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676, upload-time = "2026-01-03T17:31:04.842Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217, upload-time = "2026-01-03T17:31:06.868Z" }, + { url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303, upload-time = "2026-01-03T17:31:08.958Z" }, + { url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673, upload-time = "2026-01-03T17:31:10.676Z" }, + { url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120, upload-time = "2026-01-03T17:31:12.575Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383, upload-time = "2026-01-03T17:31:14.382Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899, upload-time = "2026-01-03T17:31:15.958Z" }, + { url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238, upload-time = "2026-01-03T17:31:17.909Z" }, + { url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292, upload-time = "2026-01-03T17:31:19.919Z" }, + { url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021, upload-time = "2026-01-03T17:31:21.636Z" }, + { url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263, upload-time = "2026-01-03T17:31:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107, upload-time = "2026-01-03T17:31:25.334Z" }, + { url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196, upload-time = "2026-01-03T17:31:27.394Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591, upload-time = "2026-01-03T17:31:29.238Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277, upload-time = "2026-01-03T17:31:31.053Z" }, + { url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575, upload-time = "2026-01-03T17:31:32.87Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455, upload-time = "2026-01-03T17:31:34.76Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417, upload-time = "2026-01-03T17:31:36.699Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968, upload-time = "2026-01-03T17:31:38.622Z" }, + { url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690, upload-time = "2026-01-03T17:31:40.57Z" }, + { url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390, upload-time = "2026-01-03T17:31:42.857Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188, upload-time = "2026-01-03T17:31:44.984Z" }, + { url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126, upload-time = "2026-01-03T17:31:47.463Z" }, + { url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128, upload-time = "2026-01-03T17:31:49.2Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512, upload-time = "2026-01-03T17:31:51.134Z" }, + { url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444, upload-time = "2026-01-03T17:31:52.85Z" }, + { url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798, upload-time = "2026-01-03T17:31:54.91Z" }, + { url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835, upload-time = "2026-01-03T17:31:56.733Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486, upload-time = "2026-01-03T17:31:58.65Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951, upload-time = "2026-01-03T17:32:00.989Z" }, + { url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001, upload-time = "2026-01-03T17:32:03.122Z" }, + { url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246, upload-time = "2026-01-03T17:32:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131, upload-time = "2026-01-03T17:32:07.607Z" }, + { url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196, upload-time = "2026-01-03T17:32:09.59Z" }, + { url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841, upload-time = "2026-01-03T17:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193, upload-time = "2026-01-03T17:32:13.705Z" }, + { url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979, upload-time = "2026-01-03T17:32:15.965Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193, upload-time = "2026-01-03T17:32:18.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801, upload-time = "2026-01-03T17:32:20.25Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523, upload-time = "2026-01-03T17:32:22.215Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694, upload-time = "2026-01-03T17:32:24.546Z" }, ] [[package]] @@ -247,25 +250,25 @@ wheels = [ [[package]] name = "babel" -version = "2.17.0" +version = "2.18.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, ] [[package]] name = "backrefs" -version = "6.1" +version = "6.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/86/e3/bb3a439d5cb255c4774724810ad8073830fac9c9dee123555820c1bcc806/backrefs-6.1.tar.gz", hash = "sha256:3bba1749aafe1db9b915f00e0dd166cba613b6f788ffd63060ac3485dc9be231", size = 7011962, upload-time = "2025-11-15T14:52:08.323Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/a6/e325ec73b638d3ede4421b5445d4a0b8b219481826cc079d510100af356c/backrefs-6.2.tar.gz", hash = "sha256:f44ff4d48808b243b6c0cdc6231e22195c32f77046018141556c66f8bab72a49", size = 7012303, upload-time = "2026-02-16T19:10:15.828Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/ee/c216d52f58ea75b5e1841022bbae24438b19834a29b163cb32aa3a2a7c6e/backrefs-6.1-py310-none-any.whl", hash = "sha256:2a2ccb96302337ce61ee4717ceacfbf26ba4efb1d55af86564b8bbaeda39cac1", size = 381059, upload-time = "2025-11-15T14:51:59.758Z" }, - { url = "https://files.pythonhosted.org/packages/e6/9a/8da246d988ded941da96c7ed945d63e94a445637eaad985a0ed88787cb89/backrefs-6.1-py311-none-any.whl", hash = "sha256:e82bba3875ee4430f4de4b6db19429a27275d95a5f3773c57e9e18abc23fd2b7", size = 392854, upload-time = "2025-11-15T14:52:01.194Z" }, - { url = "https://files.pythonhosted.org/packages/37/c9/fd117a6f9300c62bbc33bc337fd2b3c6bfe28b6e9701de336b52d7a797ad/backrefs-6.1-py312-none-any.whl", hash = "sha256:c64698c8d2269343d88947c0735cb4b78745bd3ba590e10313fbf3f78c34da5a", size = 398770, upload-time = "2025-11-15T14:52:02.584Z" }, - { url = "https://files.pythonhosted.org/packages/eb/95/7118e935b0b0bd3f94dfec2d852fd4e4f4f9757bdb49850519acd245cd3a/backrefs-6.1-py313-none-any.whl", hash = "sha256:4c9d3dc1e2e558965202c012304f33d4e0e477e1c103663fd2c3cc9bb18b0d05", size = 400726, upload-time = "2025-11-15T14:52:04.093Z" }, - { url = "https://files.pythonhosted.org/packages/1d/72/6296bad135bfafd3254ae3648cd152980a424bd6fed64a101af00cc7ba31/backrefs-6.1-py314-none-any.whl", hash = "sha256:13eafbc9ccd5222e9c1f0bec563e6d2a6d21514962f11e7fc79872fd56cbc853", size = 412584, upload-time = "2025-11-15T14:52:05.233Z" }, - { url = "https://files.pythonhosted.org/packages/02/e3/a4fa1946722c4c7b063cc25043a12d9ce9b4323777f89643be74cef2993c/backrefs-6.1-py39-none-any.whl", hash = "sha256:a9e99b8a4867852cad177a6430e31b0f6e495d65f8c6c134b68c14c3c95bf4b0", size = 381058, upload-time = "2025-11-15T14:52:06.698Z" }, + { url = "https://files.pythonhosted.org/packages/1b/39/3765df263e08a4df37f4f43cb5aa3c6c17a4bdd42ecfe841e04c26037171/backrefs-6.2-py310-none-any.whl", hash = "sha256:0fdc7b012420b6b144410342caeb8adc54c6866cf12064abc9bb211302e496f8", size = 381075, upload-time = "2026-02-16T19:10:04.322Z" }, + { url = "https://files.pythonhosted.org/packages/0f/f0/35240571e1b67ffb19dafb29ab34150b6f59f93f717b041082cdb1bfceb1/backrefs-6.2-py311-none-any.whl", hash = "sha256:08aa7fae530c6b2361d7bdcbda1a7c454e330cc9dbcd03f5c23205e430e5c3be", size = 392874, upload-time = "2026-02-16T19:10:06.314Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/77e8c9745b4d227cce9f5e0a6f68041278c5f9b18588b35905f5f19c1beb/backrefs-6.2-py312-none-any.whl", hash = "sha256:c3f4b9cb2af8cda0d87ab4f57800b57b95428488477be164dd2b47be54db0c90", size = 398787, upload-time = "2026-02-16T19:10:08.274Z" }, + { url = "https://files.pythonhosted.org/packages/c5/71/c754b1737ad99102e03fa3235acb6cb6d3ac9d6f596cbc3e5f236705abd8/backrefs-6.2-py313-none-any.whl", hash = "sha256:12df81596ab511f783b7d87c043ce26bc5b0288cf3bb03610fe76b8189282b2b", size = 400747, upload-time = "2026-02-16T19:10:09.791Z" }, + { url = "https://files.pythonhosted.org/packages/af/75/be12ba31a6eb20dccef2320cd8ccb3f7d9013b68ba4c70156259fee9e409/backrefs-6.2-py314-none-any.whl", hash = "sha256:e5f805ae09819caa1aa0623b4a83790e7028604aa2b8c73ba602c4454e665de7", size = 412602, upload-time = "2026-02-16T19:10:12.317Z" }, + { url = "https://files.pythonhosted.org/packages/21/f8/d02f650c47d05034dcd6f9c8cf94f39598b7a89c00ecda0ecb2911bc27e9/backrefs-6.2-py39-none-any.whl", hash = "sha256:664e33cd88c6840b7625b826ecf2555f32d491800900f5a541f772c485f7cda7", size = 381077, upload-time = "2026-02-16T19:10:13.74Z" }, ] [[package]] @@ -298,34 +301,6 @@ css = [ { name = "tinycss2" }, ] -[[package]] -name = "boto3" -version = "1.42.39" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "botocore" }, - { name = "jmespath" }, - { name = "s3transfer" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b8/ea/b96c77da49fed28744ee0347374d8223994a2b8570e76e8380a4064a8c4a/boto3-1.42.39.tar.gz", hash = "sha256:d03f82363314759eff7f84a27b9e6428125f89d8119e4588e8c2c1d79892c956", size = 112783, upload-time = "2026-01-30T20:38:31.226Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/c4/3493b5c86e32d6dd558b30d16b55503e24a6e6cd7115714bc102b247d26e/boto3-1.42.39-py3-none-any.whl", hash = "sha256:d9d6ce11df309707b490d2f5f785b761cfddfd6d1f665385b78c9d8ed097184b", size = 140606, upload-time = "2026-01-30T20:38:28.635Z" }, -] - -[[package]] -name = "botocore" -version = "1.42.39" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jmespath" }, - { name = "python-dateutil" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ac/a6/3a34d1b74effc0f759f5ff4e91c77729d932bc34dd3207905e9ecbba1103/botocore-1.42.39.tar.gz", hash = "sha256:0f00355050821e91a5fe6d932f7bf220f337249b752899e3e4cf6ed54326249e", size = 14914927, upload-time = "2026-01-30T20:38:19.265Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/71/9a2c88abb5fe47b46168b262254d5b5d635de371eba4bd01ea5c8c109575/botocore-1.42.39-py3-none-any.whl", hash = "sha256:9e0d0fed9226449cc26fcf2bbffc0392ac698dd8378e8395ce54f3ec13f81d58", size = 14591958, upload-time = "2026-01-30T20:38:14.814Z" }, -] - [[package]] name = "certifi" version = "2026.1.4" @@ -592,89 +567,101 @@ wheels = [ [[package]] name = "coverage" -version = "7.13.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b6/45/2c665ca77ec32ad67e25c77daf1cee28ee4558f3bc571cdbaf88a00b9f23/coverage-7.13.0.tar.gz", hash = "sha256:a394aa27f2d7ff9bc04cf703817773a59ad6dfbd577032e690f961d2460ee936", size = 820905, upload-time = "2025-12-08T13:14:38.055Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/dc/888bf90d8b1c3d0b4020a40e52b9f80957d75785931ec66c7dfaccc11c7d/coverage-7.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0dfa3855031070058add1a59fdfda0192fd3e8f97e7c81de0596c145dea51820", size = 218104, upload-time = "2025-12-08T13:12:33.333Z" }, - { url = "https://files.pythonhosted.org/packages/8d/ea/069d51372ad9c380214e86717e40d1a743713a2af191cfba30a0911b0a4a/coverage-7.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fdb6f54f38e334db97f72fa0c701e66d8479af0bc3f9bfb5b90f1c30f54500f", size = 218606, upload-time = "2025-12-08T13:12:34.498Z" }, - { url = "https://files.pythonhosted.org/packages/68/09/77b1c3a66c2aa91141b6c4471af98e5b1ed9b9e6d17255da5eb7992299e3/coverage-7.13.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7e442c013447d1d8d195be62852270b78b6e255b79b8675bad8479641e21fd96", size = 248999, upload-time = "2025-12-08T13:12:36.02Z" }, - { url = "https://files.pythonhosted.org/packages/0a/32/2e2f96e9d5691eaf1181d9040f850b8b7ce165ea10810fd8e2afa534cef7/coverage-7.13.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ed5630d946859de835a85e9a43b721123a8a44ec26e2830b296d478c7fd4259", size = 250925, upload-time = "2025-12-08T13:12:37.221Z" }, - { url = "https://files.pythonhosted.org/packages/7b/45/b88ddac1d7978859b9a39a8a50ab323186148f1d64bc068f86fc77706321/coverage-7.13.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f15a931a668e58087bc39d05d2b4bf4b14ff2875b49c994bbdb1c2217a8daeb", size = 253032, upload-time = "2025-12-08T13:12:38.763Z" }, - { url = "https://files.pythonhosted.org/packages/71/cb/e15513f94c69d4820a34b6bf3d2b1f9f8755fa6021be97c7065442d7d653/coverage-7.13.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30a3a201a127ea57f7e14ba43c93c9c4be8b7d17a26e03bb49e6966d019eede9", size = 249134, upload-time = "2025-12-08T13:12:40.382Z" }, - { url = "https://files.pythonhosted.org/packages/09/61/d960ff7dc9e902af3310ce632a875aaa7860f36d2bc8fc8b37ee7c1b82a5/coverage-7.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7a485ff48fbd231efa32d58f479befce52dcb6bfb2a88bb7bf9a0b89b1bc8030", size = 250731, upload-time = "2025-12-08T13:12:41.992Z" }, - { url = "https://files.pythonhosted.org/packages/98/34/c7c72821794afc7c7c2da1db8f00c2c98353078aa7fb6b5ff36aac834b52/coverage-7.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:22486cdafba4f9e471c816a2a5745337742a617fef68e890d8baf9f3036d7833", size = 248795, upload-time = "2025-12-08T13:12:43.331Z" }, - { url = "https://files.pythonhosted.org/packages/0a/5b/e0f07107987a43b2def9aa041c614ddb38064cbf294a71ef8c67d43a0cdd/coverage-7.13.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:263c3dbccc78e2e331e59e90115941b5f53e85cfcc6b3b2fbff1fd4e3d2c6ea8", size = 248514, upload-time = "2025-12-08T13:12:44.546Z" }, - { url = "https://files.pythonhosted.org/packages/71/c2/c949c5d3b5e9fc6dd79e1b73cdb86a59ef14f3709b1d72bf7668ae12e000/coverage-7.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e5330fa0cc1f5c3c4c3bb8e101b742025933e7848989370a1d4c8c5e401ea753", size = 249424, upload-time = "2025-12-08T13:12:45.759Z" }, - { url = "https://files.pythonhosted.org/packages/11/f1/bbc009abd6537cec0dffb2cc08c17a7f03de74c970e6302db4342a6e05af/coverage-7.13.0-cp311-cp311-win32.whl", hash = "sha256:0f4872f5d6c54419c94c25dd6ae1d015deeb337d06e448cd890a1e89a8ee7f3b", size = 220597, upload-time = "2025-12-08T13:12:47.378Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f6/d9977f2fb51c10fbaed0718ce3d0a8541185290b981f73b1d27276c12d91/coverage-7.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51a202e0f80f241ccb68e3e26e19ab5b3bf0f813314f2c967642f13ebcf1ddfe", size = 221536, upload-time = "2025-12-08T13:12:48.7Z" }, - { url = "https://files.pythonhosted.org/packages/be/ad/3fcf43fd96fb43e337a3073dea63ff148dcc5c41ba7a14d4c7d34efb2216/coverage-7.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:d2a9d7f1c11487b1c69367ab3ac2d81b9b3721f097aa409a3191c3e90f8f3dd7", size = 220206, upload-time = "2025-12-08T13:12:50.365Z" }, - { url = "https://files.pythonhosted.org/packages/9b/f1/2619559f17f31ba00fc40908efd1fbf1d0a5536eb75dc8341e7d660a08de/coverage-7.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0b3d67d31383c4c68e19a88e28fc4c2e29517580f1b0ebec4a069d502ce1e0bf", size = 218274, upload-time = "2025-12-08T13:12:52.095Z" }, - { url = "https://files.pythonhosted.org/packages/2b/11/30d71ae5d6e949ff93b2a79a2c1b4822e00423116c5c6edfaeef37301396/coverage-7.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:581f086833d24a22c89ae0fe2142cfaa1c92c930adf637ddf122d55083fb5a0f", size = 218638, upload-time = "2025-12-08T13:12:53.418Z" }, - { url = "https://files.pythonhosted.org/packages/79/c2/fce80fc6ded8d77e53207489d6065d0fed75db8951457f9213776615e0f5/coverage-7.13.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0a3a30f0e257df382f5f9534d4ce3d4cf06eafaf5192beb1a7bd066cb10e78fb", size = 250129, upload-time = "2025-12-08T13:12:54.744Z" }, - { url = "https://files.pythonhosted.org/packages/5b/b6/51b5d1eb6fcbb9a1d5d6984e26cbe09018475c2922d554fd724dd0f056ee/coverage-7.13.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:583221913fbc8f53b88c42e8dbb8fca1d0f2e597cb190ce45916662b8b9d9621", size = 252885, upload-time = "2025-12-08T13:12:56.401Z" }, - { url = "https://files.pythonhosted.org/packages/0d/f8/972a5affea41de798691ab15d023d3530f9f56a72e12e243f35031846ff7/coverage-7.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f5d9bd30756fff3e7216491a0d6d520c448d5124d3d8e8f56446d6412499e74", size = 253974, upload-time = "2025-12-08T13:12:57.718Z" }, - { url = "https://files.pythonhosted.org/packages/8a/56/116513aee860b2c7968aa3506b0f59b22a959261d1dbf3aea7b4450a7520/coverage-7.13.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a23e5a1f8b982d56fa64f8e442e037f6ce29322f1f9e6c2344cd9e9f4407ee57", size = 250538, upload-time = "2025-12-08T13:12:59.254Z" }, - { url = "https://files.pythonhosted.org/packages/d6/75/074476d64248fbadf16dfafbf93fdcede389ec821f74ca858d7c87d2a98c/coverage-7.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b01c22bc74a7fb44066aaf765224c0d933ddf1f5047d6cdfe4795504a4493f8", size = 251912, upload-time = "2025-12-08T13:13:00.604Z" }, - { url = "https://files.pythonhosted.org/packages/f2/d2/aa4f8acd1f7c06024705c12609d8698c51b27e4d635d717cd1934c9668e2/coverage-7.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:898cce66d0836973f48dda4e3514d863d70142bdf6dfab932b9b6a90ea5b222d", size = 250054, upload-time = "2025-12-08T13:13:01.892Z" }, - { url = "https://files.pythonhosted.org/packages/19/98/8df9e1af6a493b03694a1e8070e024e7d2cdc77adedc225a35e616d505de/coverage-7.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3ab483ea0e251b5790c2aac03acde31bff0c736bf8a86829b89382b407cd1c3b", size = 249619, upload-time = "2025-12-08T13:13:03.236Z" }, - { url = "https://files.pythonhosted.org/packages/d8/71/f8679231f3353018ca66ef647fa6fe7b77e6bff7845be54ab84f86233363/coverage-7.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d84e91521c5e4cb6602fe11ece3e1de03b2760e14ae4fcf1a4b56fa3c801fcd", size = 251496, upload-time = "2025-12-08T13:13:04.511Z" }, - { url = "https://files.pythonhosted.org/packages/04/86/9cb406388034eaf3c606c22094edbbb82eea1fa9d20c0e9efadff20d0733/coverage-7.13.0-cp312-cp312-win32.whl", hash = "sha256:193c3887285eec1dbdb3f2bd7fbc351d570ca9c02ca756c3afbc71b3c98af6ef", size = 220808, upload-time = "2025-12-08T13:13:06.422Z" }, - { url = "https://files.pythonhosted.org/packages/1c/59/af483673df6455795daf5f447c2f81a3d2fcfc893a22b8ace983791f6f34/coverage-7.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:4f3e223b2b2db5e0db0c2b97286aba0036ca000f06aca9b12112eaa9af3d92ae", size = 221616, upload-time = "2025-12-08T13:13:07.95Z" }, - { url = "https://files.pythonhosted.org/packages/64/b0/959d582572b30a6830398c60dd419c1965ca4b5fb38ac6b7093a0d50ca8d/coverage-7.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:086cede306d96202e15a4b77ace8472e39d9f4e5f9fd92dd4fecdfb2313b2080", size = 220261, upload-time = "2025-12-08T13:13:09.581Z" }, - { url = "https://files.pythonhosted.org/packages/7c/cc/bce226595eb3bf7d13ccffe154c3c487a22222d87ff018525ab4dd2e9542/coverage-7.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:28ee1c96109974af104028a8ef57cec21447d42d0e937c0275329272e370ebcf", size = 218297, upload-time = "2025-12-08T13:13:10.977Z" }, - { url = "https://files.pythonhosted.org/packages/3b/9f/73c4d34600aae03447dff3d7ad1d0ac649856bfb87d1ca7d681cfc913f9e/coverage-7.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d1e97353dcc5587b85986cda4ff3ec98081d7e84dd95e8b2a6d59820f0545f8a", size = 218673, upload-time = "2025-12-08T13:13:12.562Z" }, - { url = "https://files.pythonhosted.org/packages/63/ab/8fa097db361a1e8586535ae5073559e6229596b3489ec3ef2f5b38df8cb2/coverage-7.13.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:99acd4dfdfeb58e1937629eb1ab6ab0899b131f183ee5f23e0b5da5cba2fec74", size = 249652, upload-time = "2025-12-08T13:13:13.909Z" }, - { url = "https://files.pythonhosted.org/packages/90/3a/9bfd4de2ff191feb37ef9465855ca56a6f2f30a3bca172e474130731ac3d/coverage-7.13.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff45e0cd8451e293b63ced93161e189780baf444119391b3e7d25315060368a6", size = 252251, upload-time = "2025-12-08T13:13:15.553Z" }, - { url = "https://files.pythonhosted.org/packages/df/61/b5d8105f016e1b5874af0d7c67542da780ccd4a5f2244a433d3e20ceb1ad/coverage-7.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4f72a85316d8e13234cafe0a9f81b40418ad7a082792fa4165bd7d45d96066b", size = 253492, upload-time = "2025-12-08T13:13:16.849Z" }, - { url = "https://files.pythonhosted.org/packages/f3/b8/0fad449981803cc47a4694768b99823fb23632150743f9c83af329bb6090/coverage-7.13.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11c21557d0e0a5a38632cbbaca5f008723b26a89d70db6315523df6df77d6232", size = 249850, upload-time = "2025-12-08T13:13:18.142Z" }, - { url = "https://files.pythonhosted.org/packages/9a/e9/8d68337c3125014d918cf4327d5257553a710a2995a6a6de2ac77e5aa429/coverage-7.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76541dc8d53715fb4f7a3a06b34b0dc6846e3c69bc6204c55653a85dd6220971", size = 251633, upload-time = "2025-12-08T13:13:19.56Z" }, - { url = "https://files.pythonhosted.org/packages/55/14/d4112ab26b3a1bc4b3c1295d8452dcf399ed25be4cf649002fb3e64b2d93/coverage-7.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6e9e451dee940a86789134b6b0ffbe31c454ade3b849bb8a9d2cca2541a8e91d", size = 249586, upload-time = "2025-12-08T13:13:20.883Z" }, - { url = "https://files.pythonhosted.org/packages/2c/a9/22b0000186db663b0d82f86c2f1028099ae9ac202491685051e2a11a5218/coverage-7.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5c67dace46f361125e6b9cace8fe0b729ed8479f47e70c89b838d319375c8137", size = 249412, upload-time = "2025-12-08T13:13:22.22Z" }, - { url = "https://files.pythonhosted.org/packages/a1/2e/42d8e0d9e7527fba439acdc6ed24a2b97613b1dc85849b1dd935c2cffef0/coverage-7.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f59883c643cb19630500f57016f76cfdcd6845ca8c5b5ea1f6e17f74c8e5f511", size = 251191, upload-time = "2025-12-08T13:13:23.899Z" }, - { url = "https://files.pythonhosted.org/packages/a4/af/8c7af92b1377fd8860536aadd58745119252aaaa71a5213e5a8e8007a9f5/coverage-7.13.0-cp313-cp313-win32.whl", hash = "sha256:58632b187be6f0be500f553be41e277712baa278147ecb7559983c6d9faf7ae1", size = 220829, upload-time = "2025-12-08T13:13:25.182Z" }, - { url = "https://files.pythonhosted.org/packages/58/f9/725e8bf16f343d33cbe076c75dc8370262e194ff10072c0608b8e5cf33a3/coverage-7.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:73419b89f812f498aca53f757dd834919b48ce4799f9d5cad33ca0ae442bdb1a", size = 221640, upload-time = "2025-12-08T13:13:26.836Z" }, - { url = "https://files.pythonhosted.org/packages/8a/ff/e98311000aa6933cc79274e2b6b94a2fe0fe3434fca778eba82003675496/coverage-7.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:eb76670874fdd6091eedcc856128ee48c41a9bbbb9c3f1c7c3cf169290e3ffd6", size = 220269, upload-time = "2025-12-08T13:13:28.116Z" }, - { url = "https://files.pythonhosted.org/packages/cf/cf/bbaa2e1275b300343ea865f7d424cc0a2e2a1df6925a070b2b2d5d765330/coverage-7.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6e63ccc6e0ad8986386461c3c4b737540f20426e7ec932f42e030320896c311a", size = 218990, upload-time = "2025-12-08T13:13:29.463Z" }, - { url = "https://files.pythonhosted.org/packages/21/1d/82f0b3323b3d149d7672e7744c116e9c170f4957e0c42572f0366dbb4477/coverage-7.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:494f5459ffa1bd45e18558cd98710c36c0b8fbfa82a5eabcbe671d80ecffbfe8", size = 219340, upload-time = "2025-12-08T13:13:31.524Z" }, - { url = "https://files.pythonhosted.org/packages/fb/e3/fe3fd4702a3832a255f4d43013eacb0ef5fc155a5960ea9269d8696db28b/coverage-7.13.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:06cac81bf10f74034e055e903f5f946e3e26fc51c09fc9f584e4a1605d977053", size = 260638, upload-time = "2025-12-08T13:13:32.965Z" }, - { url = "https://files.pythonhosted.org/packages/ad/01/63186cb000307f2b4da463f72af9b85d380236965574c78e7e27680a2593/coverage-7.13.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f2ffc92b46ed6e6760f1d47a71e56b5664781bc68986dbd1836b2b70c0ce2071", size = 262705, upload-time = "2025-12-08T13:13:34.378Z" }, - { url = "https://files.pythonhosted.org/packages/7c/a1/c0dacef0cc865f2455d59eed3548573ce47ed603205ffd0735d1d78b5906/coverage-7.13.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0602f701057c6823e5db1b74530ce85f17c3c5be5c85fc042ac939cbd909426e", size = 265125, upload-time = "2025-12-08T13:13:35.73Z" }, - { url = "https://files.pythonhosted.org/packages/ef/92/82b99223628b61300bd382c205795533bed021505eab6dd86e11fb5d7925/coverage-7.13.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:25dc33618d45456ccb1d37bce44bc78cf269909aa14c4db2e03d63146a8a1493", size = 259844, upload-time = "2025-12-08T13:13:37.69Z" }, - { url = "https://files.pythonhosted.org/packages/cf/2c/89b0291ae4e6cd59ef042708e1c438e2290f8c31959a20055d8768349ee2/coverage-7.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:71936a8b3b977ddd0b694c28c6a34f4fff2e9dd201969a4ff5d5fc7742d614b0", size = 262700, upload-time = "2025-12-08T13:13:39.525Z" }, - { url = "https://files.pythonhosted.org/packages/bf/f9/a5f992efae1996245e796bae34ceb942b05db275e4b34222a9a40b9fbd3b/coverage-7.13.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:936bc20503ce24770c71938d1369461f0c5320830800933bc3956e2a4ded930e", size = 260321, upload-time = "2025-12-08T13:13:41.172Z" }, - { url = "https://files.pythonhosted.org/packages/4c/89/a29f5d98c64fedbe32e2ac3c227fbf78edc01cc7572eee17d61024d89889/coverage-7.13.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:af0a583efaacc52ae2521f8d7910aff65cdb093091d76291ac5820d5e947fc1c", size = 259222, upload-time = "2025-12-08T13:13:43.282Z" }, - { url = "https://files.pythonhosted.org/packages/b3/c3/940fe447aae302a6701ee51e53af7e08b86ff6eed7631e5740c157ee22b9/coverage-7.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f1c23e24a7000da892a312fb17e33c5f94f8b001de44b7cf8ba2e36fbd15859e", size = 261411, upload-time = "2025-12-08T13:13:44.72Z" }, - { url = "https://files.pythonhosted.org/packages/eb/31/12a4aec689cb942a89129587860ed4d0fd522d5fda81237147fde554b8ae/coverage-7.13.0-cp313-cp313t-win32.whl", hash = "sha256:5f8a0297355e652001015e93be345ee54393e45dc3050af4a0475c5a2b767d46", size = 221505, upload-time = "2025-12-08T13:13:46.332Z" }, - { url = "https://files.pythonhosted.org/packages/65/8c/3b5fe3259d863572d2b0827642c50c3855d26b3aefe80bdc9eba1f0af3b0/coverage-7.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6abb3a4c52f05e08460bd9acf04fec027f8718ecaa0d09c40ffbc3fbd70ecc39", size = 222569, upload-time = "2025-12-08T13:13:47.79Z" }, - { url = "https://files.pythonhosted.org/packages/b0/39/f71fa8316a96ac72fc3908839df651e8eccee650001a17f2c78cdb355624/coverage-7.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:3ad968d1e3aa6ce5be295ab5fe3ae1bf5bb4769d0f98a80a0252d543a2ef2e9e", size = 220841, upload-time = "2025-12-08T13:13:49.243Z" }, - { url = "https://files.pythonhosted.org/packages/f8/4b/9b54bedda55421449811dcd5263a2798a63f48896c24dfb92b0f1b0845bd/coverage-7.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:453b7ec753cf5e4356e14fe858064e5520c460d3bbbcb9c35e55c0d21155c256", size = 218343, upload-time = "2025-12-08T13:13:50.811Z" }, - { url = "https://files.pythonhosted.org/packages/59/df/c3a1f34d4bba2e592c8979f924da4d3d4598b0df2392fbddb7761258e3dc/coverage-7.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:af827b7cbb303e1befa6c4f94fd2bf72f108089cfa0f8abab8f4ca553cf5ca5a", size = 218672, upload-time = "2025-12-08T13:13:52.284Z" }, - { url = "https://files.pythonhosted.org/packages/07/62/eec0659e47857698645ff4e6ad02e30186eb8afd65214fd43f02a76537cb/coverage-7.13.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9987a9e4f8197a1000280f7cc089e3ea2c8b3c0a64d750537809879a7b4ceaf9", size = 249715, upload-time = "2025-12-08T13:13:53.791Z" }, - { url = "https://files.pythonhosted.org/packages/23/2d/3c7ff8b2e0e634c1f58d095f071f52ed3c23ff25be524b0ccae8b71f99f8/coverage-7.13.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3188936845cd0cb114fa6a51842a304cdbac2958145d03be2377ec41eb285d19", size = 252225, upload-time = "2025-12-08T13:13:55.274Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ac/fb03b469d20e9c9a81093575003f959cf91a4a517b783aab090e4538764b/coverage-7.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2bdb3babb74079f021696cb46b8bb5f5661165c385d3a238712b031a12355be", size = 253559, upload-time = "2025-12-08T13:13:57.161Z" }, - { url = "https://files.pythonhosted.org/packages/29/62/14afa9e792383c66cc0a3b872a06ded6e4ed1079c7d35de274f11d27064e/coverage-7.13.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7464663eaca6adba4175f6c19354feea61ebbdd735563a03d1e472c7072d27bb", size = 249724, upload-time = "2025-12-08T13:13:58.692Z" }, - { url = "https://files.pythonhosted.org/packages/31/b7/333f3dab2939070613696ab3ee91738950f0467778c6e5a5052e840646b7/coverage-7.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8069e831f205d2ff1f3d355e82f511eb7c5522d7d413f5db5756b772ec8697f8", size = 251582, upload-time = "2025-12-08T13:14:00.642Z" }, - { url = "https://files.pythonhosted.org/packages/81/cb/69162bda9381f39b2287265d7e29ee770f7c27c19f470164350a38318764/coverage-7.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6fb2d5d272341565f08e962cce14cdf843a08ac43bd621783527adb06b089c4b", size = 249538, upload-time = "2025-12-08T13:14:02.556Z" }, - { url = "https://files.pythonhosted.org/packages/e0/76/350387b56a30f4970abe32b90b2a434f87d29f8b7d4ae40d2e8a85aacfb3/coverage-7.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5e70f92ef89bac1ac8a99b3324923b4749f008fdbd7aa9cb35e01d7a284a04f9", size = 249349, upload-time = "2025-12-08T13:14:04.015Z" }, - { url = "https://files.pythonhosted.org/packages/86/0d/7f6c42b8d59f4c7e43ea3059f573c0dcfed98ba46eb43c68c69e52ae095c/coverage-7.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4b5de7d4583e60d5fd246dd57fcd3a8aa23c6e118a8c72b38adf666ba8e7e927", size = 251011, upload-time = "2025-12-08T13:14:05.505Z" }, - { url = "https://files.pythonhosted.org/packages/d7/f1/4bb2dff379721bb0b5c649d5c5eaf438462cad824acf32eb1b7ca0c7078e/coverage-7.13.0-cp314-cp314-win32.whl", hash = "sha256:a6c6e16b663be828a8f0b6c5027d36471d4a9f90d28444aa4ced4d48d7d6ae8f", size = 221091, upload-time = "2025-12-08T13:14:07.127Z" }, - { url = "https://files.pythonhosted.org/packages/ba/44/c239da52f373ce379c194b0ee3bcc121020e397242b85f99e0afc8615066/coverage-7.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:0900872f2fdb3ee5646b557918d02279dc3af3dfb39029ac4e945458b13f73bc", size = 221904, upload-time = "2025-12-08T13:14:08.542Z" }, - { url = "https://files.pythonhosted.org/packages/89/1f/b9f04016d2a29c2e4a0307baefefad1a4ec5724946a2b3e482690486cade/coverage-7.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:3a10260e6a152e5f03f26db4a407c4c62d3830b9af9b7c0450b183615f05d43b", size = 220480, upload-time = "2025-12-08T13:14:10.958Z" }, - { url = "https://files.pythonhosted.org/packages/16/d4/364a1439766c8e8647860584171c36010ca3226e6e45b1753b1b249c5161/coverage-7.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9097818b6cc1cfb5f174e3263eba4a62a17683bcfe5c4b5d07f4c97fa51fbf28", size = 219074, upload-time = "2025-12-08T13:14:13.345Z" }, - { url = "https://files.pythonhosted.org/packages/ce/f4/71ba8be63351e099911051b2089662c03d5671437a0ec2171823c8e03bec/coverage-7.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0018f73dfb4301a89292c73be6ba5f58722ff79f51593352759c1790ded1cabe", size = 219342, upload-time = "2025-12-08T13:14:15.02Z" }, - { url = "https://files.pythonhosted.org/packages/5e/25/127d8ed03d7711a387d96f132589057213e3aef7475afdaa303412463f22/coverage-7.13.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:166ad2a22ee770f5656e1257703139d3533b4a0b6909af67c6b4a3adc1c98657", size = 260713, upload-time = "2025-12-08T13:14:16.907Z" }, - { url = "https://files.pythonhosted.org/packages/fd/db/559fbb6def07d25b2243663b46ba9eb5a3c6586c0c6f4e62980a68f0ee1c/coverage-7.13.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f6aaef16d65d1787280943f1c8718dc32e9cf141014e4634d64446702d26e0ff", size = 262825, upload-time = "2025-12-08T13:14:18.68Z" }, - { url = "https://files.pythonhosted.org/packages/37/99/6ee5bf7eff884766edb43bd8736b5e1c5144d0fe47498c3779326fe75a35/coverage-7.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e999e2dcc094002d6e2c7bbc1fb85b58ba4f465a760a8014d97619330cdbbbf3", size = 265233, upload-time = "2025-12-08T13:14:20.55Z" }, - { url = "https://files.pythonhosted.org/packages/d8/90/92f18fe0356ea69e1f98f688ed80cec39f44e9f09a1f26a1bbf017cc67f2/coverage-7.13.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:00c3d22cf6fb1cf3bf662aaaa4e563be8243a5ed2630339069799835a9cc7f9b", size = 259779, upload-time = "2025-12-08T13:14:22.367Z" }, - { url = "https://files.pythonhosted.org/packages/90/5d/b312a8b45b37a42ea7d27d7d3ff98ade3a6c892dd48d1d503e773503373f/coverage-7.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22ccfe8d9bb0d6134892cbe1262493a8c70d736b9df930f3f3afae0fe3ac924d", size = 262700, upload-time = "2025-12-08T13:14:24.309Z" }, - { url = "https://files.pythonhosted.org/packages/63/f8/b1d0de5c39351eb71c366f872376d09386640840a2e09b0d03973d791e20/coverage-7.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9372dff5ea15930fea0445eaf37bbbafbc771a49e70c0aeed8b4e2c2614cc00e", size = 260302, upload-time = "2025-12-08T13:14:26.068Z" }, - { url = "https://files.pythonhosted.org/packages/aa/7c/d42f4435bc40c55558b3109a39e2d456cddcec37434f62a1f1230991667a/coverage-7.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:69ac2c492918c2461bc6ace42d0479638e60719f2a4ef3f0815fa2df88e9f940", size = 259136, upload-time = "2025-12-08T13:14:27.604Z" }, - { url = "https://files.pythonhosted.org/packages/b8/d3/23413241dc04d47cfe19b9a65b32a2edd67ecd0b817400c2843ebc58c847/coverage-7.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:739c6c051a7540608d097b8e13c76cfa85263ced467168dc6b477bae3df7d0e2", size = 261467, upload-time = "2025-12-08T13:14:29.09Z" }, - { url = "https://files.pythonhosted.org/packages/13/e6/6e063174500eee216b96272c0d1847bf215926786f85c2bd024cf4d02d2f/coverage-7.13.0-cp314-cp314t-win32.whl", hash = "sha256:fe81055d8c6c9de76d60c94ddea73c290b416e061d40d542b24a5871bad498b7", size = 221875, upload-time = "2025-12-08T13:14:31.106Z" }, - { url = "https://files.pythonhosted.org/packages/3b/46/f4fb293e4cbe3620e3ac2a3e8fd566ed33affb5861a9b20e3dd6c1896cbc/coverage-7.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:445badb539005283825959ac9fa4a28f712c214b65af3a2c464f1adc90f5fcbc", size = 222982, upload-time = "2025-12-08T13:14:33.1Z" }, - { url = "https://files.pythonhosted.org/packages/68/62/5b3b9018215ed9733fbd1ae3b2ed75c5de62c3b55377a52cae732e1b7805/coverage-7.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:de7f6748b890708578fc4b7bb967d810aeb6fcc9bff4bb77dbca77dab2f9df6a", size = 221016, upload-time = "2025-12-08T13:14:34.601Z" }, - { url = "https://files.pythonhosted.org/packages/8d/4c/1968f32fb9a2604645827e11ff84a31e59d532e01995f904723b4f5328b3/coverage-7.13.0-py3-none-any.whl", hash = "sha256:850d2998f380b1e266459ca5b47bc9e7daf9af1d070f66317972f382d46f1904", size = 210068, upload-time = "2025-12-08T13:14:36.236Z" }, +version = "7.13.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/ad/b59e5b451cf7172b8d1043dc0fa718f23aab379bc1521ee13d4bd9bfa960/coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053", size = 219278, upload-time = "2026-02-09T12:56:31.673Z" }, + { url = "https://files.pythonhosted.org/packages/f1/17/0cb7ca3de72e5f4ef2ec2fa0089beafbcaaaead1844e8b8a63d35173d77d/coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11", size = 219783, upload-time = "2026-02-09T12:56:33.104Z" }, + { url = "https://files.pythonhosted.org/packages/ab/63/325d8e5b11e0eaf6d0f6a44fad444ae58820929a9b0de943fa377fe73e85/coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa", size = 250200, upload-time = "2026-02-09T12:56:34.474Z" }, + { url = "https://files.pythonhosted.org/packages/76/53/c16972708cbb79f2942922571a687c52bd109a7bd51175aeb7558dff2236/coverage-7.13.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7", size = 252114, upload-time = "2026-02-09T12:56:35.749Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c2/7ab36d8b8cc412bec9ea2d07c83c48930eb4ba649634ba00cb7e4e0f9017/coverage-7.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00", size = 254220, upload-time = "2026-02-09T12:56:37.796Z" }, + { url = "https://files.pythonhosted.org/packages/d6/4d/cf52c9a3322c89a0e6febdfbc83bb45c0ed3c64ad14081b9503adee702e7/coverage-7.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef", size = 256164, upload-time = "2026-02-09T12:56:39.016Z" }, + { url = "https://files.pythonhosted.org/packages/78/e9/eb1dd17bd6de8289df3580e967e78294f352a5df8a57ff4671ee5fc3dcd0/coverage-7.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903", size = 250325, upload-time = "2026-02-09T12:56:40.668Z" }, + { url = "https://files.pythonhosted.org/packages/71/07/8c1542aa873728f72267c07278c5cc0ec91356daf974df21335ccdb46368/coverage-7.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f", size = 251913, upload-time = "2026-02-09T12:56:41.97Z" }, + { url = "https://files.pythonhosted.org/packages/74/d7/c62e2c5e4483a748e27868e4c32ad3daa9bdddbba58e1bc7a15e252baa74/coverage-7.13.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299", size = 249974, upload-time = "2026-02-09T12:56:43.323Z" }, + { url = "https://files.pythonhosted.org/packages/98/9f/4c5c015a6e98ced54efd0f5cf8d31b88e5504ecb6857585fc0161bb1e600/coverage-7.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505", size = 253741, upload-time = "2026-02-09T12:56:45.155Z" }, + { url = "https://files.pythonhosted.org/packages/bd/59/0f4eef89b9f0fcd9633b5d350016f54126ab49426a70ff4c4e87446cabdc/coverage-7.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6", size = 249695, upload-time = "2026-02-09T12:56:46.636Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2c/b7476f938deb07166f3eb281a385c262675d688ff4659ad56c6c6b8e2e70/coverage-7.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9", size = 250599, upload-time = "2026-02-09T12:56:48.13Z" }, + { url = "https://files.pythonhosted.org/packages/b8/34/c3420709d9846ee3785b9f2831b4d94f276f38884032dca1457fa83f7476/coverage-7.13.4-cp311-cp311-win32.whl", hash = "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9", size = 221780, upload-time = "2026-02-09T12:56:50.479Z" }, + { url = "https://files.pythonhosted.org/packages/61/08/3d9c8613079d2b11c185b865de9a4c1a68850cfda2b357fae365cf609f29/coverage-7.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f", size = 222715, upload-time = "2026-02-09T12:56:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/18/1a/54c3c80b2f056164cc0a6cdcb040733760c7c4be9d780fe655f356f433e4/coverage-7.13.4-cp311-cp311-win_arm64.whl", hash = "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f", size = 221385, upload-time = "2026-02-09T12:56:53.194Z" }, + { url = "https://files.pythonhosted.org/packages/d1/81/4ce2fdd909c5a0ed1f6dedb88aa57ab79b6d1fbd9b588c1ac7ef45659566/coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", size = 219449, upload-time = "2026-02-09T12:56:54.889Z" }, + { url = "https://files.pythonhosted.org/packages/5d/96/5238b1efc5922ddbdc9b0db9243152c09777804fb7c02ad1741eb18a11c0/coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", size = 219810, upload-time = "2026-02-09T12:56:56.33Z" }, + { url = "https://files.pythonhosted.org/packages/78/72/2f372b726d433c9c35e56377cf1d513b4c16fe51841060d826b95caacec1/coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", size = 251308, upload-time = "2026-02-09T12:56:57.858Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a0/2ea570925524ef4e00bb6c82649f5682a77fac5ab910a65c9284de422600/coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", size = 254052, upload-time = "2026-02-09T12:56:59.754Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ac/45dc2e19a1939098d783c846e130b8f862fbb50d09e0af663988f2f21973/coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", size = 255165, upload-time = "2026-02-09T12:57:01.287Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4d/26d236ff35abc3b5e63540d3386e4c3b192168c1d96da5cb2f43c640970f/coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", size = 257432, upload-time = "2026-02-09T12:57:02.637Z" }, + { url = "https://files.pythonhosted.org/packages/ec/55/14a966c757d1348b2e19caf699415a2a4c4f7feaa4bbc6326a51f5c7dd1b/coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", size = 251716, upload-time = "2026-02-09T12:57:04.056Z" }, + { url = "https://files.pythonhosted.org/packages/77/33/50116647905837c66d28b2af1321b845d5f5d19be9655cb84d4a0ea806b4/coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", size = 253089, upload-time = "2026-02-09T12:57:05.503Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/8efb11a46e3665d92635a56e4f2d4529de6d33f2cb38afd47d779d15fc99/coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", size = 251232, upload-time = "2026-02-09T12:57:06.879Z" }, + { url = "https://files.pythonhosted.org/packages/51/24/8cd73dd399b812cc76bb0ac260e671c4163093441847ffe058ac9fda1e32/coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", size = 255299, upload-time = "2026-02-09T12:57:08.245Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/0a4b12f1d0e029ce1ccc1c800944a9984cbe7d678e470bb6d3c6bc38a0da/coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", size = 250796, upload-time = "2026-02-09T12:57:10.142Z" }, + { url = "https://files.pythonhosted.org/packages/73/44/6002fbf88f6698ca034360ce474c406be6d5a985b3fdb3401128031eef6b/coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0", size = 252673, upload-time = "2026-02-09T12:57:12.197Z" }, + { url = "https://files.pythonhosted.org/packages/de/c6/a0279f7c00e786be75a749a5674e6fa267bcbd8209cd10c9a450c655dfa7/coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", size = 221990, upload-time = "2026-02-09T12:57:14.085Z" }, + { url = "https://files.pythonhosted.org/packages/77/4e/c0a25a425fcf5557d9abd18419c95b63922e897bc86c1f327f155ef234a9/coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", size = 222800, upload-time = "2026-02-09T12:57:15.944Z" }, + { url = "https://files.pythonhosted.org/packages/47/ac/92da44ad9a6f4e3a7debd178949d6f3769bedca33830ce9b1dcdab589a37/coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", size = 221415, upload-time = "2026-02-09T12:57:17.497Z" }, + { url = "https://files.pythonhosted.org/packages/db/23/aad45061a31677d68e47499197a131eea55da4875d16c1f42021ab963503/coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9", size = 219474, upload-time = "2026-02-09T12:57:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/a5/70/9b8b67a0945f3dfec1fd896c5cefb7c19d5a3a6d74630b99a895170999ae/coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac", size = 219844, upload-time = "2026-02-09T12:57:20.66Z" }, + { url = "https://files.pythonhosted.org/packages/97/fd/7e859f8fab324cef6c4ad7cff156ca7c489fef9179d5749b0c8d321281c2/coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea", size = 250832, upload-time = "2026-02-09T12:57:22.007Z" }, + { url = "https://files.pythonhosted.org/packages/e4/dc/b2442d10020c2f52617828862d8b6ee337859cd8f3a1f13d607dddda9cf7/coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b", size = 253434, upload-time = "2026-02-09T12:57:23.339Z" }, + { url = "https://files.pythonhosted.org/packages/5a/88/6728a7ad17428b18d836540630487231f5470fb82454871149502f5e5aa2/coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525", size = 254676, upload-time = "2026-02-09T12:57:24.774Z" }, + { url = "https://files.pythonhosted.org/packages/7c/bc/21244b1b8cedf0dff0a2b53b208015fe798d5f2a8d5348dbfece04224fff/coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242", size = 256807, upload-time = "2026-02-09T12:57:26.125Z" }, + { url = "https://files.pythonhosted.org/packages/97/a0/ddba7ed3251cff51006737a727d84e05b61517d1784a9988a846ba508877/coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148", size = 251058, upload-time = "2026-02-09T12:57:27.614Z" }, + { url = "https://files.pythonhosted.org/packages/9b/55/e289addf7ff54d3a540526f33751951bf0878f3809b47f6dfb3def69c6f7/coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a", size = 252805, upload-time = "2026-02-09T12:57:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/cc276b1fa4a59be56d96f1dabddbdc30f4ba22e3b1cd42504c37b3313255/coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23", size = 250766, upload-time = "2026-02-09T12:57:30.522Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/1093b8f93018f8b41a8cf29636c9292502f05e4a113d4d107d14a3acd044/coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80", size = 254923, upload-time = "2026-02-09T12:57:31.946Z" }, + { url = "https://files.pythonhosted.org/packages/8b/55/ea2796da2d42257f37dbea1aab239ba9263b31bd91d5527cdd6db5efe174/coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea", size = 250591, upload-time = "2026-02-09T12:57:33.842Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/7c4bb72aacf8af5020675aa633e59c1fbe296d22aed191b6a5b711eb2bc7/coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a", size = 252364, upload-time = "2026-02-09T12:57:35.743Z" }, + { url = "https://files.pythonhosted.org/packages/5c/38/a8d2ec0146479c20bbaa7181b5b455a0c41101eed57f10dd19a78ab44c80/coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d", size = 222010, upload-time = "2026-02-09T12:57:37.25Z" }, + { url = "https://files.pythonhosted.org/packages/e2/0c/dbfafbe90a185943dcfbc766fe0e1909f658811492d79b741523a414a6cc/coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd", size = 222818, upload-time = "2026-02-09T12:57:38.734Z" }, + { url = "https://files.pythonhosted.org/packages/04/d1/934918a138c932c90d78301f45f677fb05c39a3112b96fd2c8e60503cdc7/coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af", size = 221438, upload-time = "2026-02-09T12:57:40.223Z" }, + { url = "https://files.pythonhosted.org/packages/52/57/ee93ced533bcb3e6df961c0c6e42da2fc6addae53fb95b94a89b1e33ebd7/coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d", size = 220165, upload-time = "2026-02-09T12:57:41.639Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e0/969fc285a6fbdda49d91af278488d904dcd7651b2693872f0ff94e40e84a/coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12", size = 220516, upload-time = "2026-02-09T12:57:44.215Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b8/9531944e16267e2735a30a9641ff49671f07e8138ecf1ca13db9fd2560c7/coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b", size = 261804, upload-time = "2026-02-09T12:57:45.989Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f3/e63df6d500314a2a60390d1989240d5f27318a7a68fa30ad3806e2a9323e/coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9", size = 263885, upload-time = "2026-02-09T12:57:47.42Z" }, + { url = "https://files.pythonhosted.org/packages/f3/67/7654810de580e14b37670b60a09c599fa348e48312db5b216d730857ffe6/coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092", size = 266308, upload-time = "2026-02-09T12:57:49.345Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/39d41eca0eab3cc82115953ad41c4e77935286c930e8fad15eaed1389d83/coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9", size = 267452, upload-time = "2026-02-09T12:57:50.811Z" }, + { url = "https://files.pythonhosted.org/packages/50/6d/39c0fbb8fc5cd4d2090811e553c2108cf5112e882f82505ee7495349a6bf/coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26", size = 261057, upload-time = "2026-02-09T12:57:52.447Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a2/60010c669df5fa603bb5a97fb75407e191a846510da70ac657eb696b7fce/coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2", size = 263875, upload-time = "2026-02-09T12:57:53.938Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d9/63b22a6bdbd17f1f96e9ed58604c2a6b0e72a9133e37d663bef185877cf6/coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940", size = 261500, upload-time = "2026-02-09T12:57:56.012Z" }, + { url = "https://files.pythonhosted.org/packages/70/bf/69f86ba1ad85bc3ad240e4c0e57a2e620fbc0e1645a47b5c62f0e941ad7f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c", size = 265212, upload-time = "2026-02-09T12:57:57.5Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f2/5f65a278a8c2148731831574c73e42f57204243d33bedaaf18fa79c5958f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0", size = 260398, upload-time = "2026-02-09T12:57:59.027Z" }, + { url = "https://files.pythonhosted.org/packages/ef/80/6e8280a350ee9fea92f14b8357448a242dcaa243cb2c72ab0ca591f66c8c/coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b", size = 262584, upload-time = "2026-02-09T12:58:01.129Z" }, + { url = "https://files.pythonhosted.org/packages/22/63/01ff182fc95f260b539590fb12c11ad3e21332c15f9799cb5e2386f71d9f/coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9", size = 222688, upload-time = "2026-02-09T12:58:02.736Z" }, + { url = "https://files.pythonhosted.org/packages/a9/43/89de4ef5d3cd53b886afa114065f7e9d3707bdb3e5efae13535b46ae483d/coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd", size = 223746, upload-time = "2026-02-09T12:58:05.362Z" }, + { url = "https://files.pythonhosted.org/packages/35/39/7cf0aa9a10d470a5309b38b289b9bb07ddeac5d61af9b664fe9775a4cb3e/coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997", size = 222003, upload-time = "2026-02-09T12:58:06.952Z" }, + { url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522, upload-time = "2026-02-09T12:58:08.623Z" }, + { url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855, upload-time = "2026-02-09T12:58:10.176Z" }, + { url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887, upload-time = "2026-02-09T12:58:12.503Z" }, + { url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396, upload-time = "2026-02-09T12:58:14.615Z" }, + { url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745, upload-time = "2026-02-09T12:58:16.162Z" }, + { url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055, upload-time = "2026-02-09T12:58:17.892Z" }, + { url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911, upload-time = "2026-02-09T12:58:19.495Z" }, + { url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754, upload-time = "2026-02-09T12:58:21.064Z" }, + { url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720, upload-time = "2026-02-09T12:58:22.622Z" }, + { url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994, upload-time = "2026-02-09T12:58:24.548Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531, upload-time = "2026-02-09T12:58:26.271Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189, upload-time = "2026-02-09T12:58:27.807Z" }, + { url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258, upload-time = "2026-02-09T12:58:29.441Z" }, + { url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073, upload-time = "2026-02-09T12:58:31.026Z" }, + { url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638, upload-time = "2026-02-09T12:58:32.599Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246, upload-time = "2026-02-09T12:58:34.181Z" }, + { url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514, upload-time = "2026-02-09T12:58:35.704Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877, upload-time = "2026-02-09T12:58:37.864Z" }, + { url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004, upload-time = "2026-02-09T12:58:39.492Z" }, + { url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408, upload-time = "2026-02-09T12:58:41.852Z" }, + { url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544, upload-time = "2026-02-09T12:58:44.093Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980, upload-time = "2026-02-09T12:58:45.721Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871, upload-time = "2026-02-09T12:58:47.334Z" }, + { url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472, upload-time = "2026-02-09T12:58:48.995Z" }, + { url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210, upload-time = "2026-02-09T12:58:51.178Z" }, + { url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319, upload-time = "2026-02-09T12:58:53.081Z" }, + { url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638, upload-time = "2026-02-09T12:58:55.258Z" }, + { url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040, upload-time = "2026-02-09T12:58:56.936Z" }, + { url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148, upload-time = "2026-02-09T12:58:58.645Z" }, + { url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172, upload-time = "2026-02-09T12:59:00.396Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" }, ] [package.optional-dependencies] @@ -736,38 +723,38 @@ wheels = [ [[package]] name = "duckdb" -version = "1.4.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7f/da/17c3eb5458af69d54dedc8d18e4a32ceaa8ce4d4c699d45d6d8287e790c3/duckdb-1.4.3.tar.gz", hash = "sha256:fea43e03604c713e25a25211ada87d30cd2a044d8f27afab5deba26ac49e5268", size = 18478418, upload-time = "2025-12-09T10:59:22.945Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/bc/7c5e50e440c8629495678bc57bdfc1bb8e62f61090f2d5441e2bd0a0ed96/duckdb-1.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:366bf607088053dce845c9d24c202c04d78022436cc5d8e4c9f0492de04afbe7", size = 29019361, upload-time = "2025-12-09T10:57:59.845Z" }, - { url = "https://files.pythonhosted.org/packages/26/15/c04a4faf0dfddad2259cab72bf0bd4b3d010f2347642541bd254d516bf93/duckdb-1.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8d080e8d1bf2d226423ec781f539c8f6b6ef3fd42a9a58a7160de0a00877a21f", size = 15407465, upload-time = "2025-12-09T10:58:02.465Z" }, - { url = "https://files.pythonhosted.org/packages/cb/54/a049490187c9529932fc153f7e1b92a9e145586281fe4e03ce0535a0497c/duckdb-1.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9dc049ba7e906cb49ca2b6d4fbf7b6615ec3883193e8abb93f0bef2652e42dda", size = 13735781, upload-time = "2025-12-09T10:58:04.847Z" }, - { url = "https://files.pythonhosted.org/packages/14/b7/ee594dcecbc9469ec3cd1fb1f81cb5fa289ab444b80cfb5640c8f467f75f/duckdb-1.4.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b30245375ea94ab528c87c61fc3ab3e36331180b16af92ee3a37b810a745d24", size = 18470729, upload-time = "2025-12-09T10:58:07.116Z" }, - { url = "https://files.pythonhosted.org/packages/df/5f/a6c1862ed8a96d8d930feb6af5e55aadd983310aab75142468c2cb32a2a3/duckdb-1.4.3-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7c864df027da1ee95f0c32def67e15d02cd4a906c9c1cbae82c09c5112f526b", size = 20471399, upload-time = "2025-12-09T10:58:09.714Z" }, - { url = "https://files.pythonhosted.org/packages/5b/80/c05c0b6a6107b618927b7dcabe3bba6a7eecd951f25c9dbcd9c1f9577cc8/duckdb-1.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:813f189039b46877b5517f1909c7b94a8fe01b4bde2640ab217537ea0fe9b59b", size = 12329359, upload-time = "2025-12-09T10:58:12.147Z" }, - { url = "https://files.pythonhosted.org/packages/b0/83/9d8fc3413f854effa680dcad1781f68f3ada8679863c0c94ba3b36bae6ff/duckdb-1.4.3-cp311-cp311-win_arm64.whl", hash = "sha256:fbc63ffdd03835f660155b37a1b6db2005bcd46e5ad398b8cac141eb305d2a3d", size = 13070898, upload-time = "2025-12-09T10:58:14.301Z" }, - { url = "https://files.pythonhosted.org/packages/5a/d7/fdc2139b94297fc5659110a38adde293d025e320673ae5e472b95d323c50/duckdb-1.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:6302452e57aef29aae3977063810ed7b2927967b97912947b9cca45c1c21955f", size = 29033112, upload-time = "2025-12-09T10:58:16.52Z" }, - { url = "https://files.pythonhosted.org/packages/eb/d9/ca93df1ce19aef8f799e3aaacf754a4dde7e9169c0b333557752d21d076a/duckdb-1.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:deab351ac43b6282a3270e3d40e3d57b3b50f472d9fd8c30975d88a31be41231", size = 15414646, upload-time = "2025-12-09T10:58:19.36Z" }, - { url = "https://files.pythonhosted.org/packages/16/90/9f2748e740f5fc05b739e7c5c25aab6ab4363e5da4c3c70419c7121dc806/duckdb-1.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5634e40e1e2d972e4f75bced1fbdd9e9e90faa26445c1052b27de97ee546944a", size = 13740477, upload-time = "2025-12-09T10:58:21.778Z" }, - { url = "https://files.pythonhosted.org/packages/5f/ec/279723615b4fb454efd823b7efe97cf2504569e2e74d15defbbd6b027901/duckdb-1.4.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:274d4a31aba63115f23e7e7b401e3e3a937f3626dc9dea820a9c7d3073f450d2", size = 18483715, upload-time = "2025-12-09T10:58:24.346Z" }, - { url = "https://files.pythonhosted.org/packages/10/63/af20cd20fd7fd6565ea5a1578c16157b6a6e07923e459a6f9b0dc9ada308/duckdb-1.4.3-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f868a7e6d9b37274a1aa34849ea92aa964e9bd59a5237d6c17e8540533a1e4f", size = 20495188, upload-time = "2025-12-09T10:58:26.806Z" }, - { url = "https://files.pythonhosted.org/packages/8c/ab/0acb4b64afb2cc6c1d458a391c64e36be40137460f176c04686c965ce0e0/duckdb-1.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:ef7ef15347ce97201b1b5182a5697682679b04c3374d5a01ac10ba31cf791b95", size = 12335622, upload-time = "2025-12-09T10:58:29.707Z" }, - { url = "https://files.pythonhosted.org/packages/50/d5/2a795745f6597a5e65770141da6efdc4fd754e5ee6d652f74bcb7f9c7759/duckdb-1.4.3-cp312-cp312-win_arm64.whl", hash = "sha256:1b9b445970fd18274d5ac07a0b24c032e228f967332fb5ebab3d7db27738c0e4", size = 13075834, upload-time = "2025-12-09T10:58:32.036Z" }, - { url = "https://files.pythonhosted.org/packages/fd/76/288cca43a10ddd082788e1a71f1dc68d9130b5d078c3ffd0edf2f3a8719f/duckdb-1.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16952ac05bd7e7b39946695452bf450db1ebbe387e1e7178e10f593f2ea7b9a8", size = 29033392, upload-time = "2025-12-09T10:58:34.631Z" }, - { url = "https://files.pythonhosted.org/packages/64/07/cbad3d3da24af4d1add9bccb5fb390fac726ffa0c0cebd29bf5591cef334/duckdb-1.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de984cd24a6cbefdd6d4a349f7b9a46e583ca3e58ce10d8def0b20a6e5fcbe78", size = 15414567, upload-time = "2025-12-09T10:58:37.051Z" }, - { url = "https://files.pythonhosted.org/packages/c4/19/57af0cc66ba2ffb8900f567c9aec188c6ab2a7b3f2260e9c6c3c5f9b57b1/duckdb-1.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e5457dda91b67258aae30fb1a0df84183a9f6cd27abac1d5536c0d876c6dfa1", size = 13740960, upload-time = "2025-12-09T10:58:39.658Z" }, - { url = "https://files.pythonhosted.org/packages/73/dd/23152458cf5fd51e813fadda60b9b5f011517634aa4bb9301f5f3aa951d8/duckdb-1.4.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:006aca6a6d6736c441b02ff5c7600b099bb8b7f4de094b8b062137efddce42df", size = 18484312, upload-time = "2025-12-09T10:58:42.054Z" }, - { url = "https://files.pythonhosted.org/packages/1a/7b/adf3f611f11997fc429d4b00a730604b65d952417f36a10c4be6e38e064d/duckdb-1.4.3-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2813f4635f4d6681cc3304020374c46aca82758c6740d7edbc237fe3aae2744", size = 20495571, upload-time = "2025-12-09T10:58:44.646Z" }, - { url = "https://files.pythonhosted.org/packages/40/d5/6b7ddda7713a788ab2d622c7267ec317718f2bdc746ce1fca49b7ff0e50f/duckdb-1.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:6db124f53a3edcb32b0a896ad3519e37477f7e67bf4811cb41ab60c1ef74e4c8", size = 12335680, upload-time = "2025-12-09T10:58:46.883Z" }, - { url = "https://files.pythonhosted.org/packages/e8/28/0670135cf54525081fded9bac1254f78984e3b96a6059cd15aca262e3430/duckdb-1.4.3-cp313-cp313-win_arm64.whl", hash = "sha256:a8b0a8764e1b5dd043d168c8f749314f7a1252b5a260fa415adaa26fa3b958fd", size = 13075161, upload-time = "2025-12-09T10:58:49.47Z" }, - { url = "https://files.pythonhosted.org/packages/b6/f4/a38651e478fa41eeb8e43a0a9c0d4cd8633adea856e3ac5ac95124b0fdbf/duckdb-1.4.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:316711a9e852bcfe1ed6241a5f654983f67e909e290495f3562cccdf43be8180", size = 29042272, upload-time = "2025-12-09T10:58:51.826Z" }, - { url = "https://files.pythonhosted.org/packages/16/de/2cf171a66098ce5aeeb7371511bd2b3d7b73a2090603b0b9df39f8aaf814/duckdb-1.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9e625b2b4d52bafa1fd0ebdb0990c3961dac8bb00e30d327185de95b68202131", size = 15419343, upload-time = "2025-12-09T10:58:54.439Z" }, - { url = "https://files.pythonhosted.org/packages/35/28/6b0a7830828d4e9a37420d87e80fe6171d2869a9d3d960bf5d7c3b8c7ee4/duckdb-1.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:130c6760f6c573f9c9fe9aba56adba0fab48811a4871b7b8fd667318b4a3e8da", size = 13748905, upload-time = "2025-12-09T10:58:56.656Z" }, - { url = "https://files.pythonhosted.org/packages/15/4d/778628e194d63967870873b9581c8a6b4626974aa4fbe09f32708a2d3d3a/duckdb-1.4.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20c88effaa557a11267706b01419c542fe42f893dee66e5a6daa5974ea2d4a46", size = 18487261, upload-time = "2025-12-09T10:58:58.866Z" }, - { url = "https://files.pythonhosted.org/packages/c6/5f/87e43af2e4a0135f9675449563e7c2f9b6f1fe6a2d1691c96b091f3904dd/duckdb-1.4.3-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1b35491db98ccd11d151165497c084a9d29d3dc42fc80abea2715a6c861ca43d", size = 20497138, upload-time = "2025-12-09T10:59:01.241Z" }, - { url = "https://files.pythonhosted.org/packages/94/41/abec537cc7c519121a2a83b9a6f180af8915fabb433777dc147744513e74/duckdb-1.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:23b12854032c1a58d0452e2b212afa908d4ce64171862f3792ba9a596ba7c765", size = 12836056, upload-time = "2025-12-09T10:59:03.388Z" }, - { url = "https://files.pythonhosted.org/packages/b1/5a/8af5b96ce5622b6168854f479ce846cf7fb589813dcc7d8724233c37ded3/duckdb-1.4.3-cp314-cp314-win_arm64.whl", hash = "sha256:90f241f25cffe7241bf9f376754a5845c74775e00e1c5731119dc88cd71e0cb2", size = 13527759, upload-time = "2025-12-09T10:59:05.496Z" }, +version = "1.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/9d/ab66a06e416d71b7bdcb9904cdf8d4db3379ef632bb8e9495646702d9718/duckdb-1.4.4.tar.gz", hash = "sha256:8bba52fd2acb67668a4615ee17ee51814124223de836d9e2fdcbc4c9021b3d3c", size = 18419763, upload-time = "2026-01-26T11:50:37.68Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/68/19233412033a2bc5a144a3f531f64e3548d4487251e3f16b56c31411a06f/duckdb-1.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5ba684f498d4e924c7e8f30dd157da8da34c8479746c5011b6c0e037e9c60ad2", size = 28883816, upload-time = "2026-01-26T11:49:01.009Z" }, + { url = "https://files.pythonhosted.org/packages/b3/3e/cec70e546c298ab76d80b990109e111068d82cca67942c42328eaa7d6fdb/duckdb-1.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5536eb952a8aa6ae56469362e344d4e6403cc945a80bc8c5c2ebdd85d85eb64b", size = 15339662, upload-time = "2026-01-26T11:49:04.058Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f0/cf4241a040ec4f571859a738007ec773b642fbc27df4cbcf34b0c32ea559/duckdb-1.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:47dd4162da6a2be59a0aef640eb08d6360df1cf83c317dcc127836daaf3b7f7c", size = 13670044, upload-time = "2026-01-26T11:49:06.627Z" }, + { url = "https://files.pythonhosted.org/packages/11/64/de2bb4ec1e35ec9ebf6090a95b930fc56934a0ad6f34a24c5972a14a77ef/duckdb-1.4.4-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6cb357cfa3403910e79e2eb46c8e445bb1ee2fd62e9e9588c6b999df4256abc1", size = 18409951, upload-time = "2026-01-26T11:49:09.808Z" }, + { url = "https://files.pythonhosted.org/packages/79/a2/ac0f5ee16df890d141304bcd48733516b7202c0de34cd3555634d6eb4551/duckdb-1.4.4-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c25d5b0febda02b7944e94fdae95aecf952797afc8cb920f677b46a7c251955", size = 20411739, upload-time = "2026-01-26T11:49:12.652Z" }, + { url = "https://files.pythonhosted.org/packages/37/a2/9a3402edeedaecf72de05fe9ff7f0303d701b8dfc136aea4a4be1a5f7eee/duckdb-1.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:6703dd1bb650025b3771552333d305d62ddd7ff182de121483d4e042ea6e2e00", size = 12256972, upload-time = "2026-01-26T11:49:15.468Z" }, + { url = "https://files.pythonhosted.org/packages/f6/e6/052ea6dcdf35b259fd182eff3efd8d75a071de4010c9807556098df137b9/duckdb-1.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:bf138201f56e5d6fc276a25138341b3523e2f84733613fc43f02c54465619a95", size = 13006696, upload-time = "2026-01-26T11:49:18.054Z" }, + { url = "https://files.pythonhosted.org/packages/58/33/beadaa69f8458afe466126f2c5ee48c4759cc9d5d784f8703d44e0b52c3c/duckdb-1.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ddcfd9c6ff234da603a1edd5fd8ae6107f4d042f74951b65f91bc5e2643856b3", size = 28896535, upload-time = "2026-01-26T11:49:21.232Z" }, + { url = "https://files.pythonhosted.org/packages/76/66/82413f386df10467affc87f65bac095b7c88dbd9c767584164d5f4dc4cb8/duckdb-1.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6792ca647216bd5c4ff16396e4591cfa9b4a72e5ad7cdd312cec6d67e8431a7c", size = 15349716, upload-time = "2026-01-26T11:49:23.989Z" }, + { url = "https://files.pythonhosted.org/packages/5d/8c/c13d396fd4e9bf970916dc5b4fea410c1b10fe531069aea65f1dcf849a71/duckdb-1.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1f8d55843cc940e36261689054f7dfb6ce35b1f5b0953b0d355b6adb654b0d52", size = 13672403, upload-time = "2026-01-26T11:49:26.741Z" }, + { url = "https://files.pythonhosted.org/packages/db/77/2446a0b44226bb95217748d911c7ca66a66ca10f6481d5178d9370819631/duckdb-1.4.4-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c65d15c440c31e06baaebfd2c06d71ce877e132779d309f1edf0a85d23c07e92", size = 18419001, upload-time = "2026-01-26T11:49:29.353Z" }, + { url = "https://files.pythonhosted.org/packages/2e/a3/97715bba30040572fb15d02c26f36be988d48bc00501e7ac02b1d65ef9d0/duckdb-1.4.4-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b297eff642503fd435a9de5a9cb7db4eccb6f61d61a55b30d2636023f149855f", size = 20437385, upload-time = "2026-01-26T11:49:32.302Z" }, + { url = "https://files.pythonhosted.org/packages/8b/0a/18b9167adf528cbe3867ef8a84a5f19f37bedccb606a8a9e59cfea1880c8/duckdb-1.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:d525de5f282b03aa8be6db86b1abffdceae5f1055113a03d5b50cd2fb8cf2ef8", size = 12267343, upload-time = "2026-01-26T11:49:34.985Z" }, + { url = "https://files.pythonhosted.org/packages/f8/15/37af97f5717818f3d82d57414299c293b321ac83e048c0a90bb8b6a09072/duckdb-1.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:50f2eb173c573811b44aba51176da7a4e5c487113982be6a6a1c37337ec5fa57", size = 13007490, upload-time = "2026-01-26T11:49:37.413Z" }, + { url = "https://files.pythonhosted.org/packages/7f/fe/64810fee20030f2bf96ce28b527060564864ce5b934b50888eda2cbf99dd/duckdb-1.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:337f8b24e89bc2e12dadcfe87b4eb1c00fd920f68ab07bc9b70960d6523b8bc3", size = 28899349, upload-time = "2026-01-26T11:49:40.294Z" }, + { url = "https://files.pythonhosted.org/packages/9c/9b/3c7c5e48456b69365d952ac201666053de2700f5b0144a699a4dc6854507/duckdb-1.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0509b39ea7af8cff0198a99d206dca753c62844adab54e545984c2e2c1381616", size = 15350691, upload-time = "2026-01-26T11:49:43.242Z" }, + { url = "https://files.pythonhosted.org/packages/a6/7b/64e68a7b857ed0340045501535a0da99ea5d9d5ea3708fec0afb8663eb27/duckdb-1.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fb94de6d023de9d79b7edc1ae07ee1d0b4f5fa8a9dcec799650b5befdf7aafec", size = 13672311, upload-time = "2026-01-26T11:49:46.069Z" }, + { url = "https://files.pythonhosted.org/packages/09/5b/3e7aa490841784d223de61beb2ae64e82331501bf5a415dc87a0e27b4663/duckdb-1.4.4-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d636ceda422e7babd5e2f7275f6a0d1a3405e6a01873f00d38b72118d30c10b", size = 18422740, upload-time = "2026-01-26T11:49:49.034Z" }, + { url = "https://files.pythonhosted.org/packages/53/32/256df3dbaa198c58539ad94f9a41e98c2c8ff23f126b8f5f52c7dcd0a738/duckdb-1.4.4-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7df7351328ffb812a4a289732f500d621e7de9942a3a2c9b6d4afcf4c0e72526", size = 20435578, upload-time = "2026-01-26T11:49:51.946Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f0/620323fd87062ea43e527a2d5ed9e55b525e0847c17d3b307094ddab98a2/duckdb-1.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:6fb1225a9ea5877421481d59a6c556a9532c32c16c7ae6ca8d127e2b878c9389", size = 12268083, upload-time = "2026-01-26T11:49:54.615Z" }, + { url = "https://files.pythonhosted.org/packages/e5/07/a397fdb7c95388ba9c055b9a3d38dfee92093f4427bc6946cf9543b1d216/duckdb-1.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:f28a18cc790217e5b347bb91b2cab27aafc557c58d3d8382e04b4fe55d0c3f66", size = 13006123, upload-time = "2026-01-26T11:49:57.092Z" }, + { url = "https://files.pythonhosted.org/packages/97/a6/f19e2864e651b0bd8e4db2b0c455e7e0d71e0d4cd2cd9cc052f518e43eb3/duckdb-1.4.4-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:25874f8b1355e96178079e37312c3ba6d61a2354f51319dae860cf21335c3a20", size = 28909554, upload-time = "2026-01-26T11:50:00.107Z" }, + { url = "https://files.pythonhosted.org/packages/0e/93/8a24e932c67414fd2c45bed83218e62b73348996bf859eda020c224774b2/duckdb-1.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:452c5b5d6c349dc5d1154eb2062ee547296fcbd0c20e9df1ed00b5e1809089da", size = 15353804, upload-time = "2026-01-26T11:50:03.382Z" }, + { url = "https://files.pythonhosted.org/packages/62/13/e5378ff5bb1d4397655d840b34b642b1b23cdd82ae19599e62dc4b9461c9/duckdb-1.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8e5c2d8a0452df55e092959c0bfc8ab8897ac3ea0f754cb3b0ab3e165cd79aff", size = 13676157, upload-time = "2026-01-26T11:50:06.232Z" }, + { url = "https://files.pythonhosted.org/packages/2d/94/24364da564b27aeebe44481f15bd0197a0b535ec93f188a6b1b98c22f082/duckdb-1.4.4-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1af6e76fe8bd24875dc56dd8e38300d64dc708cd2e772f67b9fbc635cc3066a3", size = 18426882, upload-time = "2026-01-26T11:50:08.97Z" }, + { url = "https://files.pythonhosted.org/packages/26/0a/6ae31b2914b4dc34243279b2301554bcbc5f1a09ccc82600486c49ab71d1/duckdb-1.4.4-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0440f59e0cd9936a9ebfcf7a13312eda480c79214ffed3878d75947fc3b7d6d", size = 20435641, upload-time = "2026-01-26T11:50:12.188Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b1/fd5c37c53d45efe979f67e9bd49aaceef640147bb18f0699a19edd1874d6/duckdb-1.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:59c8d76016dde854beab844935b1ec31de358d4053e792988108e995b18c08e7", size = 12762360, upload-time = "2026-01-26T11:50:14.76Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2d/13e6024e613679d8a489dd922f199ef4b1d08a456a58eadd96dc2f05171f/duckdb-1.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:53cd6423136ab44383ec9955aefe7599b3fb3dd1fe006161e6396d8167e0e0d4", size = 13458633, upload-time = "2026-01-26T11:50:17.657Z" }, ] [[package]] @@ -781,7 +768,7 @@ wheels = [ [[package]] name = "factorium" -version = "0.3.2" +version = "0.4.0" source = { editable = "." } dependencies = [ { name = "aiofiles" }, @@ -798,12 +785,6 @@ dependencies = [ ] [package.optional-dependencies] -dev = [ - { name = "mypy" }, - { name = "pytest" }, - { name = "pytest-cov" }, - { name = "ruff" }, -] jupyter = [ { name = "nest-asyncio" }, ] @@ -826,7 +807,6 @@ requires-dist = [ { name = "aiohttp", specifier = ">=3.9.0" }, { name = "duckdb", specifier = ">=1.0.0" }, { name = "matplotlib", specifier = ">=3.7.0" }, - { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.0.0" }, { name = "nest-asyncio", marker = "extra == 'jupyter'", specifier = ">=1.5.0" }, { name = "numba", specifier = ">=0.57.0" }, { name = "numpy", specifier = ">=1.24.0" }, @@ -834,12 +814,9 @@ requires-dist = [ { name = "polars", specifier = ">=1.0.0" }, { name = "pyarrow", specifier = ">=14.0.0" }, { name = "pyparsing", specifier = ">=3.0.0" }, - { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" }, - { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.0.0" }, - { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1.0" }, { name = "scipy", specifier = ">=1.10.0" }, ] -provides-extras = ["jupyter", "dev"] +provides-extras = ["jupyter"] [package.metadata.requires-dev] dev = [ @@ -1198,15 +1175,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] -[[package]] -name = "jmespath" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, -] - [[package]] name = "json5" version = "0.13.0" @@ -1520,65 +1488,75 @@ wheels = [ [[package]] name = "librt" -version = "0.7.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/93/e4/b59bdf1197fdf9888452ea4d2048cdad61aef85eb83e99dc52551d7fdc04/librt-0.7.4.tar.gz", hash = "sha256:3871af56c59864d5fd21d1ac001eb2fb3b140d52ba0454720f2e4a19812404ba", size = 145862, upload-time = "2025-12-15T16:52:43.862Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/64/44089b12d8b4714a7f0e2f33fb19285ba87702d4be0829f20b36ebeeee07/librt-0.7.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3485b9bb7dfa66167d5500ffdafdc35415b45f0da06c75eb7df131f3357b174a", size = 54709, upload-time = "2025-12-15T16:51:16.699Z" }, - { url = "https://files.pythonhosted.org/packages/26/ef/6fa39fb5f37002f7d25e0da4f24d41b457582beea9369eeb7e9e73db5508/librt-0.7.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:188b4b1a770f7f95ea035d5bbb9d7367248fc9d12321deef78a269ebf46a5729", size = 56663, upload-time = "2025-12-15T16:51:17.856Z" }, - { url = "https://files.pythonhosted.org/packages/9d/e4/cbaca170a13bee2469c90df9e47108610b4422c453aea1aec1779ac36c24/librt-0.7.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1b668b1c840183e4e38ed5a99f62fac44c3a3eef16870f7f17cfdfb8b47550ed", size = 161703, upload-time = "2025-12-15T16:51:19.421Z" }, - { url = "https://files.pythonhosted.org/packages/d0/32/0b2296f9cc7e693ab0d0835e355863512e5eac90450c412777bd699c76ae/librt-0.7.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0e8f864b521f6cfedb314d171630f827efee08f5c3462bcbc2244ab8e1768cd6", size = 171027, upload-time = "2025-12-15T16:51:20.721Z" }, - { url = "https://files.pythonhosted.org/packages/d8/33/c70b6d40f7342716e5f1353c8da92d9e32708a18cbfa44897a93ec2bf879/librt-0.7.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4df7c9def4fc619a9c2ab402d73a0c5b53899abe090e0100323b13ccb5a3dd82", size = 184700, upload-time = "2025-12-15T16:51:22.272Z" }, - { url = "https://files.pythonhosted.org/packages/e4/c8/555c405155da210e4c4113a879d378f54f850dbc7b794e847750a8fadd43/librt-0.7.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f79bc3595b6ed159a1bf0cdc70ed6ebec393a874565cab7088a219cca14da727", size = 180719, upload-time = "2025-12-15T16:51:23.561Z" }, - { url = "https://files.pythonhosted.org/packages/6b/88/34dc1f1461c5613d1b73f0ecafc5316cc50adcc1b334435985b752ed53e5/librt-0.7.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:77772a4b8b5f77d47d883846928c36d730b6e612a6388c74cba33ad9eb149c11", size = 174535, upload-time = "2025-12-15T16:51:25.031Z" }, - { url = "https://files.pythonhosted.org/packages/b6/5a/f3fafe80a221626bcedfa9fe5abbf5f04070989d44782f579b2d5920d6d0/librt-0.7.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:064a286e6ab0b4c900e228ab4fa9cb3811b4b83d3e0cc5cd816b2d0f548cb61c", size = 195236, upload-time = "2025-12-15T16:51:26.328Z" }, - { url = "https://files.pythonhosted.org/packages/d8/77/5c048d471ce17f4c3a6e08419be19add4d291e2f7067b877437d482622ac/librt-0.7.4-cp311-cp311-win32.whl", hash = "sha256:42da201c47c77b6cc91fc17e0e2b330154428d35d6024f3278aa2683e7e2daf2", size = 42930, upload-time = "2025-12-15T16:51:27.853Z" }, - { url = "https://files.pythonhosted.org/packages/fb/3b/514a86305a12c3d9eac03e424b07cd312c7343a9f8a52719aa079590a552/librt-0.7.4-cp311-cp311-win_amd64.whl", hash = "sha256:d31acb5886c16ae1711741f22504195af46edec8315fe69b77e477682a87a83e", size = 49240, upload-time = "2025-12-15T16:51:29.037Z" }, - { url = "https://files.pythonhosted.org/packages/ba/01/3b7b1914f565926b780a734fac6e9a4d2c7aefe41f4e89357d73697a9457/librt-0.7.4-cp311-cp311-win_arm64.whl", hash = "sha256:114722f35093da080a333b3834fff04ef43147577ed99dd4db574b03a5f7d170", size = 42613, upload-time = "2025-12-15T16:51:30.194Z" }, - { url = "https://files.pythonhosted.org/packages/f3/e7/b805d868d21f425b7e76a0ea71a2700290f2266a4f3c8357fcf73efc36aa/librt-0.7.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7dd3b5c37e0fb6666c27cf4e2c88ae43da904f2155c4cfc1e5a2fdce3b9fcf92", size = 55688, upload-time = "2025-12-15T16:51:31.571Z" }, - { url = "https://files.pythonhosted.org/packages/59/5e/69a2b02e62a14cfd5bfd9f1e9adea294d5bcfeea219c7555730e5d068ee4/librt-0.7.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9c5de1928c486201b23ed0cc4ac92e6e07be5cd7f3abc57c88a9cf4f0f32108", size = 57141, upload-time = "2025-12-15T16:51:32.714Z" }, - { url = "https://files.pythonhosted.org/packages/6e/6b/05dba608aae1272b8ea5ff8ef12c47a4a099a04d1e00e28a94687261d403/librt-0.7.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:078ae52ffb3f036396cc4aed558e5b61faedd504a3c1f62b8ae34bf95ae39d94", size = 165322, upload-time = "2025-12-15T16:51:33.986Z" }, - { url = "https://files.pythonhosted.org/packages/8f/bc/199533d3fc04a4cda8d7776ee0d79955ab0c64c79ca079366fbc2617e680/librt-0.7.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce58420e25097b2fc201aef9b9f6d65df1eb8438e51154e1a7feb8847e4a55ab", size = 174216, upload-time = "2025-12-15T16:51:35.384Z" }, - { url = "https://files.pythonhosted.org/packages/62/ec/09239b912a45a8ed117cb4a6616d9ff508f5d3131bd84329bf2f8d6564f1/librt-0.7.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b719c8730c02a606dc0e8413287e8e94ac2d32a51153b300baf1f62347858fba", size = 189005, upload-time = "2025-12-15T16:51:36.687Z" }, - { url = "https://files.pythonhosted.org/packages/46/2e/e188313d54c02f5b0580dd31476bb4b0177514ff8d2be9f58d4a6dc3a7ba/librt-0.7.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3749ef74c170809e6dee68addec9d2458700a8de703de081c888e92a8b015cf9", size = 183960, upload-time = "2025-12-15T16:51:37.977Z" }, - { url = "https://files.pythonhosted.org/packages/eb/84/f1d568d254518463d879161d3737b784137d236075215e56c7c9be191cee/librt-0.7.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b35c63f557653c05b5b1b6559a074dbabe0afee28ee2a05b6c9ba21ad0d16a74", size = 177609, upload-time = "2025-12-15T16:51:40.584Z" }, - { url = "https://files.pythonhosted.org/packages/5d/43/060bbc1c002f0d757c33a1afe6bf6a565f947a04841139508fc7cef6c08b/librt-0.7.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1ef704e01cb6ad39ad7af668d51677557ca7e5d377663286f0ee1b6b27c28e5f", size = 199269, upload-time = "2025-12-15T16:51:41.879Z" }, - { url = "https://files.pythonhosted.org/packages/ff/7f/708f8f02d8012ee9f366c07ea6a92882f48bd06cc1ff16a35e13d0fbfb08/librt-0.7.4-cp312-cp312-win32.whl", hash = "sha256:c66c2b245926ec15188aead25d395091cb5c9df008d3b3207268cd65557d6286", size = 43186, upload-time = "2025-12-15T16:51:43.149Z" }, - { url = "https://files.pythonhosted.org/packages/f1/a5/4e051b061c8b2509be31b2c7ad4682090502c0a8b6406edcf8c6b4fe1ef7/librt-0.7.4-cp312-cp312-win_amd64.whl", hash = "sha256:71a56f4671f7ff723451f26a6131754d7c1809e04e22ebfbac1db8c9e6767a20", size = 49455, upload-time = "2025-12-15T16:51:44.336Z" }, - { url = "https://files.pythonhosted.org/packages/d0/d2/90d84e9f919224a3c1f393af1636d8638f54925fdc6cd5ee47f1548461e5/librt-0.7.4-cp312-cp312-win_arm64.whl", hash = "sha256:419eea245e7ec0fe664eb7e85e7ff97dcdb2513ca4f6b45a8ec4a3346904f95a", size = 42828, upload-time = "2025-12-15T16:51:45.498Z" }, - { url = "https://files.pythonhosted.org/packages/fe/4d/46a53ccfbb39fd0b493fd4496eb76f3ebc15bb3e45d8c2e695a27587edf5/librt-0.7.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d44a1b1ba44cbd2fc3cb77992bef6d6fdb1028849824e1dd5e4d746e1f7f7f0b", size = 55745, upload-time = "2025-12-15T16:51:46.636Z" }, - { url = "https://files.pythonhosted.org/packages/7f/2b/3ac7f5212b1828bf4f979cf87f547db948d3e28421d7a430d4db23346ce4/librt-0.7.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c9cab4b3de1f55e6c30a84c8cee20e4d3b2476f4d547256694a1b0163da4fe32", size = 57166, upload-time = "2025-12-15T16:51:48.219Z" }, - { url = "https://files.pythonhosted.org/packages/e8/99/6523509097cbe25f363795f0c0d1c6a3746e30c2994e25b5aefdab119b21/librt-0.7.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2857c875f1edd1feef3c371fbf830a61b632fb4d1e57160bb1e6a3206e6abe67", size = 165833, upload-time = "2025-12-15T16:51:49.443Z" }, - { url = "https://files.pythonhosted.org/packages/fe/35/323611e59f8fe032649b4fb7e77f746f96eb7588fcbb31af26bae9630571/librt-0.7.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b370a77be0a16e1ad0270822c12c21462dc40496e891d3b0caf1617c8cc57e20", size = 174818, upload-time = "2025-12-15T16:51:51.015Z" }, - { url = "https://files.pythonhosted.org/packages/41/e6/40fb2bb21616c6e06b6a64022802228066e9a31618f493e03f6b9661548a/librt-0.7.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d05acd46b9a52087bfc50c59dfdf96a2c480a601e8898a44821c7fd676598f74", size = 189607, upload-time = "2025-12-15T16:51:52.671Z" }, - { url = "https://files.pythonhosted.org/packages/32/48/1b47c7d5d28b775941e739ed2bfe564b091c49201b9503514d69e4ed96d7/librt-0.7.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:70969229cb23d9c1a80e14225838d56e464dc71fa34c8342c954fc50e7516dee", size = 184585, upload-time = "2025-12-15T16:51:54.027Z" }, - { url = "https://files.pythonhosted.org/packages/75/a6/ee135dfb5d3b54d5d9001dbe483806229c6beac3ee2ba1092582b7efeb1b/librt-0.7.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4450c354b89dbb266730893862dbff06006c9ed5b06b6016d529b2bf644fc681", size = 178249, upload-time = "2025-12-15T16:51:55.248Z" }, - { url = "https://files.pythonhosted.org/packages/04/87/d5b84ec997338be26af982bcd6679be0c1db9a32faadab1cf4bb24f9e992/librt-0.7.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:adefe0d48ad35b90b6f361f6ff5a1bd95af80c17d18619c093c60a20e7a5b60c", size = 199851, upload-time = "2025-12-15T16:51:56.933Z" }, - { url = "https://files.pythonhosted.org/packages/86/63/ba1333bf48306fe398e3392a7427ce527f81b0b79d0d91618c4610ce9d15/librt-0.7.4-cp313-cp313-win32.whl", hash = "sha256:21ea710e96c1e050635700695095962a22ea420d4b3755a25e4909f2172b4ff2", size = 43249, upload-time = "2025-12-15T16:51:58.498Z" }, - { url = "https://files.pythonhosted.org/packages/f9/8a/de2c6df06cdfa9308c080e6b060fe192790b6a48a47320b215e860f0e98c/librt-0.7.4-cp313-cp313-win_amd64.whl", hash = "sha256:772e18696cf5a64afee908662fbcb1f907460ddc851336ee3a848ef7684c8e1e", size = 49417, upload-time = "2025-12-15T16:51:59.618Z" }, - { url = "https://files.pythonhosted.org/packages/31/66/8ee0949efc389691381ed686185e43536c20e7ad880c122dd1f31e65c658/librt-0.7.4-cp313-cp313-win_arm64.whl", hash = "sha256:52e34c6af84e12921748c8354aa6acf1912ca98ba60cdaa6920e34793f1a0788", size = 42824, upload-time = "2025-12-15T16:52:00.784Z" }, - { url = "https://files.pythonhosted.org/packages/74/81/6921e65c8708eb6636bbf383aa77e6c7dad33a598ed3b50c313306a2da9d/librt-0.7.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4f1ee004942eaaed6e06c087d93ebc1c67e9a293e5f6b9b5da558df6bf23dc5d", size = 55191, upload-time = "2025-12-15T16:52:01.97Z" }, - { url = "https://files.pythonhosted.org/packages/0d/d6/3eb864af8a8de8b39cc8dd2e9ded1823979a27795d72c4eea0afa8c26c9f/librt-0.7.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d854c6dc0f689bad7ed452d2a3ecff58029d80612d336a45b62c35e917f42d23", size = 56898, upload-time = "2025-12-15T16:52:03.356Z" }, - { url = "https://files.pythonhosted.org/packages/49/bc/b1d4c0711fdf79646225d576faee8747b8528a6ec1ceb6accfd89ade7102/librt-0.7.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a4f7339d9e445280f23d63dea842c0c77379c4a47471c538fc8feedab9d8d063", size = 163725, upload-time = "2025-12-15T16:52:04.572Z" }, - { url = "https://files.pythonhosted.org/packages/2c/08/61c41cd8f0a6a41fc99ea78a2205b88187e45ba9800792410ed62f033584/librt-0.7.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39003fc73f925e684f8521b2dbf34f61a5deb8a20a15dcf53e0d823190ce8848", size = 172469, upload-time = "2025-12-15T16:52:05.863Z" }, - { url = "https://files.pythonhosted.org/packages/8b/c7/4ee18b4d57f01444230bc18cf59103aeab8f8c0f45e84e0e540094df1df1/librt-0.7.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6bb15ee29d95875ad697d449fe6071b67f730f15a6961913a2b0205015ca0843", size = 186804, upload-time = "2025-12-15T16:52:07.192Z" }, - { url = "https://files.pythonhosted.org/packages/a1/af/009e8ba3fbf830c936842da048eda1b34b99329f402e49d88fafff6525d1/librt-0.7.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:02a69369862099e37d00765583052a99d6a68af7e19b887e1b78fee0146b755a", size = 181807, upload-time = "2025-12-15T16:52:08.554Z" }, - { url = "https://files.pythonhosted.org/packages/85/26/51ae25f813656a8b117c27a974f25e8c1e90abcd5a791ac685bf5b489a1b/librt-0.7.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ec72342cc4d62f38b25a94e28b9efefce41839aecdecf5e9627473ed04b7be16", size = 175595, upload-time = "2025-12-15T16:52:10.186Z" }, - { url = "https://files.pythonhosted.org/packages/48/93/36d6c71f830305f88996b15c8e017aa8d1e03e2e947b40b55bbf1a34cf24/librt-0.7.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:776dbb9bfa0fc5ce64234b446995d8d9f04badf64f544ca036bd6cff6f0732ce", size = 196504, upload-time = "2025-12-15T16:52:11.472Z" }, - { url = "https://files.pythonhosted.org/packages/08/11/8299e70862bb9d704735bf132c6be09c17b00fbc7cda0429a9df222fdc1b/librt-0.7.4-cp314-cp314-win32.whl", hash = "sha256:0f8cac84196d0ffcadf8469d9ded4d4e3a8b1c666095c2a291e22bf58e1e8a9f", size = 39738, upload-time = "2025-12-15T16:52:12.962Z" }, - { url = "https://files.pythonhosted.org/packages/54/d5/656b0126e4e0f8e2725cd2d2a1ec40f71f37f6f03f135a26b663c0e1a737/librt-0.7.4-cp314-cp314-win_amd64.whl", hash = "sha256:037f5cb6fe5abe23f1dc058054d50e9699fcc90d0677eee4e4f74a8677636a1a", size = 45976, upload-time = "2025-12-15T16:52:14.441Z" }, - { url = "https://files.pythonhosted.org/packages/60/86/465ff07b75c1067da8fa7f02913c4ead096ef106cfac97a977f763783bfb/librt-0.7.4-cp314-cp314-win_arm64.whl", hash = "sha256:a5deebb53d7a4d7e2e758a96befcd8edaaca0633ae71857995a0f16033289e44", size = 39073, upload-time = "2025-12-15T16:52:15.621Z" }, - { url = "https://files.pythonhosted.org/packages/b3/a0/24941f85960774a80d4b3c2aec651d7d980466da8101cae89e8b032a3e21/librt-0.7.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b4c25312c7f4e6ab35ab16211bdf819e6e4eddcba3b2ea632fb51c9a2a97e105", size = 57369, upload-time = "2025-12-15T16:52:16.782Z" }, - { url = "https://files.pythonhosted.org/packages/77/a0/ddb259cae86ab415786c1547d0fe1b40f04a7b089f564fd5c0242a3fafb2/librt-0.7.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:618b7459bb392bdf373f2327e477597fff8f9e6a1878fffc1b711c013d1b0da4", size = 59230, upload-time = "2025-12-15T16:52:18.259Z" }, - { url = "https://files.pythonhosted.org/packages/31/11/77823cb530ab8a0c6fac848ac65b745be446f6f301753b8990e8809080c9/librt-0.7.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1437c3f72a30c7047f16fd3e972ea58b90172c3c6ca309645c1c68984f05526a", size = 183869, upload-time = "2025-12-15T16:52:19.457Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ce/157db3614cf3034b3f702ae5ba4fefda4686f11eea4b7b96542324a7a0e7/librt-0.7.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c96cb76f055b33308f6858b9b594618f1b46e147a4d03a4d7f0c449e304b9b95", size = 194606, upload-time = "2025-12-15T16:52:20.795Z" }, - { url = "https://files.pythonhosted.org/packages/30/ef/6ec4c7e3d6490f69a4fd2803516fa5334a848a4173eac26d8ee6507bff6e/librt-0.7.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28f990e6821204f516d09dc39966ef8b84556ffd648d5926c9a3f681e8de8906", size = 206776, upload-time = "2025-12-15T16:52:22.229Z" }, - { url = "https://files.pythonhosted.org/packages/ad/22/750b37bf549f60a4782ab80e9d1e9c44981374ab79a7ea68670159905918/librt-0.7.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc4aebecc79781a1b77d7d4e7d9fe080385a439e198d993b557b60f9117addaf", size = 203205, upload-time = "2025-12-15T16:52:23.603Z" }, - { url = "https://files.pythonhosted.org/packages/7a/87/2e8a0f584412a93df5faad46c5fa0a6825fdb5eba2ce482074b114877f44/librt-0.7.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:022cc673e69283a42621dd453e2407cf1647e77f8bd857d7ad7499901e62376f", size = 196696, upload-time = "2025-12-15T16:52:24.951Z" }, - { url = "https://files.pythonhosted.org/packages/e5/ca/7bf78fa950e43b564b7de52ceeb477fb211a11f5733227efa1591d05a307/librt-0.7.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2b3ca211ae8ea540569e9c513da052699b7b06928dcda61247cb4f318122bdb5", size = 217191, upload-time = "2025-12-15T16:52:26.194Z" }, - { url = "https://files.pythonhosted.org/packages/d6/49/3732b0e8424ae35ad5c3166d9dd5bcdae43ce98775e0867a716ff5868064/librt-0.7.4-cp314-cp314t-win32.whl", hash = "sha256:8a461f6456981d8c8e971ff5a55f2e34f4e60871e665d2f5fde23ee74dea4eeb", size = 40276, upload-time = "2025-12-15T16:52:27.54Z" }, - { url = "https://files.pythonhosted.org/packages/35/d6/d8823e01bd069934525fddb343189c008b39828a429b473fb20d67d5cd36/librt-0.7.4-cp314-cp314t-win_amd64.whl", hash = "sha256:721a7b125a817d60bf4924e1eec2a7867bfcf64cfc333045de1df7a0629e4481", size = 46772, upload-time = "2025-12-15T16:52:28.653Z" }, - { url = "https://files.pythonhosted.org/packages/36/e9/a0aa60f5322814dd084a89614e9e31139702e342f8459ad8af1984a18168/librt-0.7.4-cp314-cp314t-win_arm64.whl", hash = "sha256:76b2ba71265c0102d11458879b4d53ccd0b32b0164d14deb8d2b598a018e502f", size = 39724, upload-time = "2025-12-15T16:52:29.836Z" }, +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/3f/4ca7dd7819bf8ff303aca39c3c60e5320e46e766ab7f7dd627d3b9c11bdf/librt-0.8.0.tar.gz", hash = "sha256:cb74cdcbc0103fc988e04e5c58b0b31e8e5dd2babb9182b6f9490488eb36324b", size = 177306, upload-time = "2026-02-12T14:53:54.743Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/e9/42af181c89b65abfd557c1b017cba5b82098eef7bf26d1649d82ce93ccc7/librt-0.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ce33a9778e294507f3a0e3468eccb6a698b5166df7db85661543eca1cfc5369", size = 65314, upload-time = "2026-02-12T14:52:14.778Z" }, + { url = "https://files.pythonhosted.org/packages/9d/4a/15a847fca119dc0334a4b8012b1e15fdc5fc19d505b71e227eaf1bcdba09/librt-0.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8070aa3368559de81061ef752770d03ca1f5fc9467d4d512d405bd0483bfffe6", size = 68015, upload-time = "2026-02-12T14:52:15.797Z" }, + { url = "https://files.pythonhosted.org/packages/e1/87/ffc8dbd6ab68dd91b736c88529411a6729649d2b74b887f91f3aaff8d992/librt-0.8.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:20f73d4fecba969efc15cdefd030e382502d56bb6f1fc66b580cce582836c9fa", size = 194508, upload-time = "2026-02-12T14:52:16.835Z" }, + { url = "https://files.pythonhosted.org/packages/89/92/a7355cea28d6c48ff6ff5083ac4a2a866fb9b07b786aa70d1f1116680cd5/librt-0.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a512c88900bdb1d448882f5623a0b1ad27ba81a9bd75dacfe17080b72272ca1f", size = 205630, upload-time = "2026-02-12T14:52:18.58Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5e/54509038d7ac527828db95b8ba1c8f5d2649bc32fd8f39b1718ec9957dce/librt-0.8.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:015e2dde6e096d27c10238bf9f6492ba6c65822dfb69d2bf74c41a8e88b7ddef", size = 218289, upload-time = "2026-02-12T14:52:20.134Z" }, + { url = "https://files.pythonhosted.org/packages/6d/17/0ee0d13685cefee6d6f2d47bb643ddad3c62387e2882139794e6a5f1288a/librt-0.8.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1c25a131013eadd3c600686a0c0333eb2896483cbc7f65baa6a7ee761017aef9", size = 211508, upload-time = "2026-02-12T14:52:21.413Z" }, + { url = "https://files.pythonhosted.org/packages/4b/a8/1714ef6e9325582e3727de3be27e4c1b2f428ea411d09f1396374180f130/librt-0.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:21b14464bee0b604d80a638cf1ee3148d84ca4cc163dcdcecb46060c1b3605e4", size = 219129, upload-time = "2026-02-12T14:52:22.61Z" }, + { url = "https://files.pythonhosted.org/packages/89/d3/2d9fe353edff91cdc0ece179348054a6fa61f3de992c44b9477cb973509b/librt-0.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:05a3dd3f116747f7e1a2b475ccdc6fb637fd4987126d109e03013a79d40bf9e6", size = 213126, upload-time = "2026-02-12T14:52:23.819Z" }, + { url = "https://files.pythonhosted.org/packages/ad/8e/9f5c60444880f6ad50e3ff7475e5529e787797e7f3ad5432241633733b92/librt-0.8.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:fa37f99bff354ff191c6bcdffbc9d7cdd4fc37faccfc9be0ef3a4fd5613977da", size = 212279, upload-time = "2026-02-12T14:52:25.034Z" }, + { url = "https://files.pythonhosted.org/packages/fe/eb/d4a2cfa647da3022ae977f50d7eda1d91f70d7d1883cf958a4b6ef689eab/librt-0.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1566dbb9d1eb0987264c9b9460d212e809ba908d2f4a3999383a84d765f2f3f1", size = 234654, upload-time = "2026-02-12T14:52:26.204Z" }, + { url = "https://files.pythonhosted.org/packages/6a/31/26b978861c7983b036a3aea08bdbb2ec32bbaab1ad1d57c5e022be59afc1/librt-0.8.0-cp311-cp311-win32.whl", hash = "sha256:70defb797c4d5402166787a6b3c66dfb3fa7f93d118c0509ffafa35a392f4258", size = 54603, upload-time = "2026-02-12T14:52:27.342Z" }, + { url = "https://files.pythonhosted.org/packages/d0/78/f194ed7c48dacf875677e749c5d0d1d69a9daa7c994314a39466237fb1be/librt-0.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:db953b675079884ffda33d1dca7189fb961b6d372153750beb81880384300817", size = 61730, upload-time = "2026-02-12T14:52:28.31Z" }, + { url = "https://files.pythonhosted.org/packages/97/ee/ad71095478d02137b6f49469dc808c595cfe89b50985f6b39c5345f0faab/librt-0.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:75d1a8cab20b2043f03f7aab730551e9e440adc034d776f15f6f8d582b0a5ad4", size = 52274, upload-time = "2026-02-12T14:52:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fb/53/f3bc0c4921adb0d4a5afa0656f2c0fbe20e18e3e0295e12985b9a5dc3f55/librt-0.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:17269dd2745dbe8e42475acb28e419ad92dfa38214224b1b01020b8cac70b645", size = 66511, upload-time = "2026-02-12T14:52:30.34Z" }, + { url = "https://files.pythonhosted.org/packages/89/4b/4c96357432007c25a1b5e363045373a6c39481e49f6ba05234bb59a839c1/librt-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f4617cef654fca552f00ce5ffdf4f4b68770f18950e4246ce94629b789b92467", size = 68628, upload-time = "2026-02-12T14:52:31.491Z" }, + { url = "https://files.pythonhosted.org/packages/47/16/52d75374d1012e8fc709216b5eaa25f471370e2a2331b8be00f18670a6c7/librt-0.8.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5cb11061a736a9db45e3c1293cfcb1e3caf205912dfa085734ba750f2197ff9a", size = 198941, upload-time = "2026-02-12T14:52:32.489Z" }, + { url = "https://files.pythonhosted.org/packages/fc/11/d5dd89e5a2228567b1228d8602d896736247424484db086eea6b8010bcba/librt-0.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4bb00bd71b448f16749909b08a0ff16f58b079e2261c2e1000f2bbb2a4f0a45", size = 210009, upload-time = "2026-02-12T14:52:33.634Z" }, + { url = "https://files.pythonhosted.org/packages/49/d8/fc1a92a77c3020ee08ce2dc48aed4b42ab7c30fb43ce488d388673b0f164/librt-0.8.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95a719a049f0eefaf1952673223cf00d442952273cbd20cf2ed7ec423a0ef58d", size = 224461, upload-time = "2026-02-12T14:52:34.868Z" }, + { url = "https://files.pythonhosted.org/packages/7f/98/eb923e8b028cece924c246104aa800cf72e02d023a8ad4ca87135b05a2fe/librt-0.8.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bd32add59b58fba3439d48d6f36ac695830388e3da3e92e4fc26d2d02670d19c", size = 217538, upload-time = "2026-02-12T14:52:36.078Z" }, + { url = "https://files.pythonhosted.org/packages/fd/67/24e80ab170674a1d8ee9f9a83081dca4635519dbd0473b8321deecddb5be/librt-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4f764b2424cb04524ff7a486b9c391e93f93dc1bd8305b2136d25e582e99aa2f", size = 225110, upload-time = "2026-02-12T14:52:37.301Z" }, + { url = "https://files.pythonhosted.org/packages/d8/c7/6fbdcbd1a6e5243c7989c21d68ab967c153b391351174b4729e359d9977f/librt-0.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f04ca50e847abc486fa8f4107250566441e693779a5374ba211e96e238f298b9", size = 217758, upload-time = "2026-02-12T14:52:38.89Z" }, + { url = "https://files.pythonhosted.org/packages/4b/bd/4d6b36669db086e3d747434430073e14def032dd58ad97959bf7e2d06c67/librt-0.8.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9ab3a3475a55b89b87ffd7e6665838e8458e0b596c22e0177e0f961434ec474a", size = 218384, upload-time = "2026-02-12T14:52:40.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/2d/afe966beb0a8f179b132f3e95c8dd90738a23e9ebdba10f89a3f192f9366/librt-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e36a8da17134ffc29373775d88c04832f9ecfab1880470661813e6c7991ef79", size = 241187, upload-time = "2026-02-12T14:52:43.55Z" }, + { url = "https://files.pythonhosted.org/packages/02/d0/6172ea4af2b538462785ab1a68e52d5c99cfb9866a7caf00fdf388299734/librt-0.8.0-cp312-cp312-win32.whl", hash = "sha256:4eb5e06ebcc668677ed6389164f52f13f71737fc8be471101fa8b4ce77baeb0c", size = 54914, upload-time = "2026-02-12T14:52:44.676Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cb/ceb6ed6175612a4337ad49fb01ef594712b934b4bc88ce8a63554832eb44/librt-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:0a33335eb59921e77c9acc05d0e654e4e32e45b014a4d61517897c11591094f8", size = 62020, upload-time = "2026-02-12T14:52:45.676Z" }, + { url = "https://files.pythonhosted.org/packages/f1/7e/61701acbc67da74ce06ddc7ba9483e81c70f44236b2d00f6a4bfee1aacbf/librt-0.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:24a01c13a2a9bdad20997a4443ebe6e329df063d1978bbe2ebbf637878a46d1e", size = 52443, upload-time = "2026-02-12T14:52:47.218Z" }, + { url = "https://files.pythonhosted.org/packages/6d/32/3edb0bcb4113a9c8bdcd1750663a54565d255027657a5df9d90f13ee07fa/librt-0.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7f820210e21e3a8bf8fde2ae3c3d10106d4de9ead28cbfdf6d0f0f41f5b12fa1", size = 66522, upload-time = "2026-02-12T14:52:48.219Z" }, + { url = "https://files.pythonhosted.org/packages/30/ab/e8c3d05e281f5d405ebdcc5bc8ab36df23e1a4b40ac9da8c3eb9928b72b9/librt-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4831c44b8919e75ca0dfb52052897c1ef59fdae19d3589893fbd068f1e41afbf", size = 68658, upload-time = "2026-02-12T14:52:50.351Z" }, + { url = "https://files.pythonhosted.org/packages/7c/d3/74a206c47b7748bbc8c43942de3ed67de4c231156e148b4f9250869593df/librt-0.8.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:88c6e75540f1f10f5e0fc5e87b4b6c290f0e90d1db8c6734f670840494764af8", size = 199287, upload-time = "2026-02-12T14:52:51.938Z" }, + { url = "https://files.pythonhosted.org/packages/fa/29/ef98a9131cf12cb95771d24e4c411fda96c89dc78b09c2de4704877ebee4/librt-0.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9646178cd794704d722306c2c920c221abbf080fede3ba539d5afdec16c46dad", size = 210293, upload-time = "2026-02-12T14:52:53.128Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3e/89b4968cb08c53d4c2d8b02517081dfe4b9e07a959ec143d333d76899f6c/librt-0.8.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e1af31a710e17891d9adf0dbd9a5fcd94901a3922a96499abdbf7ce658f4e01", size = 224801, upload-time = "2026-02-12T14:52:54.367Z" }, + { url = "https://files.pythonhosted.org/packages/6d/28/f38526d501f9513f8b48d78e6be4a241e15dd4b000056dc8b3f06ee9ce5d/librt-0.8.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:507e94f4bec00b2f590fbe55f48cd518a208e2474a3b90a60aa8f29136ddbada", size = 218090, upload-time = "2026-02-12T14:52:55.758Z" }, + { url = "https://files.pythonhosted.org/packages/02/ec/64e29887c5009c24dc9c397116c680caffc50286f62bd99c39e3875a2854/librt-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f1178e0de0c271231a660fbef9be6acdfa1d596803464706862bef6644cc1cae", size = 225483, upload-time = "2026-02-12T14:52:57.375Z" }, + { url = "https://files.pythonhosted.org/packages/ee/16/7850bdbc9f1a32d3feff2708d90c56fc0490b13f1012e438532781aa598c/librt-0.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:71fc517efc14f75c2f74b1f0a5d5eb4a8e06aa135c34d18eaf3522f4a53cd62d", size = 218226, upload-time = "2026-02-12T14:52:58.534Z" }, + { url = "https://files.pythonhosted.org/packages/1c/4a/166bffc992d65ddefa7c47052010a87c059b44a458ebaf8f5eba384b0533/librt-0.8.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:0583aef7e9a720dd40f26a2ad5a1bf2ccbb90059dac2b32ac516df232c701db3", size = 218755, upload-time = "2026-02-12T14:52:59.701Z" }, + { url = "https://files.pythonhosted.org/packages/da/5d/9aeee038bcc72a9cfaaee934463fe9280a73c5440d36bd3175069d2cb97b/librt-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5d0f76fc73480d42285c609c0ea74d79856c160fa828ff9aceab574ea4ecfd7b", size = 241617, upload-time = "2026-02-12T14:53:00.966Z" }, + { url = "https://files.pythonhosted.org/packages/64/ff/2bec6b0296b9d0402aa6ec8540aa19ebcb875d669c37800cb43d10d9c3a3/librt-0.8.0-cp313-cp313-win32.whl", hash = "sha256:e79dbc8f57de360f0ed987dc7de7be814b4803ef0e8fc6d3ff86e16798c99935", size = 54966, upload-time = "2026-02-12T14:53:02.042Z" }, + { url = "https://files.pythonhosted.org/packages/08/8d/bf44633b0182996b2c7ea69a03a5c529683fa1f6b8e45c03fe874ff40d56/librt-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:25b3e667cbfc9000c4740b282df599ebd91dbdcc1aa6785050e4c1d6be5329ab", size = 62000, upload-time = "2026-02-12T14:53:03.822Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fd/c6472b8e0eac0925001f75e366cf5500bcb975357a65ef1f6b5749389d3a/librt-0.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:e9a3a38eb4134ad33122a6d575e6324831f930a771d951a15ce232e0237412c2", size = 52496, upload-time = "2026-02-12T14:53:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/13/79ebfe30cd273d7c0ce37a5f14dc489c5fb8b722a008983db2cfd57270bb/librt-0.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:421765e8c6b18e64d21c8ead315708a56fc24f44075059702e421d164575fdda", size = 66078, upload-time = "2026-02-12T14:53:06.085Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8f/d11eca40b62a8d5e759239a80636386ef88adecb10d1a050b38cc0da9f9e/librt-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:48f84830a8f8ad7918afd743fd7c4eb558728bceab7b0e38fd5a5cf78206a556", size = 68309, upload-time = "2026-02-12T14:53:07.121Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b4/f12ee70a3596db40ff3c88ec9eaa4e323f3b92f77505b4d900746706ec6a/librt-0.8.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9f09d4884f882baa39a7e36bbf3eae124c4ca2a223efb91e567381d1c55c6b06", size = 196804, upload-time = "2026-02-12T14:53:08.164Z" }, + { url = "https://files.pythonhosted.org/packages/8b/7e/70dbbdc0271fd626abe1671ad117bcd61a9a88cdc6a10ccfbfc703db1873/librt-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:693697133c3b32aa9b27f040e3691be210e9ac4d905061859a9ed519b1d5a376", size = 206915, upload-time = "2026-02-12T14:53:09.333Z" }, + { url = "https://files.pythonhosted.org/packages/79/13/6b9e05a635d4327608d06b3c1702166e3b3e78315846373446cf90d7b0bf/librt-0.8.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5512aae4648152abaf4d48b59890503fcbe86e85abc12fb9b096fe948bdd816", size = 221200, upload-time = "2026-02-12T14:53:10.68Z" }, + { url = "https://files.pythonhosted.org/packages/35/6c/e19a3ac53e9414de43a73d7507d2d766cd22d8ca763d29a4e072d628db42/librt-0.8.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:995d24caa6bbb34bcdd4a41df98ac6d1af637cfa8975cb0790e47d6623e70e3e", size = 214640, upload-time = "2026-02-12T14:53:12.342Z" }, + { url = "https://files.pythonhosted.org/packages/30/f0/23a78464788619e8c70f090cfd099cce4973eed142c4dccb99fc322283fd/librt-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b9aef96d7593584e31ef6ac1eb9775355b0099fee7651fae3a15bc8657b67b52", size = 221980, upload-time = "2026-02-12T14:53:13.603Z" }, + { url = "https://files.pythonhosted.org/packages/03/32/38e21420c5d7aa8a8bd2c7a7d5252ab174a5a8aaec8b5551968979b747bf/librt-0.8.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:4f6e975377fbc4c9567cb33ea9ab826031b6c7ec0515bfae66a4fb110d40d6da", size = 215146, upload-time = "2026-02-12T14:53:14.8Z" }, + { url = "https://files.pythonhosted.org/packages/bb/00/bd9ecf38b1824c25240b3ad982fb62c80f0a969e6679091ba2b3afb2b510/librt-0.8.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:daae5e955764be8fd70a93e9e5133c75297f8bce1e802e1d3683b98f77e1c5ab", size = 215203, upload-time = "2026-02-12T14:53:16.087Z" }, + { url = "https://files.pythonhosted.org/packages/b9/60/7559bcc5279d37810b98d4a52616febd7b8eef04391714fd6bdf629598b1/librt-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7bd68cebf3131bb920d5984f75fe302d758db33264e44b45ad139385662d7bc3", size = 237937, upload-time = "2026-02-12T14:53:17.236Z" }, + { url = "https://files.pythonhosted.org/packages/41/cc/be3e7da88f1abbe2642672af1dc00a0bccece11ca60241b1883f3018d8d5/librt-0.8.0-cp314-cp314-win32.whl", hash = "sha256:1e6811cac1dcb27ca4c74e0ca4a5917a8e06db0d8408d30daee3a41724bfde7a", size = 50685, upload-time = "2026-02-12T14:53:18.888Z" }, + { url = "https://files.pythonhosted.org/packages/38/27/e381d0df182a8f61ef1f6025d8b138b3318cc9d18ad4d5f47c3bf7492523/librt-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:178707cda89d910c3b28bf5aa5f69d3d4734e0f6ae102f753ad79edef83a83c7", size = 57872, upload-time = "2026-02-12T14:53:19.942Z" }, + { url = "https://files.pythonhosted.org/packages/c5/0c/ca9dfdf00554a44dea7d555001248269a4bab569e1590a91391feb863fa4/librt-0.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:3e8b77b5f54d0937b26512774916041756c9eb3e66f1031971e626eea49d0bf4", size = 48056, upload-time = "2026-02-12T14:53:21.473Z" }, + { url = "https://files.pythonhosted.org/packages/f2/ed/6cc9c4ad24f90c8e782193c7b4a857408fd49540800613d1356c63567d7b/librt-0.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:789911e8fa40a2e82f41120c936b1965f3213c67f5a483fc5a41f5839a05dcbb", size = 68307, upload-time = "2026-02-12T14:53:22.498Z" }, + { url = "https://files.pythonhosted.org/packages/84/d8/0e94292c6b3e00b6eeea39dd44d5703d1ec29b6dafce7eea19dc8f1aedbd/librt-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2b37437e7e4ef5e15a297b36ba9e577f73e29564131d86dd75875705e97402b5", size = 70999, upload-time = "2026-02-12T14:53:23.603Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f4/6be1afcbdeedbdbbf54a7c9d73ad43e1bf36897cebf3978308cd64922e02/librt-0.8.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:671a6152edf3b924d98a5ed5e6982ec9cb30894085482acadce0975f031d4c5c", size = 220782, upload-time = "2026-02-12T14:53:25.133Z" }, + { url = "https://files.pythonhosted.org/packages/f0/8d/f306e8caa93cfaf5c6c9e0d940908d75dc6af4fd856baa5535c922ee02b1/librt-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8992ca186a1678107b0af3d0c9303d8c7305981b9914989b9788319ed4d89546", size = 235420, upload-time = "2026-02-12T14:53:27.047Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f2/65d86bd462e9c351326564ca805e8457442149f348496e25ccd94583ffa2/librt-0.8.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:001e5330093d887b8b9165823eca6c5c4db183fe4edea4fdc0680bbac5f46944", size = 246452, upload-time = "2026-02-12T14:53:28.341Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/39c88b503b4cb3fcbdeb3caa29672b6b44ebee8dcc8a54d49839ac280f3f/librt-0.8.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d920789eca7ef71df7f31fd547ec0d3002e04d77f30ba6881e08a630e7b2c30e", size = 238891, upload-time = "2026-02-12T14:53:29.625Z" }, + { url = "https://files.pythonhosted.org/packages/e3/c6/6c0d68190893d01b71b9569b07a1c811e280c0065a791249921c83dc0290/librt-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:82fb4602d1b3e303a58bfe6165992b5a78d823ec646445356c332cd5f5bbaa61", size = 250249, upload-time = "2026-02-12T14:53:30.93Z" }, + { url = "https://files.pythonhosted.org/packages/52/7a/f715ed9e039035d0ea637579c3c0155ab3709a7046bc408c0fb05d337121/librt-0.8.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:4d3e38797eb482485b486898f89415a6ab163bc291476bd95712e42cf4383c05", size = 240642, upload-time = "2026-02-12T14:53:32.174Z" }, + { url = "https://files.pythonhosted.org/packages/c2/3c/609000a333debf5992efe087edc6467c1fdbdddca5b610355569bbea9589/librt-0.8.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a905091a13e0884701226860836d0386b88c72ce5c2fdfba6618e14c72be9f25", size = 239621, upload-time = "2026-02-12T14:53:33.39Z" }, + { url = "https://files.pythonhosted.org/packages/b9/df/87b0673d5c395a8f34f38569c116c93142d4dc7e04af2510620772d6bd4f/librt-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:375eda7acfce1f15f5ed56cfc960669eefa1ec8732e3e9087c3c4c3f2066759c", size = 262986, upload-time = "2026-02-12T14:53:34.617Z" }, + { url = "https://files.pythonhosted.org/packages/09/7f/6bbbe9dcda649684773aaea78b87fff4d7e59550fbc2877faa83612087a3/librt-0.8.0-cp314-cp314t-win32.whl", hash = "sha256:2ccdd20d9a72c562ffb73098ac411de351b53a6fbb3390903b2d33078ef90447", size = 51328, upload-time = "2026-02-12T14:53:36.15Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f3/e1981ab6fa9b41be0396648b5850267888a752d025313a9e929c4856208e/librt-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:25e82d920d4d62ad741592fcf8d0f3bda0e3fc388a184cb7d2f566c681c5f7b9", size = 58719, upload-time = "2026-02-12T14:53:37.183Z" }, + { url = "https://files.pythonhosted.org/packages/94/d1/433b3c06e78f23486fe4fdd19bc134657eb30997d2054b0dbf52bbf3382e/librt-0.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:92249938ab744a5890580d3cb2b22042f0dce71cdaa7c1369823df62bedf7cbc", size = 48753, upload-time = "2026-02-12T14:53:38.539Z" }, ] [[package]] @@ -1607,11 +1585,11 @@ wheels = [ [[package]] name = "markdown" -version = "3.10.1" +version = "3.10.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b7/b1/af95bcae8549f1f3fd70faacb29075826a0d689a27f232e8cee315efa053/markdown-3.10.1.tar.gz", hash = "sha256:1c19c10bd5c14ac948c53d0d762a04e2fa35a6d58a6b7b1e6bfcbe6fefc0001a", size = 365402, upload-time = "2026-01-21T18:09:28.206Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/59/1b/6ef961f543593969d25b2afe57a3564200280528caa9bd1082eecdd7b3bc/markdown-3.10.1-py3-none-any.whl", hash = "sha256:867d788939fe33e4b736426f5b9f651ad0c0ae0ecf89df0ca5d1176c70812fe3", size = 107684, upload-time = "2026-01-21T18:09:27.203Z" }, + { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, ] [[package]] @@ -1853,119 +1831,119 @@ wheels = [ [[package]] name = "multidict" -version = "6.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834, upload-time = "2025-10-06T14:52:30.657Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/34/9e/5c727587644d67b2ed479041e4b1c58e30afc011e3d45d25bbe35781217c/multidict-6.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4d409aa42a94c0b3fa617708ef5276dfe81012ba6753a0370fcc9d0195d0a1fc", size = 76604, upload-time = "2025-10-06T14:48:54.277Z" }, - { url = "https://files.pythonhosted.org/packages/17/e4/67b5c27bd17c085a5ea8f1ec05b8a3e5cba0ca734bfcad5560fb129e70ca/multidict-6.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14c9e076eede3b54c636f8ce1c9c252b5f057c62131211f0ceeec273810c9721", size = 44715, upload-time = "2025-10-06T14:48:55.445Z" }, - { url = "https://files.pythonhosted.org/packages/4d/e1/866a5d77be6ea435711bef2a4291eed11032679b6b28b56b4776ab06ba3e/multidict-6.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c09703000a9d0fa3c3404b27041e574cc7f4df4c6563873246d0e11812a94b6", size = 44332, upload-time = "2025-10-06T14:48:56.706Z" }, - { url = "https://files.pythonhosted.org/packages/31/61/0c2d50241ada71ff61a79518db85ada85fdabfcf395d5968dae1cbda04e5/multidict-6.7.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a265acbb7bb33a3a2d626afbe756371dce0279e7b17f4f4eda406459c2b5ff1c", size = 245212, upload-time = "2025-10-06T14:48:58.042Z" }, - { url = "https://files.pythonhosted.org/packages/ac/e0/919666a4e4b57fff1b57f279be1c9316e6cdc5de8a8b525d76f6598fefc7/multidict-6.7.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51cb455de290ae462593e5b1cb1118c5c22ea7f0d3620d9940bf695cea5a4bd7", size = 246671, upload-time = "2025-10-06T14:49:00.004Z" }, - { url = "https://files.pythonhosted.org/packages/a1/cc/d027d9c5a520f3321b65adea289b965e7bcbd2c34402663f482648c716ce/multidict-6.7.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:db99677b4457c7a5c5a949353e125ba72d62b35f74e26da141530fbb012218a7", size = 225491, upload-time = "2025-10-06T14:49:01.393Z" }, - { url = "https://files.pythonhosted.org/packages/75/c4/bbd633980ce6155a28ff04e6a6492dd3335858394d7bb752d8b108708558/multidict-6.7.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f470f68adc395e0183b92a2f4689264d1ea4b40504a24d9882c27375e6662bb9", size = 257322, upload-time = "2025-10-06T14:49:02.745Z" }, - { url = "https://files.pythonhosted.org/packages/4c/6d/d622322d344f1f053eae47e033b0b3f965af01212de21b10bcf91be991fb/multidict-6.7.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0db4956f82723cc1c270de9c6e799b4c341d327762ec78ef82bb962f79cc07d8", size = 254694, upload-time = "2025-10-06T14:49:04.15Z" }, - { url = "https://files.pythonhosted.org/packages/a8/9f/78f8761c2705d4c6d7516faed63c0ebdac569f6db1bef95e0d5218fdc146/multidict-6.7.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e56d780c238f9e1ae66a22d2adf8d16f485381878250db8d496623cd38b22bd", size = 246715, upload-time = "2025-10-06T14:49:05.967Z" }, - { url = "https://files.pythonhosted.org/packages/78/59/950818e04f91b9c2b95aab3d923d9eabd01689d0dcd889563988e9ea0fd8/multidict-6.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d14baca2ee12c1a64740d4531356ba50b82543017f3ad6de0deb943c5979abb", size = 243189, upload-time = "2025-10-06T14:49:07.37Z" }, - { url = "https://files.pythonhosted.org/packages/7a/3d/77c79e1934cad2ee74991840f8a0110966d9599b3af95964c0cd79bb905b/multidict-6.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:295a92a76188917c7f99cda95858c822f9e4aae5824246bba9b6b44004ddd0a6", size = 237845, upload-time = "2025-10-06T14:49:08.759Z" }, - { url = "https://files.pythonhosted.org/packages/63/1b/834ce32a0a97a3b70f86437f685f880136677ac00d8bce0027e9fd9c2db7/multidict-6.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:39f1719f57adbb767ef592a50ae5ebb794220d1188f9ca93de471336401c34d2", size = 246374, upload-time = "2025-10-06T14:49:10.574Z" }, - { url = "https://files.pythonhosted.org/packages/23/ef/43d1c3ba205b5dec93dc97f3fba179dfa47910fc73aaaea4f7ceb41cec2a/multidict-6.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0a13fb8e748dfc94749f622de065dd5c1def7e0d2216dba72b1d8069a389c6ff", size = 253345, upload-time = "2025-10-06T14:49:12.331Z" }, - { url = "https://files.pythonhosted.org/packages/6b/03/eaf95bcc2d19ead522001f6a650ef32811aa9e3624ff0ad37c445c7a588c/multidict-6.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e3aa16de190d29a0ea1b48253c57d99a68492c8dd8948638073ab9e74dc9410b", size = 246940, upload-time = "2025-10-06T14:49:13.821Z" }, - { url = "https://files.pythonhosted.org/packages/e8/df/ec8a5fd66ea6cd6f525b1fcbb23511b033c3e9bc42b81384834ffa484a62/multidict-6.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a048ce45dcdaaf1defb76b2e684f997fb5abf74437b6cb7b22ddad934a964e34", size = 242229, upload-time = "2025-10-06T14:49:15.603Z" }, - { url = "https://files.pythonhosted.org/packages/8a/a2/59b405d59fd39ec86d1142630e9049243015a5f5291ba49cadf3c090c541/multidict-6.7.0-cp311-cp311-win32.whl", hash = "sha256:a90af66facec4cebe4181b9e62a68be65e45ac9b52b67de9eec118701856e7ff", size = 41308, upload-time = "2025-10-06T14:49:16.871Z" }, - { url = "https://files.pythonhosted.org/packages/32/0f/13228f26f8b882c34da36efa776c3b7348455ec383bab4a66390e42963ae/multidict-6.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:95b5ffa4349df2887518bb839409bcf22caa72d82beec453216802f475b23c81", size = 46037, upload-time = "2025-10-06T14:49:18.457Z" }, - { url = "https://files.pythonhosted.org/packages/84/1f/68588e31b000535a3207fd3c909ebeec4fb36b52c442107499c18a896a2a/multidict-6.7.0-cp311-cp311-win_arm64.whl", hash = "sha256:329aa225b085b6f004a4955271a7ba9f1087e39dcb7e65f6284a988264a63912", size = 43023, upload-time = "2025-10-06T14:49:19.648Z" }, - { url = "https://files.pythonhosted.org/packages/c2/9e/9f61ac18d9c8b475889f32ccfa91c9f59363480613fc807b6e3023d6f60b/multidict-6.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184", size = 76877, upload-time = "2025-10-06T14:49:20.884Z" }, - { url = "https://files.pythonhosted.org/packages/38/6f/614f09a04e6184f8824268fce4bc925e9849edfa654ddd59f0b64508c595/multidict-6.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45", size = 45467, upload-time = "2025-10-06T14:49:22.054Z" }, - { url = "https://files.pythonhosted.org/packages/b3/93/c4f67a436dd026f2e780c433277fff72be79152894d9fc36f44569cab1a6/multidict-6.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa", size = 43834, upload-time = "2025-10-06T14:49:23.566Z" }, - { url = "https://files.pythonhosted.org/packages/7f/f5/013798161ca665e4a422afbc5e2d9e4070142a9ff8905e482139cd09e4d0/multidict-6.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7", size = 250545, upload-time = "2025-10-06T14:49:24.882Z" }, - { url = "https://files.pythonhosted.org/packages/71/2f/91dbac13e0ba94669ea5119ba267c9a832f0cb65419aca75549fcf09a3dc/multidict-6.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e", size = 258305, upload-time = "2025-10-06T14:49:26.778Z" }, - { url = "https://files.pythonhosted.org/packages/ef/b0/754038b26f6e04488b48ac621f779c341338d78503fb45403755af2df477/multidict-6.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546", size = 242363, upload-time = "2025-10-06T14:49:28.562Z" }, - { url = "https://files.pythonhosted.org/packages/87/15/9da40b9336a7c9fa606c4cf2ed80a649dffeb42b905d4f63a1d7eb17d746/multidict-6.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4", size = 268375, upload-time = "2025-10-06T14:49:29.96Z" }, - { url = "https://files.pythonhosted.org/packages/82/72/c53fcade0cc94dfaad583105fd92b3a783af2091eddcb41a6d5a52474000/multidict-6.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1", size = 269346, upload-time = "2025-10-06T14:49:31.404Z" }, - { url = "https://files.pythonhosted.org/packages/0d/e2/9baffdae21a76f77ef8447f1a05a96ec4bc0a24dae08767abc0a2fe680b8/multidict-6.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d", size = 256107, upload-time = "2025-10-06T14:49:32.974Z" }, - { url = "https://files.pythonhosted.org/packages/3c/06/3f06f611087dc60d65ef775f1fb5aca7c6d61c6db4990e7cda0cef9b1651/multidict-6.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304", size = 253592, upload-time = "2025-10-06T14:49:34.52Z" }, - { url = "https://files.pythonhosted.org/packages/20/24/54e804ec7945b6023b340c412ce9c3f81e91b3bf5fa5ce65558740141bee/multidict-6.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12", size = 251024, upload-time = "2025-10-06T14:49:35.956Z" }, - { url = "https://files.pythonhosted.org/packages/14/48/011cba467ea0b17ceb938315d219391d3e421dfd35928e5dbdc3f4ae76ef/multidict-6.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62", size = 251484, upload-time = "2025-10-06T14:49:37.631Z" }, - { url = "https://files.pythonhosted.org/packages/0d/2f/919258b43bb35b99fa127435cfb2d91798eb3a943396631ef43e3720dcf4/multidict-6.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0", size = 263579, upload-time = "2025-10-06T14:49:39.502Z" }, - { url = "https://files.pythonhosted.org/packages/31/22/a0e884d86b5242b5a74cf08e876bdf299e413016b66e55511f7a804a366e/multidict-6.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a", size = 259654, upload-time = "2025-10-06T14:49:41.32Z" }, - { url = "https://files.pythonhosted.org/packages/b2/e5/17e10e1b5c5f5a40f2fcbb45953c9b215f8a4098003915e46a93f5fcaa8f/multidict-6.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8", size = 251511, upload-time = "2025-10-06T14:49:46.021Z" }, - { url = "https://files.pythonhosted.org/packages/e3/9a/201bb1e17e7af53139597069c375e7b0dcbd47594604f65c2d5359508566/multidict-6.7.0-cp312-cp312-win32.whl", hash = "sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4", size = 41895, upload-time = "2025-10-06T14:49:48.718Z" }, - { url = "https://files.pythonhosted.org/packages/46/e2/348cd32faad84eaf1d20cce80e2bb0ef8d312c55bca1f7fa9865e7770aaf/multidict-6.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b", size = 46073, upload-time = "2025-10-06T14:49:50.28Z" }, - { url = "https://files.pythonhosted.org/packages/25/ec/aad2613c1910dce907480e0c3aa306905830f25df2e54ccc9dea450cb5aa/multidict-6.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec", size = 43226, upload-time = "2025-10-06T14:49:52.304Z" }, - { url = "https://files.pythonhosted.org/packages/d2/86/33272a544eeb36d66e4d9a920602d1a2f57d4ebea4ef3cdfe5a912574c95/multidict-6.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6", size = 76135, upload-time = "2025-10-06T14:49:54.26Z" }, - { url = "https://files.pythonhosted.org/packages/91/1c/eb97db117a1ebe46d457a3d235a7b9d2e6dcab174f42d1b67663dd9e5371/multidict-6.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159", size = 45117, upload-time = "2025-10-06T14:49:55.82Z" }, - { url = "https://files.pythonhosted.org/packages/f1/d8/6c3442322e41fb1dd4de8bd67bfd11cd72352ac131f6368315617de752f1/multidict-6.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca", size = 43472, upload-time = "2025-10-06T14:49:57.048Z" }, - { url = "https://files.pythonhosted.org/packages/75/3f/e2639e80325af0b6c6febdf8e57cc07043ff15f57fa1ef808f4ccb5ac4cd/multidict-6.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8", size = 249342, upload-time = "2025-10-06T14:49:58.368Z" }, - { url = "https://files.pythonhosted.org/packages/5d/cc/84e0585f805cbeaa9cbdaa95f9a3d6aed745b9d25700623ac89a6ecff400/multidict-6.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60", size = 257082, upload-time = "2025-10-06T14:49:59.89Z" }, - { url = "https://files.pythonhosted.org/packages/b0/9c/ac851c107c92289acbbf5cfb485694084690c1b17e555f44952c26ddc5bd/multidict-6.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4", size = 240704, upload-time = "2025-10-06T14:50:01.485Z" }, - { url = "https://files.pythonhosted.org/packages/50/cc/5f93e99427248c09da95b62d64b25748a5f5c98c7c2ab09825a1d6af0e15/multidict-6.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f", size = 266355, upload-time = "2025-10-06T14:50:02.955Z" }, - { url = "https://files.pythonhosted.org/packages/ec/0c/2ec1d883ceb79c6f7f6d7ad90c919c898f5d1c6ea96d322751420211e072/multidict-6.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf", size = 267259, upload-time = "2025-10-06T14:50:04.446Z" }, - { url = "https://files.pythonhosted.org/packages/c6/2d/f0b184fa88d6630aa267680bdb8623fb69cb0d024b8c6f0d23f9a0f406d3/multidict-6.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32", size = 254903, upload-time = "2025-10-06T14:50:05.98Z" }, - { url = "https://files.pythonhosted.org/packages/06/c9/11ea263ad0df7dfabcad404feb3c0dd40b131bc7f232d5537f2fb1356951/multidict-6.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036", size = 252365, upload-time = "2025-10-06T14:50:07.511Z" }, - { url = "https://files.pythonhosted.org/packages/41/88/d714b86ee2c17d6e09850c70c9d310abac3d808ab49dfa16b43aba9d53fd/multidict-6.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec", size = 250062, upload-time = "2025-10-06T14:50:09.074Z" }, - { url = "https://files.pythonhosted.org/packages/15/fe/ad407bb9e818c2b31383f6131ca19ea7e35ce93cf1310fce69f12e89de75/multidict-6.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e", size = 249683, upload-time = "2025-10-06T14:50:10.714Z" }, - { url = "https://files.pythonhosted.org/packages/8c/a4/a89abdb0229e533fb925e7c6e5c40201c2873efebc9abaf14046a4536ee6/multidict-6.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64", size = 261254, upload-time = "2025-10-06T14:50:12.28Z" }, - { url = "https://files.pythonhosted.org/packages/8d/aa/0e2b27bd88b40a4fb8dc53dd74eecac70edaa4c1dd0707eb2164da3675b3/multidict-6.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd", size = 257967, upload-time = "2025-10-06T14:50:14.16Z" }, - { url = "https://files.pythonhosted.org/packages/d0/8e/0c67b7120d5d5f6d874ed85a085f9dc770a7f9d8813e80f44a9fec820bb7/multidict-6.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288", size = 250085, upload-time = "2025-10-06T14:50:15.639Z" }, - { url = "https://files.pythonhosted.org/packages/ba/55/b73e1d624ea4b8fd4dd07a3bb70f6e4c7c6c5d9d640a41c6ffe5cdbd2a55/multidict-6.7.0-cp313-cp313-win32.whl", hash = "sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17", size = 41713, upload-time = "2025-10-06T14:50:17.066Z" }, - { url = "https://files.pythonhosted.org/packages/32/31/75c59e7d3b4205075b4c183fa4ca398a2daf2303ddf616b04ae6ef55cffe/multidict-6.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390", size = 45915, upload-time = "2025-10-06T14:50:18.264Z" }, - { url = "https://files.pythonhosted.org/packages/31/2a/8987831e811f1184c22bc2e45844934385363ee61c0a2dcfa8f71b87e608/multidict-6.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e", size = 43077, upload-time = "2025-10-06T14:50:19.853Z" }, - { url = "https://files.pythonhosted.org/packages/e8/68/7b3a5170a382a340147337b300b9eb25a9ddb573bcdfff19c0fa3f31ffba/multidict-6.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00", size = 83114, upload-time = "2025-10-06T14:50:21.223Z" }, - { url = "https://files.pythonhosted.org/packages/55/5c/3fa2d07c84df4e302060f555bbf539310980362236ad49f50eeb0a1c1eb9/multidict-6.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb", size = 48442, upload-time = "2025-10-06T14:50:22.871Z" }, - { url = "https://files.pythonhosted.org/packages/fc/56/67212d33239797f9bd91962bb899d72bb0f4c35a8652dcdb8ed049bef878/multidict-6.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b", size = 46885, upload-time = "2025-10-06T14:50:24.258Z" }, - { url = "https://files.pythonhosted.org/packages/46/d1/908f896224290350721597a61a69cd19b89ad8ee0ae1f38b3f5cd12ea2ac/multidict-6.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c", size = 242588, upload-time = "2025-10-06T14:50:25.716Z" }, - { url = "https://files.pythonhosted.org/packages/ab/67/8604288bbd68680eee0ab568fdcb56171d8b23a01bcd5cb0c8fedf6e5d99/multidict-6.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1", size = 249966, upload-time = "2025-10-06T14:50:28.192Z" }, - { url = "https://files.pythonhosted.org/packages/20/33/9228d76339f1ba51e3efef7da3ebd91964d3006217aae13211653193c3ff/multidict-6.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b", size = 228618, upload-time = "2025-10-06T14:50:29.82Z" }, - { url = "https://files.pythonhosted.org/packages/f8/2d/25d9b566d10cab1c42b3b9e5b11ef79c9111eaf4463b8c257a3bd89e0ead/multidict-6.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5", size = 257539, upload-time = "2025-10-06T14:50:31.731Z" }, - { url = "https://files.pythonhosted.org/packages/b6/b1/8d1a965e6637fc33de3c0d8f414485c2b7e4af00f42cab3d84e7b955c222/multidict-6.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad", size = 256345, upload-time = "2025-10-06T14:50:33.26Z" }, - { url = "https://files.pythonhosted.org/packages/ba/0c/06b5a8adbdeedada6f4fb8d8f193d44a347223b11939b42953eeb6530b6b/multidict-6.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c", size = 247934, upload-time = "2025-10-06T14:50:34.808Z" }, - { url = "https://files.pythonhosted.org/packages/8f/31/b2491b5fe167ca044c6eb4b8f2c9f3b8a00b24c432c365358eadac5d7625/multidict-6.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5", size = 245243, upload-time = "2025-10-06T14:50:36.436Z" }, - { url = "https://files.pythonhosted.org/packages/61/1a/982913957cb90406c8c94f53001abd9eafc271cb3e70ff6371590bec478e/multidict-6.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10", size = 235878, upload-time = "2025-10-06T14:50:37.953Z" }, - { url = "https://files.pythonhosted.org/packages/be/c0/21435d804c1a1cf7a2608593f4d19bca5bcbd7a81a70b253fdd1c12af9c0/multidict-6.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754", size = 243452, upload-time = "2025-10-06T14:50:39.574Z" }, - { url = "https://files.pythonhosted.org/packages/54/0a/4349d540d4a883863191be6eb9a928846d4ec0ea007d3dcd36323bb058ac/multidict-6.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c", size = 252312, upload-time = "2025-10-06T14:50:41.612Z" }, - { url = "https://files.pythonhosted.org/packages/26/64/d5416038dbda1488daf16b676e4dbfd9674dde10a0cc8f4fc2b502d8125d/multidict-6.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762", size = 246935, upload-time = "2025-10-06T14:50:43.972Z" }, - { url = "https://files.pythonhosted.org/packages/9f/8c/8290c50d14e49f35e0bd4abc25e1bc7711149ca9588ab7d04f886cdf03d9/multidict-6.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6", size = 243385, upload-time = "2025-10-06T14:50:45.648Z" }, - { url = "https://files.pythonhosted.org/packages/ef/a0/f83ae75e42d694b3fbad3e047670e511c138be747bc713cf1b10d5096416/multidict-6.7.0-cp313-cp313t-win32.whl", hash = "sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d", size = 47777, upload-time = "2025-10-06T14:50:47.154Z" }, - { url = "https://files.pythonhosted.org/packages/dc/80/9b174a92814a3830b7357307a792300f42c9e94664b01dee8e457551fa66/multidict-6.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6", size = 53104, upload-time = "2025-10-06T14:50:48.851Z" }, - { url = "https://files.pythonhosted.org/packages/cc/28/04baeaf0428d95bb7a7bea0e691ba2f31394338ba424fb0679a9ed0f4c09/multidict-6.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792", size = 45503, upload-time = "2025-10-06T14:50:50.16Z" }, - { url = "https://files.pythonhosted.org/packages/e2/b1/3da6934455dd4b261d4c72f897e3a5728eba81db59959f3a639245891baa/multidict-6.7.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842", size = 75128, upload-time = "2025-10-06T14:50:51.92Z" }, - { url = "https://files.pythonhosted.org/packages/14/2c/f069cab5b51d175a1a2cb4ccdf7a2c2dabd58aa5bd933fa036a8d15e2404/multidict-6.7.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b", size = 44410, upload-time = "2025-10-06T14:50:53.275Z" }, - { url = "https://files.pythonhosted.org/packages/42/e2/64bb41266427af6642b6b128e8774ed84c11b80a90702c13ac0a86bb10cc/multidict-6.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38", size = 43205, upload-time = "2025-10-06T14:50:54.911Z" }, - { url = "https://files.pythonhosted.org/packages/02/68/6b086fef8a3f1a8541b9236c594f0c9245617c29841f2e0395d979485cde/multidict-6.7.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128", size = 245084, upload-time = "2025-10-06T14:50:56.369Z" }, - { url = "https://files.pythonhosted.org/packages/15/ee/f524093232007cd7a75c1d132df70f235cfd590a7c9eaccd7ff422ef4ae8/multidict-6.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34", size = 252667, upload-time = "2025-10-06T14:50:57.991Z" }, - { url = "https://files.pythonhosted.org/packages/02/a5/eeb3f43ab45878f1895118c3ef157a480db58ede3f248e29b5354139c2c9/multidict-6.7.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99", size = 233590, upload-time = "2025-10-06T14:50:59.589Z" }, - { url = "https://files.pythonhosted.org/packages/6a/1e/76d02f8270b97269d7e3dbd45644b1785bda457b474315f8cf999525a193/multidict-6.7.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202", size = 264112, upload-time = "2025-10-06T14:51:01.183Z" }, - { url = "https://files.pythonhosted.org/packages/76/0b/c28a70ecb58963847c2a8efe334904cd254812b10e535aefb3bcce513918/multidict-6.7.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1", size = 261194, upload-time = "2025-10-06T14:51:02.794Z" }, - { url = "https://files.pythonhosted.org/packages/b4/63/2ab26e4209773223159b83aa32721b4021ffb08102f8ac7d689c943fded1/multidict-6.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3", size = 248510, upload-time = "2025-10-06T14:51:04.724Z" }, - { url = "https://files.pythonhosted.org/packages/93/cd/06c1fa8282af1d1c46fd55c10a7930af652afdce43999501d4d68664170c/multidict-6.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d", size = 248395, upload-time = "2025-10-06T14:51:06.306Z" }, - { url = "https://files.pythonhosted.org/packages/99/ac/82cb419dd6b04ccf9e7e61befc00c77614fc8134362488b553402ecd55ce/multidict-6.7.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6", size = 239520, upload-time = "2025-10-06T14:51:08.091Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f3/a0f9bf09493421bd8716a362e0cd1d244f5a6550f5beffdd6b47e885b331/multidict-6.7.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7", size = 245479, upload-time = "2025-10-06T14:51:10.365Z" }, - { url = "https://files.pythonhosted.org/packages/8d/01/476d38fc73a212843f43c852b0eee266b6971f0e28329c2184a8df90c376/multidict-6.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb", size = 258903, upload-time = "2025-10-06T14:51:12.466Z" }, - { url = "https://files.pythonhosted.org/packages/49/6d/23faeb0868adba613b817d0e69c5f15531b24d462af8012c4f6de4fa8dc3/multidict-6.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f", size = 252333, upload-time = "2025-10-06T14:51:14.48Z" }, - { url = "https://files.pythonhosted.org/packages/1e/cc/48d02ac22b30fa247f7dad82866e4b1015431092f4ba6ebc7e77596e0b18/multidict-6.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f", size = 243411, upload-time = "2025-10-06T14:51:16.072Z" }, - { url = "https://files.pythonhosted.org/packages/4a/03/29a8bf5a18abf1fe34535c88adbdfa88c9fb869b5a3b120692c64abe8284/multidict-6.7.0-cp314-cp314-win32.whl", hash = "sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885", size = 40940, upload-time = "2025-10-06T14:51:17.544Z" }, - { url = "https://files.pythonhosted.org/packages/82/16/7ed27b680791b939de138f906d5cf2b4657b0d45ca6f5dd6236fdddafb1a/multidict-6.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c", size = 45087, upload-time = "2025-10-06T14:51:18.875Z" }, - { url = "https://files.pythonhosted.org/packages/cd/3c/e3e62eb35a1950292fe39315d3c89941e30a9d07d5d2df42965ab041da43/multidict-6.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000", size = 42368, upload-time = "2025-10-06T14:51:20.225Z" }, - { url = "https://files.pythonhosted.org/packages/8b/40/cd499bd0dbc5f1136726db3153042a735fffd0d77268e2ee20d5f33c010f/multidict-6.7.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63", size = 82326, upload-time = "2025-10-06T14:51:21.588Z" }, - { url = "https://files.pythonhosted.org/packages/13/8a/18e031eca251c8df76daf0288e6790561806e439f5ce99a170b4af30676b/multidict-6.7.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718", size = 48065, upload-time = "2025-10-06T14:51:22.93Z" }, - { url = "https://files.pythonhosted.org/packages/40/71/5e6701277470a87d234e433fb0a3a7deaf3bcd92566e421e7ae9776319de/multidict-6.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2", size = 46475, upload-time = "2025-10-06T14:51:24.352Z" }, - { url = "https://files.pythonhosted.org/packages/fe/6a/bab00cbab6d9cfb57afe1663318f72ec28289ea03fd4e8236bb78429893a/multidict-6.7.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e", size = 239324, upload-time = "2025-10-06T14:51:25.822Z" }, - { url = "https://files.pythonhosted.org/packages/2a/5f/8de95f629fc22a7769ade8b41028e3e5a822c1f8904f618d175945a81ad3/multidict-6.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064", size = 246877, upload-time = "2025-10-06T14:51:27.604Z" }, - { url = "https://files.pythonhosted.org/packages/23/b4/38881a960458f25b89e9f4a4fdcb02ac101cfa710190db6e5528841e67de/multidict-6.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e", size = 225824, upload-time = "2025-10-06T14:51:29.664Z" }, - { url = "https://files.pythonhosted.org/packages/1e/39/6566210c83f8a261575f18e7144736059f0c460b362e96e9cf797a24b8e7/multidict-6.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd", size = 253558, upload-time = "2025-10-06T14:51:31.684Z" }, - { url = "https://files.pythonhosted.org/packages/00/a3/67f18315100f64c269f46e6c0319fa87ba68f0f64f2b8e7fd7c72b913a0b/multidict-6.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a", size = 252339, upload-time = "2025-10-06T14:51:33.699Z" }, - { url = "https://files.pythonhosted.org/packages/c8/2a/1cb77266afee2458d82f50da41beba02159b1d6b1f7973afc9a1cad1499b/multidict-6.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96", size = 244895, upload-time = "2025-10-06T14:51:36.189Z" }, - { url = "https://files.pythonhosted.org/packages/dd/72/09fa7dd487f119b2eb9524946ddd36e2067c08510576d43ff68469563b3b/multidict-6.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e", size = 241862, upload-time = "2025-10-06T14:51:41.291Z" }, - { url = "https://files.pythonhosted.org/packages/65/92/bc1f8bd0853d8669300f732c801974dfc3702c3eeadae2f60cef54dc69d7/multidict-6.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599", size = 232376, upload-time = "2025-10-06T14:51:43.55Z" }, - { url = "https://files.pythonhosted.org/packages/09/86/ac39399e5cb9d0c2ac8ef6e10a768e4d3bc933ac808d49c41f9dc23337eb/multidict-6.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394", size = 240272, upload-time = "2025-10-06T14:51:45.265Z" }, - { url = "https://files.pythonhosted.org/packages/3d/b6/fed5ac6b8563ec72df6cb1ea8dac6d17f0a4a1f65045f66b6d3bf1497c02/multidict-6.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38", size = 248774, upload-time = "2025-10-06T14:51:46.836Z" }, - { url = "https://files.pythonhosted.org/packages/6b/8d/b954d8c0dc132b68f760aefd45870978deec6818897389dace00fcde32ff/multidict-6.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9", size = 242731, upload-time = "2025-10-06T14:51:48.541Z" }, - { url = "https://files.pythonhosted.org/packages/16/9d/a2dac7009125d3540c2f54e194829ea18ac53716c61b655d8ed300120b0f/multidict-6.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0", size = 240193, upload-time = "2025-10-06T14:51:50.355Z" }, - { url = "https://files.pythonhosted.org/packages/39/ca/c05f144128ea232ae2178b008d5011d4e2cea86e4ee8c85c2631b1b94802/multidict-6.7.0-cp314-cp314t-win32.whl", hash = "sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13", size = 48023, upload-time = "2025-10-06T14:51:51.883Z" }, - { url = "https://files.pythonhosted.org/packages/ba/8f/0a60e501584145588be1af5cc829265701ba3c35a64aec8e07cbb71d39bb/multidict-6.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd", size = 53507, upload-time = "2025-10-06T14:51:53.672Z" }, - { url = "https://files.pythonhosted.org/packages/7f/ae/3148b988a9c6239903e786eac19c889fab607c31d6efa7fb2147e5680f23/multidict-6.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827", size = 44804, upload-time = "2025-10-06T14:51:55.415Z" }, - { url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317, upload-time = "2025-10-06T14:52:29.272Z" }, +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/f1/a90635c4f88fb913fbf4ce660b83b7445b7a02615bda034b2f8eb38fd597/multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", size = 76626, upload-time = "2026-01-26T02:43:26.485Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/267e64eaf6fc637a15b35f5de31a566634a2740f97d8d094a69d34f524a4/multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e", size = 44706, upload-time = "2026-01-26T02:43:27.607Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", size = 44356, upload-time = "2026-01-26T02:43:28.661Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d2/0a36c8473f0cbaeadd5db6c8b72d15bbceeec275807772bfcd059bef487d/multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3", size = 244355, upload-time = "2026-01-26T02:43:31.165Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/8c65be997fd7dd311b7d39c7b6e71a0cb449bad093761481eccbbe4b42a2/multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e", size = 246433, upload-time = "2026-01-26T02:43:32.581Z" }, + { url = "https://files.pythonhosted.org/packages/01/fb/4dbd7e848d2799c6a026ec88ad39cf2b8416aa167fcc903baa55ecaa045c/multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a", size = 225376, upload-time = "2026-01-26T02:43:34.417Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8a/4a3a6341eac3830f6053062f8fbc9a9e54407c80755b3f05bc427295c2d0/multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8", size = 257365, upload-time = "2026-01-26T02:43:35.741Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a2/dd575a69c1aa206e12d27d0770cdf9b92434b48a9ef0cd0d1afdecaa93c4/multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0", size = 254747, upload-time = "2026-01-26T02:43:36.976Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144", size = 246293, upload-time = "2026-01-26T02:43:38.258Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a4/23466059dc3854763423d0ad6c0f3683a379d97673b1b89ec33826e46728/multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49", size = 242962, upload-time = "2026-01-26T02:43:40.034Z" }, + { url = "https://files.pythonhosted.org/packages/1f/67/51dd754a3524d685958001e8fa20a0f5f90a6a856e0a9dcabff69be3dbb7/multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71", size = 237360, upload-time = "2026-01-26T02:43:41.752Z" }, + { url = "https://files.pythonhosted.org/packages/64/3f/036dfc8c174934d4b55d86ff4f978e558b0e585cef70cfc1ad01adc6bf18/multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3", size = 245940, upload-time = "2026-01-26T02:43:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/3d/20/6214d3c105928ebc353a1c644a6ef1408bc5794fcb4f170bb524a3c16311/multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c", size = 253502, upload-time = "2026-01-26T02:43:44.371Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e2/c653bc4ae1be70a0f836b82172d643fcf1dade042ba2676ab08ec08bff0f/multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0", size = 247065, upload-time = "2026-01-26T02:43:45.745Z" }, + { url = "https://files.pythonhosted.org/packages/c8/11/a854b4154cd3bd8b1fd375e8a8ca9d73be37610c361543d56f764109509b/multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa", size = 241870, upload-time = "2026-01-26T02:43:47.054Z" }, + { url = "https://files.pythonhosted.org/packages/13/bf/9676c0392309b5fdae322333d22a829715b570edb9baa8016a517b55b558/multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a", size = 41302, upload-time = "2026-01-26T02:43:48.753Z" }, + { url = "https://files.pythonhosted.org/packages/c9/68/f16a3a8ba6f7b6dc92a1f19669c0810bd2c43fc5a02da13b1cbf8e253845/multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b", size = 45981, upload-time = "2026-01-26T02:43:49.921Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/9dd5305253fa00cd3c7555dbef69d5bf4133debc53b87ab8d6a44d411665/multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6", size = 43159, upload-time = "2026-01-26T02:43:51.635Z" }, + { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, + { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, + { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, + { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, + { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, + { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, + { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, + { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, + { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, + { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, + { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, + { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, + { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, + { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, + { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, + { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, + { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, + { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, ] [[package]] @@ -2212,11 +2190,11 @@ wheels = [ [[package]] name = "packaging" -version = "25.0" +version = "26.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] [[package]] @@ -2230,56 +2208,62 @@ wheels = [ [[package]] name = "pandas" -version = "2.3.3" +version = "3.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, { name = "python-dateutil" }, - { name = "pytz" }, - { name = "tzdata" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" }, - { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" }, - { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" }, - { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" }, - { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" }, - { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" }, - { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" }, - { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, - { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, - { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, - { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, - { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, - { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, - { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, - { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" }, - { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" }, - { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" }, - { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" }, - { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" }, - { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" }, - { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" }, - { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" }, - { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" }, - { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" }, - { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" }, - { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" }, - { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" }, - { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" }, - { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" }, - { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" }, - { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" }, - { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" }, - { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" }, - { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" }, - { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" }, - { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" }, - { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" }, - { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" }, - { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" }, - { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/da/b1dc0481ab8d55d0f46e343cfe67d4551a0e14fcee52bd38ca1bd73258d8/pandas-3.0.0.tar.gz", hash = "sha256:0facf7e87d38f721f0af46fe70d97373a37701b1c09f7ed7aeeb292ade5c050f", size = 4633005, upload-time = "2026-01-21T15:52:04.726Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/1e/b184654a856e75e975a6ee95d6577b51c271cd92cb2b020c9378f53e0032/pandas-3.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d64ce01eb9cdca96a15266aa679ae50212ec52757c79204dbc7701a222401850", size = 10313247, upload-time = "2026-01-21T15:50:15.775Z" }, + { url = "https://files.pythonhosted.org/packages/dd/5e/e04a547ad0f0183bf151fd7c7a477468e3b85ff2ad231c566389e6cc9587/pandas-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:613e13426069793aa1ec53bdcc3b86e8d32071daea138bbcf4fa959c9cdaa2e2", size = 9913131, upload-time = "2026-01-21T15:50:18.611Z" }, + { url = "https://files.pythonhosted.org/packages/a2/93/bb77bfa9fc2aba9f7204db807d5d3fb69832ed2854c60ba91b4c65ba9219/pandas-3.0.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0192fee1f1a8e743b464a6607858ee4b071deb0b118eb143d71c2a1d170996d5", size = 10741925, upload-time = "2026-01-21T15:50:21.058Z" }, + { url = "https://files.pythonhosted.org/packages/62/fb/89319812eb1d714bfc04b7f177895caeba8ab4a37ef6712db75ed786e2e0/pandas-3.0.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0b853319dec8d5e0c8b875374c078ef17f2269986a78168d9bd57e49bf650ae", size = 11245979, upload-time = "2026-01-21T15:50:23.413Z" }, + { url = "https://files.pythonhosted.org/packages/a9/63/684120486f541fc88da3862ed31165b3b3e12b6a1c7b93be4597bc84e26c/pandas-3.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:707a9a877a876c326ae2cb640fbdc4ef63b0a7b9e2ef55c6df9942dcee8e2af9", size = 11756337, upload-time = "2026-01-21T15:50:25.932Z" }, + { url = "https://files.pythonhosted.org/packages/39/92/7eb0ad232312b59aec61550c3c81ad0743898d10af5df7f80bc5e5065416/pandas-3.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:afd0aa3d0b5cda6e0b8ffc10dbcca3b09ef3cbcd3fe2b27364f85fdc04e1989d", size = 12325517, upload-time = "2026-01-21T15:50:27.952Z" }, + { url = "https://files.pythonhosted.org/packages/51/27/bf9436dd0a4fc3130acec0828951c7ef96a0631969613a9a35744baf27f6/pandas-3.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:113b4cca2614ff7e5b9fee9b6f066618fe73c5a83e99d721ffc41217b2bf57dd", size = 9881576, upload-time = "2026-01-21T15:50:30.149Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2b/c618b871fce0159fd107516336e82891b404e3f340821853c2fc28c7830f/pandas-3.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c14837eba8e99a8da1527c0280bba29b0eb842f64aa94982c5e21227966e164b", size = 9140807, upload-time = "2026-01-21T15:50:32.308Z" }, + { url = "https://files.pythonhosted.org/packages/0b/38/db33686f4b5fa64d7af40d96361f6a4615b8c6c8f1b3d334eee46ae6160e/pandas-3.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9803b31f5039b3c3b10cc858c5e40054adb4b29b4d81cb2fd789f4121c8efbcd", size = 10334013, upload-time = "2026-01-21T15:50:34.771Z" }, + { url = "https://files.pythonhosted.org/packages/a5/7b/9254310594e9774906bacdd4e732415e1f86ab7dbb4b377ef9ede58cd8ec/pandas-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:14c2a4099cd38a1d18ff108168ea417909b2dea3bd1ebff2ccf28ddb6a74d740", size = 9874154, upload-time = "2026-01-21T15:50:36.67Z" }, + { url = "https://files.pythonhosted.org/packages/63/d4/726c5a67a13bc66643e66d2e9ff115cead482a44fc56991d0c4014f15aaf/pandas-3.0.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d257699b9a9960e6125686098d5714ac59d05222bef7a5e6af7a7fd87c650801", size = 10384433, upload-time = "2026-01-21T15:50:39.132Z" }, + { url = "https://files.pythonhosted.org/packages/bf/2e/9211f09bedb04f9832122942de8b051804b31a39cfbad199a819bb88d9f3/pandas-3.0.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:69780c98f286076dcafca38d8b8eee1676adf220199c0a39f0ecbf976b68151a", size = 10864519, upload-time = "2026-01-21T15:50:41.043Z" }, + { url = "https://files.pythonhosted.org/packages/00/8d/50858522cdc46ac88b9afdc3015e298959a70a08cd21e008a44e9520180c/pandas-3.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4a66384f017240f3858a4c8a7cf21b0591c3ac885cddb7758a589f0f71e87ebb", size = 11394124, upload-time = "2026-01-21T15:50:43.377Z" }, + { url = "https://files.pythonhosted.org/packages/86/3f/83b2577db02503cd93d8e95b0f794ad9d4be0ba7cb6c8bcdcac964a34a42/pandas-3.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be8c515c9bc33989d97b89db66ea0cececb0f6e3c2a87fcc8b69443a6923e95f", size = 11920444, upload-time = "2026-01-21T15:50:45.932Z" }, + { url = "https://files.pythonhosted.org/packages/64/2d/4f8a2f192ed12c90a0aab47f5557ece0e56b0370c49de9454a09de7381b2/pandas-3.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:a453aad8c4f4e9f166436994a33884442ea62aa8b27d007311e87521b97246e1", size = 9730970, upload-time = "2026-01-21T15:50:47.962Z" }, + { url = "https://files.pythonhosted.org/packages/d4/64/ff571be435cf1e643ca98d0945d76732c0b4e9c37191a89c8550b105eed1/pandas-3.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:da768007b5a33057f6d9053563d6b74dd6d029c337d93c6d0d22a763a5c2ecc0", size = 9041950, upload-time = "2026-01-21T15:50:50.422Z" }, + { url = "https://files.pythonhosted.org/packages/6f/fa/7f0ac4ca8877c57537aaff2a842f8760e630d8e824b730eb2e859ffe96ca/pandas-3.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b78d646249b9a2bc191040988c7bb524c92fa8534fb0898a0741d7e6f2ffafa6", size = 10307129, upload-time = "2026-01-21T15:50:52.877Z" }, + { url = "https://files.pythonhosted.org/packages/6f/11/28a221815dcea4c0c9414dfc845e34a84a6a7dabc6da3194498ed5ba4361/pandas-3.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bc9cba7b355cb4162442a88ce495e01cb605f17ac1e27d6596ac963504e0305f", size = 9850201, upload-time = "2026-01-21T15:50:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/ba/da/53bbc8c5363b7e5bd10f9ae59ab250fc7a382ea6ba08e4d06d8694370354/pandas-3.0.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c9a1a149aed3b6c9bf246033ff91e1b02d529546c5d6fb6b74a28fea0cf4c70", size = 10354031, upload-time = "2026-01-21T15:50:57.463Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a3/51e02ebc2a14974170d51e2410dfdab58870ea9bcd37cda15bd553d24dc4/pandas-3.0.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95683af6175d884ee89471842acfca29172a85031fccdabc35e50c0984470a0e", size = 10861165, upload-time = "2026-01-21T15:50:59.32Z" }, + { url = "https://files.pythonhosted.org/packages/a5/fe/05a51e3cac11d161472b8297bd41723ea98013384dd6d76d115ce3482f9b/pandas-3.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1fbbb5a7288719e36b76b4f18d46ede46e7f916b6c8d9915b756b0a6c3f792b3", size = 11359359, upload-time = "2026-01-21T15:51:02.014Z" }, + { url = "https://files.pythonhosted.org/packages/ee/56/ba620583225f9b85a4d3e69c01df3e3870659cc525f67929b60e9f21dcd1/pandas-3.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8e8b9808590fa364416b49b2a35c1f4cf2785a6c156935879e57f826df22038e", size = 11912907, upload-time = "2026-01-21T15:51:05.175Z" }, + { url = "https://files.pythonhosted.org/packages/c9/8c/c6638d9f67e45e07656b3826405c5cc5f57f6fd07c8b2572ade328c86e22/pandas-3.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:98212a38a709feb90ae658cb6227ea3657c22ba8157d4b8f913cd4c950de5e7e", size = 9732138, upload-time = "2026-01-21T15:51:07.569Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bf/bd1335c3bf1770b6d8fed2799993b11c4971af93bb1b729b9ebbc02ca2ec/pandas-3.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:177d9df10b3f43b70307a149d7ec49a1229a653f907aa60a48f1877d0e6be3be", size = 9033568, upload-time = "2026-01-21T15:51:09.484Z" }, + { url = "https://files.pythonhosted.org/packages/8e/c6/f5e2171914d5e29b9171d495344097d54e3ffe41d2d85d8115baba4dc483/pandas-3.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2713810ad3806767b89ad3b7b69ba153e1c6ff6d9c20f9c2140379b2a98b6c98", size = 10741936, upload-time = "2026-01-21T15:51:11.693Z" }, + { url = "https://files.pythonhosted.org/packages/51/88/9a0164f99510a1acb9f548691f022c756c2314aad0d8330a24616c14c462/pandas-3.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:15d59f885ee5011daf8335dff47dcb8a912a27b4ad7826dc6cbe809fd145d327", size = 10393884, upload-time = "2026-01-21T15:51:14.197Z" }, + { url = "https://files.pythonhosted.org/packages/e0/53/b34d78084d88d8ae2b848591229da8826d1e65aacf00b3abe34023467648/pandas-3.0.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24e6547fb64d2c92665dd2adbfa4e85fa4fd70a9c070e7cfb03b629a0bbab5eb", size = 10310740, upload-time = "2026-01-21T15:51:16.093Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d3/bee792e7c3d6930b74468d990604325701412e55d7aaf47460a22311d1a5/pandas-3.0.0-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48ee04b90e2505c693d3f8e8f524dab8cb8aaf7ddcab52c92afa535e717c4812", size = 10700014, upload-time = "2026-01-21T15:51:18.818Z" }, + { url = "https://files.pythonhosted.org/packages/55/db/2570bc40fb13aaed1cbc3fbd725c3a60ee162477982123c3adc8971e7ac1/pandas-3.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66f72fb172959af42a459e27a8d8d2c7e311ff4c1f7db6deb3b643dbc382ae08", size = 11323737, upload-time = "2026-01-21T15:51:20.784Z" }, + { url = "https://files.pythonhosted.org/packages/bc/2e/297ac7f21c8181b62a4cccebad0a70caf679adf3ae5e83cb676194c8acc3/pandas-3.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4a4a400ca18230976724a5066f20878af785f36c6756e498e94c2a5e5d57779c", size = 11771558, upload-time = "2026-01-21T15:51:22.977Z" }, + { url = "https://files.pythonhosted.org/packages/0a/46/e1c6876d71c14332be70239acce9ad435975a80541086e5ffba2f249bcf6/pandas-3.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:940eebffe55528074341a5a36515f3e4c5e25e958ebbc764c9502cfc35ba3faa", size = 10473771, upload-time = "2026-01-21T15:51:25.285Z" }, + { url = "https://files.pythonhosted.org/packages/c0/db/0270ad9d13c344b7a36fa77f5f8344a46501abf413803e885d22864d10bf/pandas-3.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:597c08fb9fef0edf1e4fa2f9828dd27f3d78f9b8c9b4a748d435ffc55732310b", size = 10312075, upload-time = "2026-01-21T15:51:28.5Z" }, + { url = "https://files.pythonhosted.org/packages/09/9f/c176f5e9717f7c91becfe0f55a52ae445d3f7326b4a2cf355978c51b7913/pandas-3.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:447b2d68ac5edcbf94655fe909113a6dba6ef09ad7f9f60c80477825b6c489fe", size = 9900213, upload-time = "2026-01-21T15:51:30.955Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e7/63ad4cc10b257b143e0a5ebb04304ad806b4e1a61c5da25f55896d2ca0f4/pandas-3.0.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:debb95c77ff3ed3ba0d9aa20c3a2f19165cc7956362f9873fce1ba0a53819d70", size = 10428768, upload-time = "2026-01-21T15:51:33.018Z" }, + { url = "https://files.pythonhosted.org/packages/9e/0e/4e4c2d8210f20149fd2248ef3fff26623604922bd564d915f935a06dd63d/pandas-3.0.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fedabf175e7cd82b69b74c30adbaa616de301291a5231138d7242596fc296a8d", size = 10882954, upload-time = "2026-01-21T15:51:35.287Z" }, + { url = "https://files.pythonhosted.org/packages/c6/60/c9de8ac906ba1f4d2250f8a951abe5135b404227a55858a75ad26f84db47/pandas-3.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:412d1a89aab46889f3033a386912efcdfa0f1131c5705ff5b668dda88305e986", size = 11430293, upload-time = "2026-01-21T15:51:37.57Z" }, + { url = "https://files.pythonhosted.org/packages/a1/69/806e6637c70920e5787a6d6896fd707f8134c2c55cd761e7249a97b7dc5a/pandas-3.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e979d22316f9350c516479dd3a92252be2937a9531ed3a26ec324198a99cdd49", size = 11952452, upload-time = "2026-01-21T15:51:39.618Z" }, + { url = "https://files.pythonhosted.org/packages/cb/de/918621e46af55164c400ab0ef389c9d969ab85a43d59ad1207d4ddbe30a5/pandas-3.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:083b11415b9970b6e7888800c43c82e81a06cd6b06755d84804444f0007d6bb7", size = 9851081, upload-time = "2026-01-21T15:51:41.758Z" }, + { url = "https://files.pythonhosted.org/packages/91/a1/3562a18dd0bd8c73344bfa26ff90c53c72f827df119d6d6b1dacc84d13e3/pandas-3.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:5db1e62cb99e739fa78a28047e861b256d17f88463c76b8dafc7c1338086dca8", size = 9174610, upload-time = "2026-01-21T15:51:44.312Z" }, + { url = "https://files.pythonhosted.org/packages/ce/26/430d91257eaf366f1737d7a1c158677caaf6267f338ec74e3a1ec444111c/pandas-3.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:697b8f7d346c68274b1b93a170a70974cdc7d7354429894d5927c1effdcccd73", size = 10761999, upload-time = "2026-01-21T15:51:46.899Z" }, + { url = "https://files.pythonhosted.org/packages/ec/1a/954eb47736c2b7f7fe6a9d56b0cb6987773c00faa3c6451a43db4beb3254/pandas-3.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8cb3120f0d9467ed95e77f67a75e030b67545bcfa08964e349252d674171def2", size = 10410279, upload-time = "2026-01-21T15:51:48.89Z" }, + { url = "https://files.pythonhosted.org/packages/20/fc/b96f3a5a28b250cd1b366eb0108df2501c0f38314a00847242abab71bb3a/pandas-3.0.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33fd3e6baa72899746b820c31e4b9688c8e1b7864d7aec2de7ab5035c285277a", size = 10330198, upload-time = "2026-01-21T15:51:51.015Z" }, + { url = "https://files.pythonhosted.org/packages/90/b3/d0e2952f103b4fbef1ef22d0c2e314e74fc9064b51cee30890b5e3286ee6/pandas-3.0.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8942e333dc67ceda1095227ad0febb05a3b36535e520154085db632c40ad084", size = 10728513, upload-time = "2026-01-21T15:51:53.387Z" }, + { url = "https://files.pythonhosted.org/packages/76/81/832894f286df828993dc5fd61c63b231b0fb73377e99f6c6c369174cf97e/pandas-3.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:783ac35c4d0fe0effdb0d67161859078618b1b6587a1af15928137525217a721", size = 11345550, upload-time = "2026-01-21T15:51:55.329Z" }, + { url = "https://files.pythonhosted.org/packages/34/a0/ed160a00fb4f37d806406bc0a79a8b62fe67f29d00950f8d16203ff3409b/pandas-3.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:125eb901e233f155b268bbef9abd9afb5819db74f0e677e89a61b246228c71ac", size = 11799386, upload-time = "2026-01-21T15:51:57.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/c8/2ac00d7255252c5e3cf61b35ca92ca25704b0188f7454ca4aec08a33cece/pandas-3.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b86d113b6c109df3ce0ad5abbc259fe86a1bd4adfd4a31a89da42f84f65509bb", size = 10873041, upload-time = "2026-01-21T15:52:00.034Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3f/a80ac00acbc6b35166b42850e98a4f466e2c0d9c64054161ba9620f95680/pandas-3.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:1c39eab3ad38f2d7a249095f0a3d8f8c22cc0f847e98ccf5bbe732b272e2d9fa", size = 9441003, upload-time = "2026-01-21T15:52:02.281Z" }, ] [[package]] @@ -2302,11 +2286,11 @@ wheels = [ [[package]] name = "pathspec" -version = "0.12.1" +version = "1.0.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, ] [[package]] @@ -2314,7 +2298,7 @@ name = "pexpect" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ptyprocess" }, + { name = "ptyprocess", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } wheels = [ @@ -2323,98 +2307,98 @@ wheels = [ [[package]] name = "pillow" -version = "12.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/5a/a2f6773b64edb921a756eb0729068acad9fc5208a53f4a349396e9436721/pillow-12.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc", size = 5289798, upload-time = "2025-10-15T18:21:47.763Z" }, - { url = "https://files.pythonhosted.org/packages/2e/05/069b1f8a2e4b5a37493da6c5868531c3f77b85e716ad7a590ef87d58730d/pillow-12.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257", size = 4650589, upload-time = "2025-10-15T18:21:49.515Z" }, - { url = "https://files.pythonhosted.org/packages/61/e3/2c820d6e9a36432503ead175ae294f96861b07600a7156154a086ba7111a/pillow-12.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642", size = 6230472, upload-time = "2025-10-15T18:21:51.052Z" }, - { url = "https://files.pythonhosted.org/packages/4f/89/63427f51c64209c5e23d4d52071c8d0f21024d3a8a487737caaf614a5795/pillow-12.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3", size = 8033887, upload-time = "2025-10-15T18:21:52.604Z" }, - { url = "https://files.pythonhosted.org/packages/f6/1b/c9711318d4901093c15840f268ad649459cd81984c9ec9887756cca049a5/pillow-12.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c", size = 6343964, upload-time = "2025-10-15T18:21:54.619Z" }, - { url = "https://files.pythonhosted.org/packages/41/1e/db9470f2d030b4995083044cd8738cdd1bf773106819f6d8ba12597d5352/pillow-12.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227", size = 7034756, upload-time = "2025-10-15T18:21:56.151Z" }, - { url = "https://files.pythonhosted.org/packages/cc/b0/6177a8bdd5ee4ed87cba2de5a3cc1db55ffbbec6176784ce5bb75aa96798/pillow-12.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b", size = 6458075, upload-time = "2025-10-15T18:21:57.759Z" }, - { url = "https://files.pythonhosted.org/packages/bc/5e/61537aa6fa977922c6a03253a0e727e6e4a72381a80d63ad8eec350684f2/pillow-12.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e", size = 7125955, upload-time = "2025-10-15T18:21:59.372Z" }, - { url = "https://files.pythonhosted.org/packages/1f/3d/d5033539344ee3cbd9a4d69e12e63ca3a44a739eb2d4c8da350a3d38edd7/pillow-12.0.0-cp311-cp311-win32.whl", hash = "sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739", size = 6298440, upload-time = "2025-10-15T18:22:00.982Z" }, - { url = "https://files.pythonhosted.org/packages/4d/42/aaca386de5cc8bd8a0254516957c1f265e3521c91515b16e286c662854c4/pillow-12.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e", size = 6999256, upload-time = "2025-10-15T18:22:02.617Z" }, - { url = "https://files.pythonhosted.org/packages/ba/f1/9197c9c2d5708b785f631a6dfbfa8eb3fb9672837cb92ae9af812c13b4ed/pillow-12.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d", size = 2436025, upload-time = "2025-10-15T18:22:04.598Z" }, - { url = "https://files.pythonhosted.org/packages/2c/90/4fcce2c22caf044e660a198d740e7fbc14395619e3cb1abad12192c0826c/pillow-12.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371", size = 5249377, upload-time = "2025-10-15T18:22:05.993Z" }, - { url = "https://files.pythonhosted.org/packages/fd/e0/ed960067543d080691d47d6938ebccbf3976a931c9567ab2fbfab983a5dd/pillow-12.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082", size = 4650343, upload-time = "2025-10-15T18:22:07.718Z" }, - { url = "https://files.pythonhosted.org/packages/e7/a1/f81fdeddcb99c044bf7d6faa47e12850f13cee0849537a7d27eeab5534d4/pillow-12.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f", size = 6232981, upload-time = "2025-10-15T18:22:09.287Z" }, - { url = "https://files.pythonhosted.org/packages/88/e1/9098d3ce341a8750b55b0e00c03f1630d6178f38ac191c81c97a3b047b44/pillow-12.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d", size = 8041399, upload-time = "2025-10-15T18:22:10.872Z" }, - { url = "https://files.pythonhosted.org/packages/a7/62/a22e8d3b602ae8cc01446d0c57a54e982737f44b6f2e1e019a925143771d/pillow-12.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953", size = 6347740, upload-time = "2025-10-15T18:22:12.769Z" }, - { url = "https://files.pythonhosted.org/packages/4f/87/424511bdcd02c8d7acf9f65caa09f291a519b16bd83c3fb3374b3d4ae951/pillow-12.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8", size = 7040201, upload-time = "2025-10-15T18:22:14.813Z" }, - { url = "https://files.pythonhosted.org/packages/dc/4d/435c8ac688c54d11755aedfdd9f29c9eeddf68d150fe42d1d3dbd2365149/pillow-12.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79", size = 6462334, upload-time = "2025-10-15T18:22:16.375Z" }, - { url = "https://files.pythonhosted.org/packages/2b/f2/ad34167a8059a59b8ad10bc5c72d4d9b35acc6b7c0877af8ac885b5f2044/pillow-12.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba", size = 7134162, upload-time = "2025-10-15T18:22:17.996Z" }, - { url = "https://files.pythonhosted.org/packages/0c/b1/a7391df6adacf0a5c2cf6ac1cf1fcc1369e7d439d28f637a847f8803beb3/pillow-12.0.0-cp312-cp312-win32.whl", hash = "sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0", size = 6298769, upload-time = "2025-10-15T18:22:19.923Z" }, - { url = "https://files.pythonhosted.org/packages/a2/0b/d87733741526541c909bbf159e338dcace4f982daac6e5a8d6be225ca32d/pillow-12.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a", size = 7001107, upload-time = "2025-10-15T18:22:21.644Z" }, - { url = "https://files.pythonhosted.org/packages/bc/96/aaa61ce33cc98421fb6088af2a03be4157b1e7e0e87087c888e2370a7f45/pillow-12.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad", size = 2436012, upload-time = "2025-10-15T18:22:23.621Z" }, - { url = "https://files.pythonhosted.org/packages/62/f2/de993bb2d21b33a98d031ecf6a978e4b61da207bef02f7b43093774c480d/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643", size = 4045493, upload-time = "2025-10-15T18:22:25.758Z" }, - { url = "https://files.pythonhosted.org/packages/0e/b6/bc8d0c4c9f6f111a783d045310945deb769b806d7574764234ffd50bc5ea/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4", size = 4120461, upload-time = "2025-10-15T18:22:27.286Z" }, - { url = "https://files.pythonhosted.org/packages/5d/57/d60d343709366a353dc56adb4ee1e7d8a2cc34e3fbc22905f4167cfec119/pillow-12.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399", size = 3576912, upload-time = "2025-10-15T18:22:28.751Z" }, - { url = "https://files.pythonhosted.org/packages/a4/a4/a0a31467e3f83b94d37568294b01d22b43ae3c5d85f2811769b9c66389dd/pillow-12.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5", size = 5249132, upload-time = "2025-10-15T18:22:30.641Z" }, - { url = "https://files.pythonhosted.org/packages/83/06/48eab21dd561de2914242711434c0c0eb992ed08ff3f6107a5f44527f5e9/pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b", size = 4650099, upload-time = "2025-10-15T18:22:32.73Z" }, - { url = "https://files.pythonhosted.org/packages/fc/bd/69ed99fd46a8dba7c1887156d3572fe4484e3f031405fcc5a92e31c04035/pillow-12.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3", size = 6230808, upload-time = "2025-10-15T18:22:34.337Z" }, - { url = "https://files.pythonhosted.org/packages/ea/94/8fad659bcdbf86ed70099cb60ae40be6acca434bbc8c4c0d4ef356d7e0de/pillow-12.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07", size = 8037804, upload-time = "2025-10-15T18:22:36.402Z" }, - { url = "https://files.pythonhosted.org/packages/20/39/c685d05c06deecfd4e2d1950e9a908aa2ca8bc4e6c3b12d93b9cafbd7837/pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e", size = 6345553, upload-time = "2025-10-15T18:22:38.066Z" }, - { url = "https://files.pythonhosted.org/packages/38/57/755dbd06530a27a5ed74f8cb0a7a44a21722ebf318edbe67ddbd7fb28f88/pillow-12.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344", size = 7037729, upload-time = "2025-10-15T18:22:39.769Z" }, - { url = "https://files.pythonhosted.org/packages/ca/b6/7e94f4c41d238615674d06ed677c14883103dce1c52e4af16f000338cfd7/pillow-12.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27", size = 6459789, upload-time = "2025-10-15T18:22:41.437Z" }, - { url = "https://files.pythonhosted.org/packages/9c/14/4448bb0b5e0f22dd865290536d20ec8a23b64e2d04280b89139f09a36bb6/pillow-12.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79", size = 7130917, upload-time = "2025-10-15T18:22:43.152Z" }, - { url = "https://files.pythonhosted.org/packages/dd/ca/16c6926cc1c015845745d5c16c9358e24282f1e588237a4c36d2b30f182f/pillow-12.0.0-cp313-cp313-win32.whl", hash = "sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098", size = 6302391, upload-time = "2025-10-15T18:22:44.753Z" }, - { url = "https://files.pythonhosted.org/packages/6d/2a/dd43dcfd6dae9b6a49ee28a8eedb98c7d5ff2de94a5d834565164667b97b/pillow-12.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905", size = 7007477, upload-time = "2025-10-15T18:22:46.838Z" }, - { url = "https://files.pythonhosted.org/packages/77/f0/72ea067f4b5ae5ead653053212af05ce3705807906ba3f3e8f58ddf617e6/pillow-12.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a", size = 2435918, upload-time = "2025-10-15T18:22:48.399Z" }, - { url = "https://files.pythonhosted.org/packages/f5/5e/9046b423735c21f0487ea6cb5b10f89ea8f8dfbe32576fe052b5ba9d4e5b/pillow-12.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3", size = 5251406, upload-time = "2025-10-15T18:22:49.905Z" }, - { url = "https://files.pythonhosted.org/packages/12/66/982ceebcdb13c97270ef7a56c3969635b4ee7cd45227fa707c94719229c5/pillow-12.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced", size = 4653218, upload-time = "2025-10-15T18:22:51.587Z" }, - { url = "https://files.pythonhosted.org/packages/16/b3/81e625524688c31859450119bf12674619429cab3119eec0e30a7a1029cb/pillow-12.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b", size = 6266564, upload-time = "2025-10-15T18:22:53.215Z" }, - { url = "https://files.pythonhosted.org/packages/98/59/dfb38f2a41240d2408096e1a76c671d0a105a4a8471b1871c6902719450c/pillow-12.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d", size = 8069260, upload-time = "2025-10-15T18:22:54.933Z" }, - { url = "https://files.pythonhosted.org/packages/dc/3d/378dbea5cd1874b94c312425ca77b0f47776c78e0df2df751b820c8c1d6c/pillow-12.0.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a", size = 6379248, upload-time = "2025-10-15T18:22:56.605Z" }, - { url = "https://files.pythonhosted.org/packages/84/b0/d525ef47d71590f1621510327acec75ae58c721dc071b17d8d652ca494d8/pillow-12.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe", size = 7066043, upload-time = "2025-10-15T18:22:58.53Z" }, - { url = "https://files.pythonhosted.org/packages/61/2c/aced60e9cf9d0cde341d54bf7932c9ffc33ddb4a1595798b3a5150c7ec4e/pillow-12.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee", size = 6490915, upload-time = "2025-10-15T18:23:00.582Z" }, - { url = "https://files.pythonhosted.org/packages/ef/26/69dcb9b91f4e59f8f34b2332a4a0a951b44f547c4ed39d3e4dcfcff48f89/pillow-12.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef", size = 7157998, upload-time = "2025-10-15T18:23:02.627Z" }, - { url = "https://files.pythonhosted.org/packages/61/2b/726235842220ca95fa441ddf55dd2382b52ab5b8d9c0596fe6b3f23dafe8/pillow-12.0.0-cp313-cp313t-win32.whl", hash = "sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9", size = 6306201, upload-time = "2025-10-15T18:23:04.709Z" }, - { url = "https://files.pythonhosted.org/packages/c0/3d/2afaf4e840b2df71344ababf2f8edd75a705ce500e5dc1e7227808312ae1/pillow-12.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b", size = 7013165, upload-time = "2025-10-15T18:23:06.46Z" }, - { url = "https://files.pythonhosted.org/packages/6f/75/3fa09aa5cf6ed04bee3fa575798ddf1ce0bace8edb47249c798077a81f7f/pillow-12.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47", size = 2437834, upload-time = "2025-10-15T18:23:08.194Z" }, - { url = "https://files.pythonhosted.org/packages/54/2a/9a8c6ba2c2c07b71bec92cf63e03370ca5e5f5c5b119b742bcc0cde3f9c5/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9", size = 4045531, upload-time = "2025-10-15T18:23:10.121Z" }, - { url = "https://files.pythonhosted.org/packages/84/54/836fdbf1bfb3d66a59f0189ff0b9f5f666cee09c6188309300df04ad71fa/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2", size = 4120554, upload-time = "2025-10-15T18:23:12.14Z" }, - { url = "https://files.pythonhosted.org/packages/0d/cd/16aec9f0da4793e98e6b54778a5fbce4f375c6646fe662e80600b8797379/pillow-12.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a", size = 3576812, upload-time = "2025-10-15T18:23:13.962Z" }, - { url = "https://files.pythonhosted.org/packages/f6/b7/13957fda356dc46339298b351cae0d327704986337c3c69bb54628c88155/pillow-12.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b", size = 5252689, upload-time = "2025-10-15T18:23:15.562Z" }, - { url = "https://files.pythonhosted.org/packages/fc/f5/eae31a306341d8f331f43edb2e9122c7661b975433de5e447939ae61c5da/pillow-12.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad", size = 4650186, upload-time = "2025-10-15T18:23:17.379Z" }, - { url = "https://files.pythonhosted.org/packages/86/62/2a88339aa40c4c77e79108facbd307d6091e2c0eb5b8d3cf4977cfca2fe6/pillow-12.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01", size = 6230308, upload-time = "2025-10-15T18:23:18.971Z" }, - { url = "https://files.pythonhosted.org/packages/c7/33/5425a8992bcb32d1cb9fa3dd39a89e613d09a22f2c8083b7bf43c455f760/pillow-12.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c", size = 8039222, upload-time = "2025-10-15T18:23:20.909Z" }, - { url = "https://files.pythonhosted.org/packages/d8/61/3f5d3b35c5728f37953d3eec5b5f3e77111949523bd2dd7f31a851e50690/pillow-12.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e", size = 6346657, upload-time = "2025-10-15T18:23:23.077Z" }, - { url = "https://files.pythonhosted.org/packages/3a/be/ee90a3d79271227e0f0a33c453531efd6ed14b2e708596ba5dd9be948da3/pillow-12.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e", size = 7038482, upload-time = "2025-10-15T18:23:25.005Z" }, - { url = "https://files.pythonhosted.org/packages/44/34/a16b6a4d1ad727de390e9bd9f19f5f669e079e5826ec0f329010ddea492f/pillow-12.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9", size = 6461416, upload-time = "2025-10-15T18:23:27.009Z" }, - { url = "https://files.pythonhosted.org/packages/b6/39/1aa5850d2ade7d7ba9f54e4e4c17077244ff7a2d9e25998c38a29749eb3f/pillow-12.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab", size = 7131584, upload-time = "2025-10-15T18:23:29.752Z" }, - { url = "https://files.pythonhosted.org/packages/bf/db/4fae862f8fad0167073a7733973bfa955f47e2cac3dc3e3e6257d10fab4a/pillow-12.0.0-cp314-cp314-win32.whl", hash = "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b", size = 6400621, upload-time = "2025-10-15T18:23:32.06Z" }, - { url = "https://files.pythonhosted.org/packages/2b/24/b350c31543fb0107ab2599464d7e28e6f856027aadda995022e695313d94/pillow-12.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b", size = 7142916, upload-time = "2025-10-15T18:23:34.71Z" }, - { url = "https://files.pythonhosted.org/packages/0f/9b/0ba5a6fd9351793996ef7487c4fdbde8d3f5f75dbedc093bb598648fddf0/pillow-12.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0", size = 2523836, upload-time = "2025-10-15T18:23:36.967Z" }, - { url = "https://files.pythonhosted.org/packages/f5/7a/ceee0840aebc579af529b523d530840338ecf63992395842e54edc805987/pillow-12.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6", size = 5255092, upload-time = "2025-10-15T18:23:38.573Z" }, - { url = "https://files.pythonhosted.org/packages/44/76/20776057b4bfd1aef4eeca992ebde0f53a4dce874f3ae693d0ec90a4f79b/pillow-12.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6", size = 4653158, upload-time = "2025-10-15T18:23:40.238Z" }, - { url = "https://files.pythonhosted.org/packages/82/3f/d9ff92ace07be8836b4e7e87e6a4c7a8318d47c2f1463ffcf121fc57d9cb/pillow-12.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1", size = 6267882, upload-time = "2025-10-15T18:23:42.434Z" }, - { url = "https://files.pythonhosted.org/packages/9f/7a/4f7ff87f00d3ad33ba21af78bfcd2f032107710baf8280e3722ceec28cda/pillow-12.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e", size = 8071001, upload-time = "2025-10-15T18:23:44.29Z" }, - { url = "https://files.pythonhosted.org/packages/75/87/fcea108944a52dad8cca0715ae6247e271eb80459364a98518f1e4f480c1/pillow-12.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca", size = 6380146, upload-time = "2025-10-15T18:23:46.065Z" }, - { url = "https://files.pythonhosted.org/packages/91/52/0d31b5e571ef5fd111d2978b84603fce26aba1b6092f28e941cb46570745/pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925", size = 7067344, upload-time = "2025-10-15T18:23:47.898Z" }, - { url = "https://files.pythonhosted.org/packages/7b/f4/2dd3d721f875f928d48e83bb30a434dee75a2531bca839bb996bb0aa5a91/pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8", size = 6491864, upload-time = "2025-10-15T18:23:49.607Z" }, - { url = "https://files.pythonhosted.org/packages/30/4b/667dfcf3d61fc309ba5a15b141845cece5915e39b99c1ceab0f34bf1d124/pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4", size = 7158911, upload-time = "2025-10-15T18:23:51.351Z" }, - { url = "https://files.pythonhosted.org/packages/a2/2f/16cabcc6426c32218ace36bf0d55955e813f2958afddbf1d391849fee9d1/pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52", size = 6408045, upload-time = "2025-10-15T18:23:53.177Z" }, - { url = "https://files.pythonhosted.org/packages/35/73/e29aa0c9c666cf787628d3f0dcf379f4791fba79f4936d02f8b37165bdf8/pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a", size = 7148282, upload-time = "2025-10-15T18:23:55.316Z" }, - { url = "https://files.pythonhosted.org/packages/c1/70/6b41bdcddf541b437bbb9f47f94d2db5d9ddef6c37ccab8c9107743748a4/pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", size = 2525630, upload-time = "2025-10-15T18:23:57.149Z" }, - { url = "https://files.pythonhosted.org/packages/1d/b3/582327e6c9f86d037b63beebe981425d6811104cb443e8193824ef1a2f27/pillow-12.0.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8", size = 5215068, upload-time = "2025-10-15T18:23:59.594Z" }, - { url = "https://files.pythonhosted.org/packages/fd/d6/67748211d119f3b6540baf90f92fae73ae51d5217b171b0e8b5f7e5d558f/pillow-12.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a", size = 4614994, upload-time = "2025-10-15T18:24:01.669Z" }, - { url = "https://files.pythonhosted.org/packages/2d/e1/f8281e5d844c41872b273b9f2c34a4bf64ca08905668c8ae730eedc7c9fa/pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197", size = 5246639, upload-time = "2025-10-15T18:24:03.403Z" }, - { url = "https://files.pythonhosted.org/packages/94/5a/0d8ab8ffe8a102ff5df60d0de5af309015163bf710c7bb3e8311dd3b3ad0/pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c", size = 6986839, upload-time = "2025-10-15T18:24:05.344Z" }, - { url = "https://files.pythonhosted.org/packages/20/2e/3434380e8110b76cd9eb00a363c484b050f949b4bbe84ba770bb8508a02c/pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e", size = 5313505, upload-time = "2025-10-15T18:24:07.137Z" }, - { url = "https://files.pythonhosted.org/packages/57/ca/5a9d38900d9d74785141d6580950fe705de68af735ff6e727cb911b64740/pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76", size = 5963654, upload-time = "2025-10-15T18:24:09.579Z" }, - { url = "https://files.pythonhosted.org/packages/95/7e/f896623c3c635a90537ac093c6a618ebe1a90d87206e42309cb5d98a1b9e/pillow-12.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5", size = 6997850, upload-time = "2025-10-15T18:24:11.495Z" }, +version = "12.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/46/5da1ec4a5171ee7bf1a0efa064aba70ba3d6e0788ce3f5acd1375d23c8c0/pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32", size = 5304084, upload-time = "2026-02-11T04:20:27.501Z" }, + { url = "https://files.pythonhosted.org/packages/78/93/a29e9bc02d1cf557a834da780ceccd54e02421627200696fcf805ebdc3fb/pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38", size = 4657866, upload-time = "2026-02-11T04:20:29.827Z" }, + { url = "https://files.pythonhosted.org/packages/13/84/583a4558d492a179d31e4aae32eadce94b9acf49c0337c4ce0b70e0a01f2/pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5", size = 6232148, upload-time = "2026-02-11T04:20:31.329Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e2/53c43334bbbb2d3b938978532fbda8e62bb6e0b23a26ce8592f36bcc4987/pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090", size = 8038007, upload-time = "2026-02-11T04:20:34.225Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a6/3d0e79c8a9d58150dd98e199d7c1c56861027f3829a3a60b3c2784190180/pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af", size = 6345418, upload-time = "2026-02-11T04:20:35.858Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c8/46dfeac5825e600579157eea177be43e2f7ff4a99da9d0d0a49533509ac5/pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b", size = 7034590, upload-time = "2026-02-11T04:20:37.91Z" }, + { url = "https://files.pythonhosted.org/packages/af/bf/e6f65d3db8a8bbfeaf9e13cc0417813f6319863a73de934f14b2229ada18/pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5", size = 6458655, upload-time = "2026-02-11T04:20:39.496Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c2/66091f3f34a25894ca129362e510b956ef26f8fb67a0e6417bc5744e56f1/pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d", size = 7159286, upload-time = "2026-02-11T04:20:41.139Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5a/24bc8eb526a22f957d0cec6243146744966d40857e3d8deb68f7902ca6c1/pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c", size = 6328663, upload-time = "2026-02-11T04:20:43.184Z" }, + { url = "https://files.pythonhosted.org/packages/31/03/bef822e4f2d8f9d7448c133d0a18185d3cce3e70472774fffefe8b0ed562/pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563", size = 7031448, upload-time = "2026-02-11T04:20:44.696Z" }, + { url = "https://files.pythonhosted.org/packages/49/70/f76296f53610bd17b2e7d31728b8b7825e3ac3b5b3688b51f52eab7c0818/pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80", size = 2453651, upload-time = "2026-02-11T04:20:46.243Z" }, + { url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" }, + { url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" }, + { url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" }, + { url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" }, + { url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" }, + { url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" }, + { url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" }, + { url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" }, + { url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" }, + { url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" }, + { url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561, upload-time = "2026-02-11T04:21:11.742Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460, upload-time = "2026-02-11T04:21:13.786Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698, upload-time = "2026-02-11T04:21:15.949Z" }, + { url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706, upload-time = "2026-02-11T04:21:17.723Z" }, + { url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621, upload-time = "2026-02-11T04:21:19.547Z" }, + { url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069, upload-time = "2026-02-11T04:21:21.378Z" }, + { url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040, upload-time = "2026-02-11T04:21:23.148Z" }, + { url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523, upload-time = "2026-02-11T04:21:25.01Z" }, + { url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" }, + { url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880, upload-time = "2026-02-11T04:21:32.865Z" }, + { url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616, upload-time = "2026-02-11T04:21:34.97Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008, upload-time = "2026-02-11T04:21:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226, upload-time = "2026-02-11T04:21:38.585Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136, upload-time = "2026-02-11T04:21:40.562Z" }, + { url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129, upload-time = "2026-02-11T04:21:42.521Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807, upload-time = "2026-02-11T04:21:44.22Z" }, + { url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954, upload-time = "2026-02-11T04:21:46.114Z" }, + { url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" }, + { url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" }, + { url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" }, + { url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" }, + { url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254, upload-time = "2026-02-11T04:22:07.656Z" }, + { url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499, upload-time = "2026-02-11T04:22:09.613Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137, upload-time = "2026-02-11T04:22:11.434Z" }, + { url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721, upload-time = "2026-02-11T04:22:13.321Z" }, + { url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798, upload-time = "2026-02-11T04:22:15.449Z" }, + { url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315, upload-time = "2026-02-11T04:22:17.24Z" }, + { url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360, upload-time = "2026-02-11T04:22:19.111Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438, upload-time = "2026-02-11T04:22:21.041Z" }, + { url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" }, + { url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" }, + { url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612, upload-time = "2026-02-11T04:22:29.884Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567, upload-time = "2026-02-11T04:22:31.799Z" }, + { url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951, upload-time = "2026-02-11T04:22:33.921Z" }, + { url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769, upload-time = "2026-02-11T04:22:35.877Z" }, + { url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358, upload-time = "2026-02-11T04:22:37.698Z" }, + { url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558, upload-time = "2026-02-11T04:22:39.597Z" }, + { url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028, upload-time = "2026-02-11T04:22:42.73Z" }, + { url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940, upload-time = "2026-02-11T04:22:44.543Z" }, + { url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" }, + { url = "https://files.pythonhosted.org/packages/56/11/5d43209aa4cb58e0cc80127956ff1796a68b928e6324bbf06ef4db34367b/pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f", size = 5228606, upload-time = "2026-02-11T04:22:52.106Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d5/3b005b4e4fda6698b371fa6c21b097d4707585d7db99e98d9b0b87ac612a/pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9", size = 4622321, upload-time = "2026-02-11T04:22:53.827Z" }, + { url = "https://files.pythonhosted.org/packages/df/36/ed3ea2d594356fd8037e5a01f6156c74bc8d92dbb0fa60746cc96cabb6e8/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e", size = 5247579, upload-time = "2026-02-11T04:22:56.094Z" }, + { url = "https://files.pythonhosted.org/packages/54/9a/9cc3e029683cf6d20ae5085da0dafc63148e3252c2f13328e553aaa13cfb/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9", size = 6989094, upload-time = "2026-02-11T04:22:58.288Z" }, + { url = "https://files.pythonhosted.org/packages/00/98/fc53ab36da80b88df0967896b6c4b4cd948a0dc5aa40a754266aa3ae48b3/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3", size = 5313850, upload-time = "2026-02-11T04:23:00.554Z" }, + { url = "https://files.pythonhosted.org/packages/30/02/00fa585abfd9fe9d73e5f6e554dc36cc2b842898cbfc46d70353dae227f8/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735", size = 5963343, upload-time = "2026-02-11T04:23:02.934Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/c56ce33ca856e358d27fda9676c055395abddb82c35ac0f593877ed4562e/pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e", size = 7029880, upload-time = "2026-02-11T04:23:04.783Z" }, ] [[package]] name = "platformdirs" -version = "4.5.1" +version = "4.9.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/04/fea538adf7dbbd6d186f551d595961e564a3b6715bdf276b477460858672/platformdirs-4.9.2.tar.gz", hash = "sha256:9a33809944b9db043ad67ca0db94b14bf452cc6aeaac46a88ea55b26e2e9d291", size = 28394, upload-time = "2026-02-16T03:56:10.574Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, + { url = "https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd", size = 21168, upload-time = "2026-02-16T03:56:08.891Z" }, ] [[package]] @@ -2428,30 +2412,30 @@ wheels = [ [[package]] name = "polars" -version = "1.37.1" +version = "1.38.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "polars-runtime-32" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/84/ae/dfebf31b9988c20998140b54d5b521f64ce08879f2c13d9b4d44d7c87e32/polars-1.37.1.tar.gz", hash = "sha256:0309e2a4633e712513401964b4d95452f124ceabf7aec6db50affb9ced4a274e", size = 715572, upload-time = "2026-01-12T23:27:03.267Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/5e/208a24471a433bcd0e9a6889ac49025fd4daad2815c8220c5bd2576e5f1b/polars-1.38.1.tar.gz", hash = "sha256:803a2be5344ef880ad625addfb8f641995cfd777413b08a10de0897345778239", size = 717667, upload-time = "2026-02-06T18:13:23.013Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/08/75/ec73e38812bca7c2240aff481b9ddff20d1ad2f10dee4b3353f5eeaacdab/polars-1.37.1-py3-none-any.whl", hash = "sha256:377fed8939a2f1223c1563cfabdc7b4a3d6ff846efa1f2ddeb8644fafd9b1aff", size = 805749, upload-time = "2026-01-12T23:25:48.595Z" }, + { url = "https://files.pythonhosted.org/packages/0a/49/737c1a6273c585719858261753da0b688454d1b634438ccba8a9c4eb5aab/polars-1.38.1-py3-none-any.whl", hash = "sha256:a29479c48fed4984d88b656486d221f638cba45d3e961631a50ee5fdde38cb2c", size = 810368, upload-time = "2026-02-06T18:11:55.819Z" }, ] [[package]] name = "polars-runtime-32" -version = "1.37.1" +version = "1.38.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/40/0b/addabe5e8d28a5a4c9887a08907be7ddc3fce892dc38f37d14b055438a57/polars_runtime_32-1.37.1.tar.gz", hash = "sha256:68779d4a691da20a5eb767d74165a8f80a2bdfbde4b54acf59af43f7fa028d8f", size = 2818945, upload-time = "2026-01-12T23:27:04.653Z" } +sdist = { url = "https://files.pythonhosted.org/packages/07/4b/04d6b3fb7cf336fbe12fbc4b43f36d1783e11bb0f2b1e3980ec44878df06/polars_runtime_32-1.38.1.tar.gz", hash = "sha256:04f20ed1f5c58771f34296a27029dc755a9e4b1390caeaef8f317e06fdfce2ec", size = 2812631, upload-time = "2026-02-06T18:13:25.206Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/a2/e828ea9f845796de02d923edb790e408ca0b560cd68dbd74bb99a1b3c461/polars_runtime_32-1.37.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:0b8d4d73ea9977d3731927740e59d814647c5198bdbe359bcf6a8bfce2e79771", size = 43499912, upload-time = "2026-01-12T23:25:51.182Z" }, - { url = "https://files.pythonhosted.org/packages/7e/46/81b71b7aa9e3703ee6e4ef1f69a87e40f58ea7c99212bf49a95071e99c8c/polars_runtime_32-1.37.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:c682bf83f5f352e5e02f5c16c652c48ca40442f07b236f30662b22217320ce76", size = 39695707, upload-time = "2026-01-12T23:25:54.289Z" }, - { url = "https://files.pythonhosted.org/packages/81/2e/20009d1fde7ee919e24040f5c87cb9d0e4f8e3f109b74ba06bc10c02459c/polars_runtime_32-1.37.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc82b5bbe70ca1a4b764eed1419f6336752d6ba9fc1245388d7f8b12438afa2c", size = 41467034, upload-time = "2026-01-12T23:25:56.925Z" }, - { url = "https://files.pythonhosted.org/packages/eb/21/9b55bea940524324625b1e8fd96233290303eb1bf2c23b54573487bbbc25/polars_runtime_32-1.37.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8362d11ac5193b994c7e9048ffe22ccfb976699cfbf6e128ce0302e06728894", size = 45142711, upload-time = "2026-01-12T23:26:00.817Z" }, - { url = "https://files.pythonhosted.org/packages/8c/25/c5f64461aeccdac6834a89f826d051ccd3b4ce204075e562c87a06ed2619/polars_runtime_32-1.37.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:04f5d5a2f013dca7391b7d8e7672fa6d37573a87f1d45d3dd5f0d9b5565a4b0f", size = 41638564, upload-time = "2026-01-12T23:26:04.186Z" }, - { url = "https://files.pythonhosted.org/packages/35/af/509d3cf6c45e764ccf856beaae26fc34352f16f10f94a7839b1042920a73/polars_runtime_32-1.37.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:fbfde7c0ca8209eeaed546e4a32cca1319189aa61c5f0f9a2b4494262bd0c689", size = 44721136, upload-time = "2026-01-12T23:26:07.088Z" }, - { url = "https://files.pythonhosted.org/packages/af/d1/5c0a83a625f72beef59394bebc57d12637997632a4f9d3ab2ffc2cc62bbf/polars_runtime_32-1.37.1-cp310-abi3-win_amd64.whl", hash = "sha256:da3d3642ae944e18dd17109d2a3036cb94ce50e5495c5023c77b1599d4c861bc", size = 44948288, upload-time = "2026-01-12T23:26:10.214Z" }, - { url = "https://files.pythonhosted.org/packages/10/f3/061bb702465904b6502f7c9081daee34b09ccbaa4f8c94cf43a2a3b6dd6f/polars_runtime_32-1.37.1-cp310-abi3-win_arm64.whl", hash = "sha256:55f2c4847a8d2e267612f564de7b753a4bde3902eaabe7b436a0a4abf75949a0", size = 41001914, upload-time = "2026-01-12T23:26:12.997Z" }, + { url = "https://files.pythonhosted.org/packages/ae/a2/a00defbddadd8cf1042f52380dcba6b6592b03bac8e3b34c436b62d12d3b/polars_runtime_32-1.38.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:18154e96044724a0ac38ce155cf63aa03c02dd70500efbbf1a61b08cadd269ef", size = 44108001, upload-time = "2026-02-06T18:11:58.127Z" }, + { url = "https://files.pythonhosted.org/packages/a7/fb/599ff3709e6a303024efd7edfd08cf8de55c6ac39527d8f41cbc4399385f/polars_runtime_32-1.38.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:c49acac34cc4049ed188f1eb67d6ff3971a39b4af7f7b734b367119970f313ac", size = 40230140, upload-time = "2026-02-06T18:12:01.181Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8c/3ac18d6f89dc05fe2c7c0ee1dc5b81f77a5c85ad59898232c2500fe2ebbf/polars_runtime_32-1.38.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fef2ef2626a954e010e006cc8e4de467ecf32d08008f130cea1c78911f545323", size = 41994039, upload-time = "2026-02-06T18:12:04.332Z" }, + { url = "https://files.pythonhosted.org/packages/f2/5a/61d60ec5cc0ab37cbd5a699edb2f9af2875b7fdfdfb2a4608ca3cc5f0448/polars_runtime_32-1.38.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8a5f7a8125e2d50e2e060296551c929aec09be23a9edcb2b12ca923f555a5ba", size = 45755804, upload-time = "2026-02-06T18:12:07.846Z" }, + { url = "https://files.pythonhosted.org/packages/91/54/02cd4074c98c361ccd3fec3bcb0bd68dbc639c0550c42a4436b0ff0f3ccf/polars_runtime_32-1.38.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:10d19cd9863e129273b18b7fcaab625b5c8143c2d22b3e549067b78efa32e4fa", size = 42159605, upload-time = "2026-02-06T18:12:10.919Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f3/b2a5e720cc56eaa38b4518e63aa577b4bbd60e8b05a00fe43ca051be5879/polars_runtime_32-1.38.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:61e8d73c614b46a00d2f853625a7569a2e4a0999333e876354ac81d1bf1bb5e2", size = 45336615, upload-time = "2026-02-06T18:12:14.074Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8d/ee2e4b7de948090cfb3df37d401c521233daf97bfc54ddec5d61d1d31618/polars_runtime_32-1.38.1-cp310-abi3-win_amd64.whl", hash = "sha256:08c2b3b93509c1141ac97891294ff5c5b0c548a373f583eaaea873a4bf506437", size = 45680732, upload-time = "2026-02-06T18:12:19.097Z" }, + { url = "https://files.pythonhosted.org/packages/bf/18/72c216f4ab0c82b907009668f79183ae029116ff0dd245d56ef58aac48e7/polars_runtime_32-1.38.1-cp310-abi3-win_arm64.whl", hash = "sha256:6d07d0cc832bfe4fb54b6e04218c2c27afcfa6b9498f9f6bbf262a00d58cc7c4", size = 41639413, upload-time = "2026-02-06T18:12:22.044Z" }, ] [[package]] @@ -2622,52 +2606,52 @@ wheels = [ [[package]] name = "pyarrow" -version = "22.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/30/53/04a7fdc63e6056116c9ddc8b43bc28c12cdd181b85cbeadb79278475f3ae/pyarrow-22.0.0.tar.gz", hash = "sha256:3d600dc583260d845c7d8a6db540339dd883081925da2bd1c5cb808f720b3cd9", size = 1151151, upload-time = "2025-10-24T12:30:00.762Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/b7/18f611a8cdc43417f9394a3ccd3eace2f32183c08b9eddc3d17681819f37/pyarrow-22.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:3e294c5eadfb93d78b0763e859a0c16d4051fc1c5231ae8956d61cb0b5666f5a", size = 34272022, upload-time = "2025-10-24T10:04:28.973Z" }, - { url = "https://files.pythonhosted.org/packages/26/5c/f259e2526c67eb4b9e511741b19870a02363a47a35edbebc55c3178db22d/pyarrow-22.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:69763ab2445f632d90b504a815a2a033f74332997052b721002298ed6de40f2e", size = 35995834, upload-time = "2025-10-24T10:04:35.467Z" }, - { url = "https://files.pythonhosted.org/packages/50/8d/281f0f9b9376d4b7f146913b26fac0aa2829cd1ee7e997f53a27411bbb92/pyarrow-22.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:b41f37cabfe2463232684de44bad753d6be08a7a072f6a83447eeaf0e4d2a215", size = 45030348, upload-time = "2025-10-24T10:04:43.366Z" }, - { url = "https://files.pythonhosted.org/packages/f5/e5/53c0a1c428f0976bf22f513d79c73000926cb00b9c138d8e02daf2102e18/pyarrow-22.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:35ad0f0378c9359b3f297299c3309778bb03b8612f987399a0333a560b43862d", size = 47699480, upload-time = "2025-10-24T10:04:51.486Z" }, - { url = "https://files.pythonhosted.org/packages/95/e1/9dbe4c465c3365959d183e6345d0a8d1dc5b02ca3f8db4760b3bc834cf25/pyarrow-22.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8382ad21458075c2e66a82a29d650f963ce51c7708c7c0ff313a8c206c4fd5e8", size = 48011148, upload-time = "2025-10-24T10:04:59.585Z" }, - { url = "https://files.pythonhosted.org/packages/c5/b4/7caf5d21930061444c3cf4fa7535c82faf5263e22ce43af7c2759ceb5b8b/pyarrow-22.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1a812a5b727bc09c3d7ea072c4eebf657c2f7066155506ba31ebf4792f88f016", size = 50276964, upload-time = "2025-10-24T10:05:08.175Z" }, - { url = "https://files.pythonhosted.org/packages/ae/f3/cec89bd99fa3abf826f14d4e53d3d11340ce6f6af4d14bdcd54cd83b6576/pyarrow-22.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:ec5d40dd494882704fb876c16fa7261a69791e784ae34e6b5992e977bd2e238c", size = 28106517, upload-time = "2025-10-24T10:05:14.314Z" }, - { url = "https://files.pythonhosted.org/packages/af/63/ba23862d69652f85b615ca14ad14f3bcfc5bf1b99ef3f0cd04ff93fdad5a/pyarrow-22.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:bea79263d55c24a32b0d79c00a1c58bb2ee5f0757ed95656b01c0fb310c5af3d", size = 34211578, upload-time = "2025-10-24T10:05:21.583Z" }, - { url = "https://files.pythonhosted.org/packages/b1/d0/f9ad86fe809efd2bcc8be32032fa72e8b0d112b01ae56a053006376c5930/pyarrow-22.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:12fe549c9b10ac98c91cf791d2945e878875d95508e1a5d14091a7aaa66d9cf8", size = 35989906, upload-time = "2025-10-24T10:05:29.485Z" }, - { url = "https://files.pythonhosted.org/packages/b4/a8/f910afcb14630e64d673f15904ec27dd31f1e009b77033c365c84e8c1e1d/pyarrow-22.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:334f900ff08ce0423407af97e6c26ad5d4e3b0763645559ece6fbf3747d6a8f5", size = 45021677, upload-time = "2025-10-24T10:05:38.274Z" }, - { url = "https://files.pythonhosted.org/packages/13/95/aec81f781c75cd10554dc17a25849c720d54feafb6f7847690478dcf5ef8/pyarrow-22.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c6c791b09c57ed76a18b03f2631753a4960eefbbca80f846da8baefc6491fcfe", size = 47726315, upload-time = "2025-10-24T10:05:47.314Z" }, - { url = "https://files.pythonhosted.org/packages/bb/d4/74ac9f7a54cfde12ee42734ea25d5a3c9a45db78f9def949307a92720d37/pyarrow-22.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c3200cb41cdbc65156e5f8c908d739b0dfed57e890329413da2748d1a2cd1a4e", size = 47990906, upload-time = "2025-10-24T10:05:58.254Z" }, - { url = "https://files.pythonhosted.org/packages/2e/71/fedf2499bf7a95062eafc989ace56572f3343432570e1c54e6599d5b88da/pyarrow-22.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ac93252226cf288753d8b46280f4edf3433bf9508b6977f8dd8526b521a1bbb9", size = 50306783, upload-time = "2025-10-24T10:06:08.08Z" }, - { url = "https://files.pythonhosted.org/packages/68/ed/b202abd5a5b78f519722f3d29063dda03c114711093c1995a33b8e2e0f4b/pyarrow-22.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:44729980b6c50a5f2bfcc2668d36c569ce17f8b17bccaf470c4313dcbbf13c9d", size = 27972883, upload-time = "2025-10-24T10:06:14.204Z" }, - { url = "https://files.pythonhosted.org/packages/a6/d6/d0fac16a2963002fc22c8fa75180a838737203d558f0ed3b564c4a54eef5/pyarrow-22.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e6e95176209257803a8b3d0394f21604e796dadb643d2f7ca21b66c9c0b30c9a", size = 34204629, upload-time = "2025-10-24T10:06:20.274Z" }, - { url = "https://files.pythonhosted.org/packages/c6/9c/1d6357347fbae062ad3f17082f9ebc29cc733321e892c0d2085f42a2212b/pyarrow-22.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:001ea83a58024818826a9e3f89bf9310a114f7e26dfe404a4c32686f97bd7901", size = 35985783, upload-time = "2025-10-24T10:06:27.301Z" }, - { url = "https://files.pythonhosted.org/packages/ff/c0/782344c2ce58afbea010150df07e3a2f5fdad299cd631697ae7bd3bac6e3/pyarrow-22.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:ce20fe000754f477c8a9125543f1936ea5b8867c5406757c224d745ed033e691", size = 45020999, upload-time = "2025-10-24T10:06:35.387Z" }, - { url = "https://files.pythonhosted.org/packages/1b/8b/5362443737a5307a7b67c1017c42cd104213189b4970bf607e05faf9c525/pyarrow-22.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e0a15757fccb38c410947df156f9749ae4a3c89b2393741a50521f39a8cf202a", size = 47724601, upload-time = "2025-10-24T10:06:43.551Z" }, - { url = "https://files.pythonhosted.org/packages/69/4d/76e567a4fc2e190ee6072967cb4672b7d9249ac59ae65af2d7e3047afa3b/pyarrow-22.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cedb9dd9358e4ea1d9bce3665ce0797f6adf97ff142c8e25b46ba9cdd508e9b6", size = 48001050, upload-time = "2025-10-24T10:06:52.284Z" }, - { url = "https://files.pythonhosted.org/packages/01/5e/5653f0535d2a1aef8223cee9d92944cb6bccfee5cf1cd3f462d7cb022790/pyarrow-22.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:252be4a05f9d9185bb8c18e83764ebcfea7185076c07a7a662253af3a8c07941", size = 50307877, upload-time = "2025-10-24T10:07:02.405Z" }, - { url = "https://files.pythonhosted.org/packages/2d/f8/1d0bd75bf9328a3b826e24a16e5517cd7f9fbf8d34a3184a4566ef5a7f29/pyarrow-22.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:a4893d31e5ef780b6edcaf63122df0f8d321088bb0dee4c8c06eccb1ca28d145", size = 27977099, upload-time = "2025-10-24T10:08:07.259Z" }, - { url = "https://files.pythonhosted.org/packages/90/81/db56870c997805bf2b0f6eeeb2d68458bf4654652dccdcf1bf7a42d80903/pyarrow-22.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:f7fe3dbe871294ba70d789be16b6e7e52b418311e166e0e3cba9522f0f437fb1", size = 34336685, upload-time = "2025-10-24T10:07:11.47Z" }, - { url = "https://files.pythonhosted.org/packages/1c/98/0727947f199aba8a120f47dfc229eeb05df15bcd7a6f1b669e9f882afc58/pyarrow-22.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:ba95112d15fd4f1105fb2402c4eab9068f0554435e9b7085924bcfaac2cc306f", size = 36032158, upload-time = "2025-10-24T10:07:18.626Z" }, - { url = "https://files.pythonhosted.org/packages/96/b4/9babdef9c01720a0785945c7cf550e4acd0ebcd7bdd2e6f0aa7981fa85e2/pyarrow-22.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c064e28361c05d72eed8e744c9605cbd6d2bb7481a511c74071fd9b24bc65d7d", size = 44892060, upload-time = "2025-10-24T10:07:26.002Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ca/2f8804edd6279f78a37062d813de3f16f29183874447ef6d1aadbb4efa0f/pyarrow-22.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6f9762274496c244d951c819348afbcf212714902742225f649cf02823a6a10f", size = 47504395, upload-time = "2025-10-24T10:07:34.09Z" }, - { url = "https://files.pythonhosted.org/packages/b9/f0/77aa5198fd3943682b2e4faaf179a674f0edea0d55d326d83cb2277d9363/pyarrow-22.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a9d9ffdc2ab696f6b15b4d1f7cec6658e1d788124418cb30030afbae31c64746", size = 48066216, upload-time = "2025-10-24T10:07:43.528Z" }, - { url = "https://files.pythonhosted.org/packages/79/87/a1937b6e78b2aff18b706d738c9e46ade5bfcf11b294e39c87706a0089ac/pyarrow-22.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ec1a15968a9d80da01e1d30349b2b0d7cc91e96588ee324ce1b5228175043e95", size = 50288552, upload-time = "2025-10-24T10:07:53.519Z" }, - { url = "https://files.pythonhosted.org/packages/60/ae/b5a5811e11f25788ccfdaa8f26b6791c9807119dffcf80514505527c384c/pyarrow-22.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:bba208d9c7decf9961998edf5c65e3ea4355d5818dd6cd0f6809bec1afb951cc", size = 28262504, upload-time = "2025-10-24T10:08:00.932Z" }, - { url = "https://files.pythonhosted.org/packages/bd/b0/0fa4d28a8edb42b0a7144edd20befd04173ac79819547216f8a9f36f9e50/pyarrow-22.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:9bddc2cade6561f6820d4cd73f99a0243532ad506bc510a75a5a65a522b2d74d", size = 34224062, upload-time = "2025-10-24T10:08:14.101Z" }, - { url = "https://files.pythonhosted.org/packages/0f/a8/7a719076b3c1be0acef56a07220c586f25cd24de0e3f3102b438d18ae5df/pyarrow-22.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:e70ff90c64419709d38c8932ea9fe1cc98415c4f87ea8da81719e43f02534bc9", size = 35990057, upload-time = "2025-10-24T10:08:21.842Z" }, - { url = "https://files.pythonhosted.org/packages/89/3c/359ed54c93b47fb6fe30ed16cdf50e3f0e8b9ccfb11b86218c3619ae50a8/pyarrow-22.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:92843c305330aa94a36e706c16209cd4df274693e777ca47112617db7d0ef3d7", size = 45068002, upload-time = "2025-10-24T10:08:29.034Z" }, - { url = "https://files.pythonhosted.org/packages/55/fc/4945896cc8638536ee787a3bd6ce7cec8ec9acf452d78ec39ab328efa0a1/pyarrow-22.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:6dda1ddac033d27421c20d7a7943eec60be44e0db4e079f33cc5af3b8280ccde", size = 47737765, upload-time = "2025-10-24T10:08:38.559Z" }, - { url = "https://files.pythonhosted.org/packages/cd/5e/7cb7edeb2abfaa1f79b5d5eb89432356155c8426f75d3753cbcb9592c0fd/pyarrow-22.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:84378110dd9a6c06323b41b56e129c504d157d1a983ce8f5443761eb5256bafc", size = 48048139, upload-time = "2025-10-24T10:08:46.784Z" }, - { url = "https://files.pythonhosted.org/packages/88/c6/546baa7c48185f5e9d6e59277c4b19f30f48c94d9dd938c2a80d4d6b067c/pyarrow-22.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:854794239111d2b88b40b6ef92aa478024d1e5074f364033e73e21e3f76b25e0", size = 50314244, upload-time = "2025-10-24T10:08:55.771Z" }, - { url = "https://files.pythonhosted.org/packages/3c/79/755ff2d145aafec8d347bf18f95e4e81c00127f06d080135dfc86aea417c/pyarrow-22.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:b883fe6fd85adad7932b3271c38ac289c65b7337c2c132e9569f9d3940620730", size = 28757501, upload-time = "2025-10-24T10:09:59.891Z" }, - { url = "https://files.pythonhosted.org/packages/0e/d2/237d75ac28ced3147912954e3c1a174df43a95f4f88e467809118a8165e0/pyarrow-22.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7a820d8ae11facf32585507c11f04e3f38343c1e784c9b5a8b1da5c930547fe2", size = 34355506, upload-time = "2025-10-24T10:09:02.953Z" }, - { url = "https://files.pythonhosted.org/packages/1e/2c/733dfffe6d3069740f98e57ff81007809067d68626c5faef293434d11bd6/pyarrow-22.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:c6ec3675d98915bf1ec8b3c7986422682f7232ea76cad276f4c8abd5b7319b70", size = 36047312, upload-time = "2025-10-24T10:09:10.334Z" }, - { url = "https://files.pythonhosted.org/packages/7c/2b/29d6e3782dc1f299727462c1543af357a0f2c1d3c160ce199950d9ca51eb/pyarrow-22.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:3e739edd001b04f654b166204fc7a9de896cf6007eaff33409ee9e50ceaff754", size = 45081609, upload-time = "2025-10-24T10:09:18.61Z" }, - { url = "https://files.pythonhosted.org/packages/8d/42/aa9355ecc05997915af1b7b947a7f66c02dcaa927f3203b87871c114ba10/pyarrow-22.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7388ac685cab5b279a41dfe0a6ccd99e4dbf322edfb63e02fc0443bf24134e91", size = 47703663, upload-time = "2025-10-24T10:09:27.369Z" }, - { url = "https://files.pythonhosted.org/packages/ee/62/45abedde480168e83a1de005b7b7043fd553321c1e8c5a9a114425f64842/pyarrow-22.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f633074f36dbc33d5c05b5dc75371e5660f1dbf9c8b1d95669def05e5425989c", size = 48066543, upload-time = "2025-10-24T10:09:34.908Z" }, - { url = "https://files.pythonhosted.org/packages/84/e9/7878940a5b072e4f3bf998770acafeae13b267f9893af5f6d4ab3904b67e/pyarrow-22.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4c19236ae2402a8663a2c8f21f1870a03cc57f0bef7e4b6eb3238cc82944de80", size = 50288838, upload-time = "2025-10-24T10:09:44.394Z" }, - { url = "https://files.pythonhosted.org/packages/7b/03/f335d6c52b4a4761bcc83499789a1e2e16d9d201a58c327a9b5cc9a41bd9/pyarrow-22.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0c34fe18094686194f204a3b1787a27456897d8a2d62caf84b61e8dfbc0252ae", size = 29185594, upload-time = "2025-10-24T10:09:53.111Z" }, +version = "23.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/22/134986a4cc224d593c1afde5494d18ff629393d74cc2eddb176669f234a4/pyarrow-23.0.1.tar.gz", hash = "sha256:b8c5873e33440b2bc2f4a79d2b47017a89c5a24116c055625e6f2ee50523f019", size = 1167336, upload-time = "2026-02-16T10:14:12.39Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/41/8e6b6ef7e225d4ceead8459427a52afdc23379768f54dd3566014d7618c1/pyarrow-23.0.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:6f0147ee9e0386f519c952cc670eb4a8b05caa594eeffe01af0e25f699e4e9bb", size = 34302230, upload-time = "2026-02-16T10:09:03.859Z" }, + { url = "https://files.pythonhosted.org/packages/bf/4a/1472c00392f521fea03ae93408bf445cc7bfa1ab81683faf9bc188e36629/pyarrow-23.0.1-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:0ae6e17c828455b6265d590100c295193f93cc5675eb0af59e49dbd00d2de350", size = 35850050, upload-time = "2026-02-16T10:09:11.877Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b2/bd1f2f05ded56af7f54d702c8364c9c43cd6abb91b0e9933f3d77b4f4132/pyarrow-23.0.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:fed7020203e9ef273360b9e45be52a2a47d3103caf156a30ace5247ffb51bdbd", size = 44491918, upload-time = "2026-02-16T10:09:18.144Z" }, + { url = "https://files.pythonhosted.org/packages/0b/62/96459ef5b67957eac38a90f541d1c28833d1b367f014a482cb63f3b7cd2d/pyarrow-23.0.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:26d50dee49d741ac0e82185033488d28d35be4d763ae6f321f97d1140eb7a0e9", size = 47562811, upload-time = "2026-02-16T10:09:25.792Z" }, + { url = "https://files.pythonhosted.org/packages/7d/94/1170e235add1f5f45a954e26cd0e906e7e74e23392dcb560de471f7366ec/pyarrow-23.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3c30143b17161310f151f4a2bcfe41b5ff744238c1039338779424e38579d701", size = 48183766, upload-time = "2026-02-16T10:09:34.645Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2d/39a42af4570377b99774cdb47f63ee6c7da7616bd55b3d5001aa18edfe4f/pyarrow-23.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db2190fa79c80a23fdd29fef4b8992893f024ae7c17d2f5f4db7171fa30c2c78", size = 50607669, upload-time = "2026-02-16T10:09:44.153Z" }, + { url = "https://files.pythonhosted.org/packages/00/ca/db94101c187f3df742133ac837e93b1f269ebdac49427f8310ee40b6a58f/pyarrow-23.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:f00f993a8179e0e1c9713bcc0baf6d6c01326a406a9c23495ec1ba9c9ebf2919", size = 27527698, upload-time = "2026-02-16T10:09:50.263Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4b/4166bb5abbfe6f750fc60ad337c43ecf61340fa52ab386da6e8dbf9e63c4/pyarrow-23.0.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:f4b0dbfa124c0bb161f8b5ebb40f1a680b70279aa0c9901d44a2b5a20806039f", size = 34214575, upload-time = "2026-02-16T10:09:56.225Z" }, + { url = "https://files.pythonhosted.org/packages/e1/da/3f941e3734ac8088ea588b53e860baeddac8323ea40ce22e3d0baa865cc9/pyarrow-23.0.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:7707d2b6673f7de054e2e83d59f9e805939038eebe1763fe811ee8fa5c0cd1a7", size = 35832540, upload-time = "2026-02-16T10:10:03.428Z" }, + { url = "https://files.pythonhosted.org/packages/88/7c/3d841c366620e906d54430817531b877ba646310296df42ef697308c2705/pyarrow-23.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:86ff03fb9f1a320266e0de855dee4b17da6794c595d207f89bba40d16b5c78b9", size = 44470940, upload-time = "2026-02-16T10:10:10.704Z" }, + { url = "https://files.pythonhosted.org/packages/2c/a5/da83046273d990f256cb79796a190bbf7ec999269705ddc609403f8c6b06/pyarrow-23.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:813d99f31275919c383aab17f0f455a04f5a429c261cc411b1e9a8f5e4aaaa05", size = 47586063, upload-time = "2026-02-16T10:10:17.95Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/b7d2ebcff47a514f47f9da1e74b7949138c58cfeb108cdd4ee62f43f0cf3/pyarrow-23.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bf5842f960cddd2ef757d486041d57c96483efc295a8c4a0e20e704cbbf39c67", size = 48173045, upload-time = "2026-02-16T10:10:25.363Z" }, + { url = "https://files.pythonhosted.org/packages/43/b2/b40961262213beaba6acfc88698eb773dfce32ecdf34d19291db94c2bd73/pyarrow-23.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564baf97c858ecc03ec01a41062e8f4698abc3e6e2acd79c01c2e97880a19730", size = 50621741, upload-time = "2026-02-16T10:10:33.477Z" }, + { url = "https://files.pythonhosted.org/packages/f6/70/1fdda42d65b28b078e93d75d371b2185a61da89dda4def8ba6ba41ebdeb4/pyarrow-23.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:07deae7783782ac7250989a7b2ecde9b3c343a643f82e8a4df03d93b633006f0", size = 27620678, upload-time = "2026-02-16T10:10:39.31Z" }, + { url = "https://files.pythonhosted.org/packages/47/10/2cbe4c6f0fb83d2de37249567373d64327a5e4d8db72f486db42875b08f6/pyarrow-23.0.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6b8fda694640b00e8af3c824f99f789e836720aa8c9379fb435d4c4953a756b8", size = 34210066, upload-time = "2026-02-16T10:10:45.487Z" }, + { url = "https://files.pythonhosted.org/packages/cb/4f/679fa7e84dadbaca7a65f7cdba8d6c83febbd93ca12fa4adf40ba3b6362b/pyarrow-23.0.1-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:8ff51b1addc469b9444b7c6f3548e19dc931b172ab234e995a60aea9f6e6025f", size = 35825526, upload-time = "2026-02-16T10:10:52.266Z" }, + { url = "https://files.pythonhosted.org/packages/f9/63/d2747d930882c9d661e9398eefc54f15696547b8983aaaf11d4a2e8b5426/pyarrow-23.0.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:71c5be5cbf1e1cb6169d2a0980850bccb558ddc9b747b6206435313c47c37677", size = 44473279, upload-time = "2026-02-16T10:11:01.557Z" }, + { url = "https://files.pythonhosted.org/packages/b3/93/10a48b5e238de6d562a411af6467e71e7aedbc9b87f8d3a35f1560ae30fb/pyarrow-23.0.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:9b6f4f17b43bc39d56fec96e53fe89d94bac3eb134137964371b45352d40d0c2", size = 47585798, upload-time = "2026-02-16T10:11:09.401Z" }, + { url = "https://files.pythonhosted.org/packages/5c/20/476943001c54ef078dbf9542280e22741219a184a0632862bca4feccd666/pyarrow-23.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fc13fc6c403d1337acab46a2c4346ca6c9dec5780c3c697cf8abfd5e19b6b37", size = 48179446, upload-time = "2026-02-16T10:11:17.781Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b6/5dd0c47b335fcd8edba9bfab78ad961bd0fd55ebe53468cc393f45e0be60/pyarrow-23.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5c16ed4f53247fa3ffb12a14d236de4213a4415d127fe9cebed33d51671113e2", size = 50623972, upload-time = "2026-02-16T10:11:26.185Z" }, + { url = "https://files.pythonhosted.org/packages/d5/09/a532297c9591a727d67760e2e756b83905dd89adb365a7f6e9c72578bcc1/pyarrow-23.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:cecfb12ef629cf6be0b1887f9f86463b0dd3dc3195ae6224e74006be4736035a", size = 27540749, upload-time = "2026-02-16T10:12:23.297Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8e/38749c4b1303e6ae76b3c80618f84861ae0c55dd3c2273842ea6f8258233/pyarrow-23.0.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:29f7f7419a0e30264ea261fdc0e5fe63ce5a6095003db2945d7cd78df391a7e1", size = 34471544, upload-time = "2026-02-16T10:11:32.535Z" }, + { url = "https://files.pythonhosted.org/packages/a3/73/f237b2bc8c669212f842bcfd842b04fc8d936bfc9d471630569132dc920d/pyarrow-23.0.1-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:33d648dc25b51fd8055c19e4261e813dfc4d2427f068bcecc8b53d01b81b0500", size = 35949911, upload-time = "2026-02-16T10:11:39.813Z" }, + { url = "https://files.pythonhosted.org/packages/0c/86/b912195eee0903b5611bf596833def7d146ab2d301afeb4b722c57ffc966/pyarrow-23.0.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd395abf8f91c673dd3589cadc8cc1ee4e8674fa61b2e923c8dd215d9c7d1f41", size = 44520337, upload-time = "2026-02-16T10:11:47.764Z" }, + { url = "https://files.pythonhosted.org/packages/69/c2/f2a717fb824f62d0be952ea724b4f6f9372a17eed6f704b5c9526f12f2f1/pyarrow-23.0.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:00be9576d970c31defb5c32eb72ef585bf600ef6d0a82d5eccaae96639cf9d07", size = 47548944, upload-time = "2026-02-16T10:11:56.607Z" }, + { url = "https://files.pythonhosted.org/packages/84/a7/90007d476b9f0dc308e3bc57b832d004f848fd6c0da601375d20d92d1519/pyarrow-23.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c2139549494445609f35a5cda4eb94e2c9e4d704ce60a095b342f82460c73a83", size = 48236269, upload-time = "2026-02-16T10:12:04.47Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3f/b16fab3e77709856eb6ac328ce35f57a6d4a18462c7ca5186ef31b45e0e0/pyarrow-23.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7044b442f184d84e2351e5084600f0d7343d6117aabcbc1ac78eb1ae11eb4125", size = 50604794, upload-time = "2026-02-16T10:12:11.797Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a1/22df0620a9fac31d68397a75465c344e83c3dfe521f7612aea33e27ab6c0/pyarrow-23.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a35581e856a2fafa12f3f54fce4331862b1cfb0bef5758347a858a4aa9d6bae8", size = 27660642, upload-time = "2026-02-16T10:12:17.746Z" }, + { url = "https://files.pythonhosted.org/packages/8d/1b/6da9a89583ce7b23ac611f183ae4843cd3a6cf54f079549b0e8c14031e73/pyarrow-23.0.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:5df1161da23636a70838099d4aaa65142777185cc0cdba4037a18cee7d8db9ca", size = 34238755, upload-time = "2026-02-16T10:12:32.819Z" }, + { url = "https://files.pythonhosted.org/packages/ae/b5/d58a241fbe324dbaeb8df07be6af8752c846192d78d2272e551098f74e88/pyarrow-23.0.1-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:fa8e51cb04b9f8c9c5ace6bab63af9a1f88d35c0d6cbf53e8c17c098552285e1", size = 35847826, upload-time = "2026-02-16T10:12:38.949Z" }, + { url = "https://files.pythonhosted.org/packages/54/a5/8cbc83f04aba433ca7b331b38f39e000efd9f0c7ce47128670e737542996/pyarrow-23.0.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:0b95a3994f015be13c63148fef8832e8a23938128c185ee951c98908a696e0eb", size = 44536859, upload-time = "2026-02-16T10:12:45.467Z" }, + { url = "https://files.pythonhosted.org/packages/36/2e/c0f017c405fcdc252dbccafbe05e36b0d0eb1ea9a958f081e01c6972927f/pyarrow-23.0.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:4982d71350b1a6e5cfe1af742c53dfb759b11ce14141870d05d9e540d13bc5d1", size = 47614443, upload-time = "2026-02-16T10:12:55.525Z" }, + { url = "https://files.pythonhosted.org/packages/af/6b/2314a78057912f5627afa13ba43809d9d653e6630859618b0fd81a4e0759/pyarrow-23.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c250248f1fe266db627921c89b47b7c06fee0489ad95b04d50353537d74d6886", size = 48232991, upload-time = "2026-02-16T10:13:04.729Z" }, + { url = "https://files.pythonhosted.org/packages/40/f2/1bcb1d3be3460832ef3370d621142216e15a2c7c62602a4ea19ec240dd64/pyarrow-23.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f4763b83c11c16e5f4c15601ba6dfa849e20723b46aa2617cb4bffe8768479f", size = 50645077, upload-time = "2026-02-16T10:13:14.147Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3f/b1da7b61cd66566a4d4c8383d376c606d1c34a906c3f1cb35c479f59d1aa/pyarrow-23.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:3a4c85ef66c134161987c17b147d6bffdca4566f9a4c1d81a0a01cdf08414ea5", size = 28234271, upload-time = "2026-02-16T10:14:09.397Z" }, + { url = "https://files.pythonhosted.org/packages/b5/78/07f67434e910a0f7323269be7bfbf58699bd0c1d080b18a1ab49ba943fe8/pyarrow-23.0.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:17cd28e906c18af486a499422740298c52d7c6795344ea5002a7720b4eadf16d", size = 34488692, upload-time = "2026-02-16T10:13:21.541Z" }, + { url = "https://files.pythonhosted.org/packages/50/76/34cf7ae93ece1f740a04910d9f7e80ba166b9b4ab9596a953e9e62b90fe1/pyarrow-23.0.1-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:76e823d0e86b4fb5e1cf4a58d293036e678b5a4b03539be933d3b31f9406859f", size = 35964383, upload-time = "2026-02-16T10:13:28.63Z" }, + { url = "https://files.pythonhosted.org/packages/46/90/459b827238936d4244214be7c684e1b366a63f8c78c380807ae25ed92199/pyarrow-23.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a62e1899e3078bf65943078b3ad2a6ddcacf2373bc06379aac61b1e548a75814", size = 44538119, upload-time = "2026-02-16T10:13:35.506Z" }, + { url = "https://files.pythonhosted.org/packages/28/a1/93a71ae5881e99d1f9de1d4554a87be37da11cd6b152239fb5bd924fdc64/pyarrow-23.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:df088e8f640c9fae3b1f495b3c64755c4e719091caf250f3a74d095ddf3c836d", size = 47571199, upload-time = "2026-02-16T10:13:42.504Z" }, + { url = "https://files.pythonhosted.org/packages/88/a3/d2c462d4ef313521eaf2eff04d204ac60775263f1fb08c374b543f79f610/pyarrow-23.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:46718a220d64677c93bc243af1d44b55998255427588e400677d7192671845c7", size = 48259435, upload-time = "2026-02-16T10:13:49.226Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f1/11a544b8c3d38a759eb3fbb022039117fd633e9a7b19e4841cc3da091915/pyarrow-23.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a09f3876e87f48bc2f13583ab551f0379e5dfb83210391e68ace404181a20690", size = 50629149, upload-time = "2026-02-16T10:13:57.238Z" }, + { url = "https://files.pythonhosted.org/packages/50/f2/c0e76a0b451ffdf0cf788932e182758eb7558953f4f27f1aff8e2518b653/pyarrow-23.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:527e8d899f14bd15b740cd5a54ad56b7f98044955373a17179d5956ddb93d9ce", size = 28365807, upload-time = "2026-02-16T10:14:03.892Z" }, ] [[package]] @@ -2690,24 +2674,24 @@ wheels = [ [[package]] name = "pymdown-extensions" -version = "10.20.1" +version = "10.21" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1e/6c/9e370934bfa30e889d12e61d0dae009991294f40055c238980066a7fbd83/pymdown_extensions-10.20.1.tar.gz", hash = "sha256:e7e39c865727338d434b55f1dd8da51febcffcaebd6e1a0b9c836243f660740a", size = 852860, upload-time = "2026-01-24T05:56:56.758Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/63/06673d1eb6d8f83c0ea1f677d770e12565fb516928b4109c9e2055656a9e/pymdown_extensions-10.21.tar.gz", hash = "sha256:39f4a020f40773f6b2ff31d2cd2546c2c04d0a6498c31d9c688d2be07e1767d5", size = 853363, upload-time = "2026-02-15T20:44:06.748Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/40/6d/b6ee155462a0156b94312bdd82d2b92ea56e909740045a87ccb98bf52405/pymdown_extensions-10.20.1-py3-none-any.whl", hash = "sha256:24af7feacbca56504b313b7b418c4f5e1317bb5fea60f03d57be7fcc40912aa0", size = 268768, upload-time = "2026-01-24T05:56:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/6f/2c/5b079febdc65e1c3fb2729bf958d18b45be7113828528e8a0b5850dd819a/pymdown_extensions-10.21-py3-none-any.whl", hash = "sha256:91b879f9f864d49794c2d9534372b10150e6141096c3908a455e45ca72ad9d3f", size = 268877, upload-time = "2026-02-15T20:44:05.464Z" }, ] [[package]] name = "pyparsing" -version = "3.3.1" +version = "3.3.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/33/c1/1d9de9aeaa1b89b0186e5fe23294ff6517fce1bc69149185577cd31016b2/pyparsing-3.3.1.tar.gz", hash = "sha256:47fad0f17ac1e2cad3de3b458570fbc9b03560aa029ed5e16ee5554da9a2251c", size = 1550512, upload-time = "2025-12-23T03:14:04.391Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/40/2614036cdd416452f5bf98ec037f38a1afb17f327cb8e6b652d4729e0af8/pyparsing-3.3.1-py3-none-any.whl", hash = "sha256:023b5e7e5520ad96642e2c6db4cb683d3970bd640cdf7115049a6e9c3682df82", size = 121793, upload-time = "2025-12-23T03:14:02.103Z" }, + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, ] [[package]] @@ -2761,15 +2745,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl", hash = "sha256:af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2", size = 15548, upload-time = "2025-10-06T04:15:17.553Z" }, ] -[[package]] -name = "pytz" -version = "2025.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, -] - [[package]] name = "pywinpty" version = "3.0.3" @@ -3087,99 +3062,98 @@ wheels = [ [[package]] name = "ruff" -version = "0.14.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/57/08/52232a877978dd8f9cf2aeddce3e611b40a63287dfca29b6b8da791f5e8d/ruff-0.14.10.tar.gz", hash = "sha256:9a2e830f075d1a42cd28420d7809ace390832a490ed0966fe373ba288e77aaf4", size = 5859763, upload-time = "2025-12-18T19:28:57.98Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/60/01/933704d69f3f05ee16ef11406b78881733c186fe14b6a46b05cfcaf6d3b2/ruff-0.14.10-py3-none-linux_armv6l.whl", hash = "sha256:7a3ce585f2ade3e1f29ec1b92df13e3da262178df8c8bdf876f48fa0e8316c49", size = 13527080, upload-time = "2025-12-18T19:29:25.642Z" }, - { url = "https://files.pythonhosted.org/packages/df/58/a0349197a7dfa603ffb7f5b0470391efa79ddc327c1e29c4851e85b09cc5/ruff-0.14.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:674f9be9372907f7257c51f1d4fc902cb7cf014b9980152b802794317941f08f", size = 13797320, upload-time = "2025-12-18T19:29:02.571Z" }, - { url = "https://files.pythonhosted.org/packages/7b/82/36be59f00a6082e38c23536df4e71cdbc6af8d7c707eade97fcad5c98235/ruff-0.14.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d85713d522348837ef9df8efca33ccb8bd6fcfc86a2cde3ccb4bc9d28a18003d", size = 12918434, upload-time = "2025-12-18T19:28:51.202Z" }, - { url = "https://files.pythonhosted.org/packages/a6/00/45c62a7f7e34da92a25804f813ebe05c88aa9e0c25e5cb5a7d23dd7450e3/ruff-0.14.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6987ebe0501ae4f4308d7d24e2d0fe3d7a98430f5adfd0f1fead050a740a3a77", size = 13371961, upload-time = "2025-12-18T19:29:04.991Z" }, - { url = "https://files.pythonhosted.org/packages/40/31/a5906d60f0405f7e57045a70f2d57084a93ca7425f22e1d66904769d1628/ruff-0.14.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16a01dfb7b9e4eee556fbfd5392806b1b8550c9b4a9f6acd3dbe6812b193c70a", size = 13275629, upload-time = "2025-12-18T19:29:21.381Z" }, - { url = "https://files.pythonhosted.org/packages/3e/60/61c0087df21894cf9d928dc04bcd4fb10e8b2e8dca7b1a276ba2155b2002/ruff-0.14.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7165d31a925b7a294465fa81be8c12a0e9b60fb02bf177e79067c867e71f8b1f", size = 14029234, upload-time = "2025-12-18T19:29:00.132Z" }, - { url = "https://files.pythonhosted.org/packages/44/84/77d911bee3b92348b6e5dab5a0c898d87084ea03ac5dc708f46d88407def/ruff-0.14.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c561695675b972effb0c0a45db233f2c816ff3da8dcfbe7dfc7eed625f218935", size = 15449890, upload-time = "2025-12-18T19:28:53.573Z" }, - { url = "https://files.pythonhosted.org/packages/e9/36/480206eaefa24a7ec321582dda580443a8f0671fdbf6b1c80e9c3e93a16a/ruff-0.14.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bb98fcbbc61725968893682fd4df8966a34611239c9fd07a1f6a07e7103d08e", size = 15123172, upload-time = "2025-12-18T19:29:23.453Z" }, - { url = "https://files.pythonhosted.org/packages/5c/38/68e414156015ba80cef5473d57919d27dfb62ec804b96180bafdeaf0e090/ruff-0.14.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f24b47993a9d8cb858429e97bdf8544c78029f09b520af615c1d261bf827001d", size = 14460260, upload-time = "2025-12-18T19:29:27.808Z" }, - { url = "https://files.pythonhosted.org/packages/b3/19/9e050c0dca8aba824d67cc0db69fb459c28d8cd3f6855b1405b3f29cc91d/ruff-0.14.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59aabd2e2c4fd614d2862e7939c34a532c04f1084476d6833dddef4afab87e9f", size = 14229978, upload-time = "2025-12-18T19:29:11.32Z" }, - { url = "https://files.pythonhosted.org/packages/51/eb/e8dd1dd6e05b9e695aa9dd420f4577debdd0f87a5ff2fedda33c09e9be8c/ruff-0.14.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:213db2b2e44be8625002dbea33bb9c60c66ea2c07c084a00d55732689d697a7f", size = 14338036, upload-time = "2025-12-18T19:29:09.184Z" }, - { url = "https://files.pythonhosted.org/packages/6a/12/f3e3a505db7c19303b70af370d137795fcfec136d670d5de5391e295c134/ruff-0.14.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b914c40ab64865a17a9a5b67911d14df72346a634527240039eb3bd650e5979d", size = 13264051, upload-time = "2025-12-18T19:29:13.431Z" }, - { url = "https://files.pythonhosted.org/packages/08/64/8c3a47eaccfef8ac20e0484e68e0772013eb85802f8a9f7603ca751eb166/ruff-0.14.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1484983559f026788e3a5c07c81ef7d1e97c1c78ed03041a18f75df104c45405", size = 13283998, upload-time = "2025-12-18T19:29:06.994Z" }, - { url = "https://files.pythonhosted.org/packages/12/84/534a5506f4074e5cc0529e5cd96cfc01bb480e460c7edf5af70d2bcae55e/ruff-0.14.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c70427132db492d25f982fffc8d6c7535cc2fd2c83fc8888f05caaa248521e60", size = 13601891, upload-time = "2025-12-18T19:28:55.811Z" }, - { url = "https://files.pythonhosted.org/packages/0d/1e/14c916087d8598917dbad9b2921d340f7884824ad6e9c55de948a93b106d/ruff-0.14.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5bcf45b681e9f1ee6445d317ce1fa9d6cba9a6049542d1c3d5b5958986be8830", size = 14336660, upload-time = "2025-12-18T19:29:16.531Z" }, - { url = "https://files.pythonhosted.org/packages/f2/1c/d7b67ab43f30013b47c12b42d1acd354c195351a3f7a1d67f59e54227ede/ruff-0.14.10-py3-none-win32.whl", hash = "sha256:104c49fc7ab73f3f3a758039adea978869a918f31b73280db175b43a2d9b51d6", size = 13196187, upload-time = "2025-12-18T19:29:19.006Z" }, - { url = "https://files.pythonhosted.org/packages/fb/9c/896c862e13886fae2af961bef3e6312db9ebc6adc2b156fe95e615dee8c1/ruff-0.14.10-py3-none-win_amd64.whl", hash = "sha256:466297bd73638c6bdf06485683e812db1c00c7ac96d4ddd0294a338c62fdc154", size = 14661283, upload-time = "2025-12-18T19:29:30.16Z" }, - { url = "https://files.pythonhosted.org/packages/74/31/b0e29d572670dca3674eeee78e418f20bdf97fa8aa9ea71380885e175ca0/ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6", size = 13729839, upload-time = "2025-12-18T19:28:48.636Z" }, +version = "0.15.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/dc/4e6ac71b511b141cf626357a3946679abeba4cf67bc7cc5a17920f31e10d/ruff-0.15.1.tar.gz", hash = "sha256:c590fe13fb57c97141ae975c03a1aedb3d3156030cabd740d6ff0b0d601e203f", size = 4540855, upload-time = "2026-02-12T23:09:09.998Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/bf/e6e4324238c17f9d9120a9d60aa99a7daaa21204c07fcd84e2ef03bb5fd1/ruff-0.15.1-py3-none-linux_armv6l.whl", hash = "sha256:b101ed7cf4615bda6ffe65bdb59f964e9f4a0d3f85cbf0e54f0ab76d7b90228a", size = 10367819, upload-time = "2026-02-12T23:09:03.598Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ea/c8f89d32e7912269d38c58f3649e453ac32c528f93bb7f4219258be2e7ed/ruff-0.15.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:939c995e9277e63ea632cc8d3fae17aa758526f49a9a850d2e7e758bfef46602", size = 10798618, upload-time = "2026-02-12T23:09:22.928Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0f/1d0d88bc862624247d82c20c10d4c0f6bb2f346559d8af281674cf327f15/ruff-0.15.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d83466455fdefe60b8d9c8df81d3c1bbb2115cede53549d3b522ce2bc703899", size = 10148518, upload-time = "2026-02-12T23:08:58.339Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c8/291c49cefaa4a9248e986256df2ade7add79388fe179e0691be06fae6f37/ruff-0.15.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9457e3c3291024866222b96108ab2d8265b477e5b1534c7ddb1810904858d16", size = 10518811, upload-time = "2026-02-12T23:09:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/c3/1a/f5707440e5ae43ffa5365cac8bbb91e9665f4a883f560893829cf16a606b/ruff-0.15.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92c92b003e9d4f7fbd33b1867bb15a1b785b1735069108dfc23821ba045b29bc", size = 10196169, upload-time = "2026-02-12T23:09:17.306Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ff/26ddc8c4da04c8fd3ee65a89c9fb99eaa5c30394269d424461467be2271f/ruff-0.15.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fe5c41ab43e3a06778844c586251eb5a510f67125427625f9eb2b9526535779", size = 10990491, upload-time = "2026-02-12T23:09:25.503Z" }, + { url = "https://files.pythonhosted.org/packages/fc/00/50920cb385b89413f7cdb4bb9bc8fc59c1b0f30028d8bccc294189a54955/ruff-0.15.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66a6dd6df4d80dc382c6484f8ce1bcceb55c32e9f27a8b94c32f6c7331bf14fb", size = 11843280, upload-time = "2026-02-12T23:09:19.88Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6d/2f5cad8380caf5632a15460c323ae326f1e1a2b5b90a6ee7519017a017ca/ruff-0.15.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a4a42cbb8af0bda9bcd7606b064d7c0bc311a88d141d02f78920be6acb5aa83", size = 11274336, upload-time = "2026-02-12T23:09:14.907Z" }, + { url = "https://files.pythonhosted.org/packages/a3/1d/5f56cae1d6c40b8a318513599b35ea4b075d7dc1cd1d04449578c29d1d75/ruff-0.15.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ab064052c31dddada35079901592dfba2e05f5b1e43af3954aafcbc1096a5b2", size = 11137288, upload-time = "2026-02-12T23:09:07.475Z" }, + { url = "https://files.pythonhosted.org/packages/cd/20/6f8d7d8f768c93b0382b33b9306b3b999918816da46537d5a61635514635/ruff-0.15.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5631c940fe9fe91f817a4c2ea4e81f47bee3ca4aa646134a24374f3c19ad9454", size = 11070681, upload-time = "2026-02-12T23:08:55.43Z" }, + { url = "https://files.pythonhosted.org/packages/9a/67/d640ac76069f64cdea59dba02af2e00b1fa30e2103c7f8d049c0cff4cafd/ruff-0.15.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68138a4ba184b4691ccdc39f7795c66b3c68160c586519e7e8444cf5a53e1b4c", size = 10486401, upload-time = "2026-02-12T23:09:27.927Z" }, + { url = "https://files.pythonhosted.org/packages/65/3d/e1429f64a3ff89297497916b88c32a5cc88eeca7e9c787072d0e7f1d3e1e/ruff-0.15.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:518f9af03bfc33c03bdb4cb63fabc935341bb7f54af500f92ac309ecfbba6330", size = 10197452, upload-time = "2026-02-12T23:09:12.147Z" }, + { url = "https://files.pythonhosted.org/packages/78/83/e2c3bade17dad63bf1e1c2ffaf11490603b760be149e1419b07049b36ef2/ruff-0.15.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:da79f4d6a826caaea95de0237a67e33b81e6ec2e25fc7e1993a4015dffca7c61", size = 10693900, upload-time = "2026-02-12T23:09:34.418Z" }, + { url = "https://files.pythonhosted.org/packages/a1/27/fdc0e11a813e6338e0706e8b39bb7a1d61ea5b36873b351acee7e524a72a/ruff-0.15.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3dd86dccb83cd7d4dcfac303ffc277e6048600dfc22e38158afa208e8bf94a1f", size = 11227302, upload-time = "2026-02-12T23:09:36.536Z" }, + { url = "https://files.pythonhosted.org/packages/f6/58/ac864a75067dcbd3b95be5ab4eb2b601d7fbc3d3d736a27e391a4f92a5c1/ruff-0.15.1-py3-none-win32.whl", hash = "sha256:660975d9cb49b5d5278b12b03bb9951d554543a90b74ed5d366b20e2c57c2098", size = 10462555, upload-time = "2026-02-12T23:09:29.899Z" }, + { url = "https://files.pythonhosted.org/packages/e0/5e/d4ccc8a27ecdb78116feac4935dfc39d1304536f4296168f91ed3ec00cd2/ruff-0.15.1-py3-none-win_amd64.whl", hash = "sha256:c820fef9dd5d4172a6570e5721704a96c6679b80cf7be41659ed439653f62336", size = 11599956, upload-time = "2026-02-12T23:09:01.157Z" }, + { url = "https://files.pythonhosted.org/packages/2a/07/5bda6a85b220c64c65686bc85bd0bbb23b29c62b3a9f9433fa55f17cda93/ruff-0.15.1-py3-none-win_arm64.whl", hash = "sha256:5ff7d5f0f88567850f45081fac8f4ec212be8d0b963e385c3f7d0d2eb4899416", size = 10874604, upload-time = "2026-02-12T23:09:05.515Z" }, ] [[package]] name = "scipy" -version = "1.16.3" +version = "1.17.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0a/ca/d8ace4f98322d01abcd52d381134344bf7b431eba7ed8b42bdea5a3c2ac9/scipy-1.16.3.tar.gz", hash = "sha256:01e87659402762f43bd2fee13370553a17ada367d42e7487800bf2916535aecb", size = 30597883, upload-time = "2025-10-28T17:38:54.068Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/5f/6f37d7439de1455ce9c5a556b8d1db0979f03a796c030bafdf08d35b7bf9/scipy-1.16.3-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:40be6cf99e68b6c4321e9f8782e7d5ff8265af28ef2cd56e9c9b2638fa08ad97", size = 36630881, upload-time = "2025-10-28T17:31:47.104Z" }, - { url = "https://files.pythonhosted.org/packages/7c/89/d70e9f628749b7e4db2aa4cd89735502ff3f08f7b9b27d2e799485987cd9/scipy-1.16.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:8be1ca9170fcb6223cc7c27f4305d680ded114a1567c0bd2bfcbf947d1b17511", size = 28941012, upload-time = "2025-10-28T17:31:53.411Z" }, - { url = "https://files.pythonhosted.org/packages/a8/a8/0e7a9a6872a923505dbdf6bb93451edcac120363131c19013044a1e7cb0c/scipy-1.16.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:bea0a62734d20d67608660f69dcda23e7f90fb4ca20974ab80b6ed40df87a005", size = 20931935, upload-time = "2025-10-28T17:31:57.361Z" }, - { url = "https://files.pythonhosted.org/packages/bd/c7/020fb72bd79ad798e4dbe53938543ecb96b3a9ac3fe274b7189e23e27353/scipy-1.16.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:2a207a6ce9c24f1951241f4693ede2d393f59c07abc159b2cb2be980820e01fb", size = 23534466, upload-time = "2025-10-28T17:32:01.875Z" }, - { url = "https://files.pythonhosted.org/packages/be/a0/668c4609ce6dbf2f948e167836ccaf897f95fb63fa231c87da7558a374cd/scipy-1.16.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:532fb5ad6a87e9e9cd9c959b106b73145a03f04c7d57ea3e6f6bb60b86ab0876", size = 33593618, upload-time = "2025-10-28T17:32:06.902Z" }, - { url = "https://files.pythonhosted.org/packages/ca/6e/8942461cf2636cdae083e3eb72622a7fbbfa5cf559c7d13ab250a5dbdc01/scipy-1.16.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0151a0749efeaaab78711c78422d413c583b8cdd2011a3c1d6c794938ee9fdb2", size = 35899798, upload-time = "2025-10-28T17:32:12.665Z" }, - { url = "https://files.pythonhosted.org/packages/79/e8/d0f33590364cdbd67f28ce79368b373889faa4ee959588beddf6daef9abe/scipy-1.16.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b7180967113560cca57418a7bc719e30366b47959dd845a93206fbed693c867e", size = 36226154, upload-time = "2025-10-28T17:32:17.961Z" }, - { url = "https://files.pythonhosted.org/packages/39/c1/1903de608c0c924a1749c590064e65810f8046e437aba6be365abc4f7557/scipy-1.16.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:deb3841c925eeddb6afc1e4e4a45e418d19ec7b87c5df177695224078e8ec733", size = 38878540, upload-time = "2025-10-28T17:32:23.907Z" }, - { url = "https://files.pythonhosted.org/packages/f1/d0/22ec7036ba0b0a35bccb7f25ab407382ed34af0b111475eb301c16f8a2e5/scipy-1.16.3-cp311-cp311-win_amd64.whl", hash = "sha256:53c3844d527213631e886621df5695d35e4f6a75f620dca412bcd292f6b87d78", size = 38722107, upload-time = "2025-10-28T17:32:29.921Z" }, - { url = "https://files.pythonhosted.org/packages/7b/60/8a00e5a524bb3bf8898db1650d350f50e6cffb9d7a491c561dc9826c7515/scipy-1.16.3-cp311-cp311-win_arm64.whl", hash = "sha256:9452781bd879b14b6f055b26643703551320aa8d79ae064a71df55c00286a184", size = 25506272, upload-time = "2025-10-28T17:32:34.577Z" }, - { url = "https://files.pythonhosted.org/packages/40/41/5bf55c3f386b1643812f3a5674edf74b26184378ef0f3e7c7a09a7e2ca7f/scipy-1.16.3-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:81fc5827606858cf71446a5e98715ba0e11f0dbc83d71c7409d05486592a45d6", size = 36659043, upload-time = "2025-10-28T17:32:40.285Z" }, - { url = "https://files.pythonhosted.org/packages/1e/0f/65582071948cfc45d43e9870bf7ca5f0e0684e165d7c9ef4e50d783073eb/scipy-1.16.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:c97176013d404c7346bf57874eaac5187d969293bf40497140b0a2b2b7482e07", size = 28898986, upload-time = "2025-10-28T17:32:45.325Z" }, - { url = "https://files.pythonhosted.org/packages/96/5e/36bf3f0ac298187d1ceadde9051177d6a4fe4d507e8f59067dc9dd39e650/scipy-1.16.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2b71d93c8a9936046866acebc915e2af2e292b883ed6e2cbe5c34beb094b82d9", size = 20889814, upload-time = "2025-10-28T17:32:49.277Z" }, - { url = "https://files.pythonhosted.org/packages/80/35/178d9d0c35394d5d5211bbff7ac4f2986c5488b59506fef9e1de13ea28d3/scipy-1.16.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3d4a07a8e785d80289dfe66b7c27d8634a773020742ec7187b85ccc4b0e7b686", size = 23565795, upload-time = "2025-10-28T17:32:53.337Z" }, - { url = "https://files.pythonhosted.org/packages/fa/46/d1146ff536d034d02f83c8afc3c4bab2eddb634624d6529a8512f3afc9da/scipy-1.16.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0553371015692a898e1aa858fed67a3576c34edefa6b7ebdb4e9dde49ce5c203", size = 33349476, upload-time = "2025-10-28T17:32:58.353Z" }, - { url = "https://files.pythonhosted.org/packages/79/2e/415119c9ab3e62249e18c2b082c07aff907a273741b3f8160414b0e9193c/scipy-1.16.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:72d1717fd3b5e6ec747327ce9bda32d5463f472c9dce9f54499e81fbd50245a1", size = 35676692, upload-time = "2025-10-28T17:33:03.88Z" }, - { url = "https://files.pythonhosted.org/packages/27/82/df26e44da78bf8d2aeaf7566082260cfa15955a5a6e96e6a29935b64132f/scipy-1.16.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1fb2472e72e24d1530debe6ae078db70fb1605350c88a3d14bc401d6306dbffe", size = 36019345, upload-time = "2025-10-28T17:33:09.773Z" }, - { url = "https://files.pythonhosted.org/packages/82/31/006cbb4b648ba379a95c87262c2855cd0d09453e500937f78b30f02fa1cd/scipy-1.16.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c5192722cffe15f9329a3948c4b1db789fbb1f05c97899187dcf009b283aea70", size = 38678975, upload-time = "2025-10-28T17:33:15.809Z" }, - { url = "https://files.pythonhosted.org/packages/c2/7f/acbd28c97e990b421af7d6d6cd416358c9c293fc958b8529e0bd5d2a2a19/scipy-1.16.3-cp312-cp312-win_amd64.whl", hash = "sha256:56edc65510d1331dae01ef9b658d428e33ed48b4f77b1d51caf479a0253f96dc", size = 38555926, upload-time = "2025-10-28T17:33:21.388Z" }, - { url = "https://files.pythonhosted.org/packages/ce/69/c5c7807fd007dad4f48e0a5f2153038dc96e8725d3345b9ee31b2b7bed46/scipy-1.16.3-cp312-cp312-win_arm64.whl", hash = "sha256:a8a26c78ef223d3e30920ef759e25625a0ecdd0d60e5a8818b7513c3e5384cf2", size = 25463014, upload-time = "2025-10-28T17:33:25.975Z" }, - { url = "https://files.pythonhosted.org/packages/72/f1/57e8327ab1508272029e27eeef34f2302ffc156b69e7e233e906c2a5c379/scipy-1.16.3-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:d2ec56337675e61b312179a1ad124f5f570c00f920cc75e1000025451b88241c", size = 36617856, upload-time = "2025-10-28T17:33:31.375Z" }, - { url = "https://files.pythonhosted.org/packages/44/13/7e63cfba8a7452eb756306aa2fd9b37a29a323b672b964b4fdeded9a3f21/scipy-1.16.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:16b8bc35a4cc24db80a0ec836a9286d0e31b2503cb2fd7ff7fb0e0374a97081d", size = 28874306, upload-time = "2025-10-28T17:33:36.516Z" }, - { url = "https://files.pythonhosted.org/packages/15/65/3a9400efd0228a176e6ec3454b1fa998fbbb5a8defa1672c3f65706987db/scipy-1.16.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:5803c5fadd29de0cf27fa08ccbfe7a9e5d741bf63e4ab1085437266f12460ff9", size = 20865371, upload-time = "2025-10-28T17:33:42.094Z" }, - { url = "https://files.pythonhosted.org/packages/33/d7/eda09adf009a9fb81827194d4dd02d2e4bc752cef16737cc4ef065234031/scipy-1.16.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:b81c27fc41954319a943d43b20e07c40bdcd3ff7cf013f4fb86286faefe546c4", size = 23524877, upload-time = "2025-10-28T17:33:48.483Z" }, - { url = "https://files.pythonhosted.org/packages/7d/6b/3f911e1ebc364cb81320223a3422aab7d26c9c7973109a9cd0f27c64c6c0/scipy-1.16.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0c3b4dd3d9b08dbce0f3440032c52e9e2ab9f96ade2d3943313dfe51a7056959", size = 33342103, upload-time = "2025-10-28T17:33:56.495Z" }, - { url = "https://files.pythonhosted.org/packages/21/f6/4bfb5695d8941e5c570a04d9fcd0d36bce7511b7d78e6e75c8f9791f82d0/scipy-1.16.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7dc1360c06535ea6116a2220f760ae572db9f661aba2d88074fe30ec2aa1ff88", size = 35697297, upload-time = "2025-10-28T17:34:04.722Z" }, - { url = "https://files.pythonhosted.org/packages/04/e1/6496dadbc80d8d896ff72511ecfe2316b50313bfc3ebf07a3f580f08bd8c/scipy-1.16.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:663b8d66a8748051c3ee9c96465fb417509315b99c71550fda2591d7dd634234", size = 36021756, upload-time = "2025-10-28T17:34:13.482Z" }, - { url = "https://files.pythonhosted.org/packages/fe/bd/a8c7799e0136b987bda3e1b23d155bcb31aec68a4a472554df5f0937eef7/scipy-1.16.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eab43fae33a0c39006a88096cd7b4f4ef545ea0447d250d5ac18202d40b6611d", size = 38696566, upload-time = "2025-10-28T17:34:22.384Z" }, - { url = "https://files.pythonhosted.org/packages/cd/01/1204382461fcbfeb05b6161b594f4007e78b6eba9b375382f79153172b4d/scipy-1.16.3-cp313-cp313-win_amd64.whl", hash = "sha256:062246acacbe9f8210de8e751b16fc37458213f124bef161a5a02c7a39284304", size = 38529877, upload-time = "2025-10-28T17:35:51.076Z" }, - { url = "https://files.pythonhosted.org/packages/7f/14/9d9fbcaa1260a94f4bb5b64ba9213ceb5d03cd88841fe9fd1ffd47a45b73/scipy-1.16.3-cp313-cp313-win_arm64.whl", hash = "sha256:50a3dbf286dbc7d84f176f9a1574c705f277cb6565069f88f60db9eafdbe3ee2", size = 25455366, upload-time = "2025-10-28T17:35:59.014Z" }, - { url = "https://files.pythonhosted.org/packages/e2/a3/9ec205bd49f42d45d77f1730dbad9ccf146244c1647605cf834b3a8c4f36/scipy-1.16.3-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:fb4b29f4cf8cc5a8d628bc8d8e26d12d7278cd1f219f22698a378c3d67db5e4b", size = 37027931, upload-time = "2025-10-28T17:34:31.451Z" }, - { url = "https://files.pythonhosted.org/packages/25/06/ca9fd1f3a4589cbd825b1447e5db3a8ebb969c1eaf22c8579bd286f51b6d/scipy-1.16.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:8d09d72dc92742988b0e7750bddb8060b0c7079606c0d24a8cc8e9c9c11f9079", size = 29400081, upload-time = "2025-10-28T17:34:39.087Z" }, - { url = "https://files.pythonhosted.org/packages/6a/56/933e68210d92657d93fb0e381683bc0e53a965048d7358ff5fbf9e6a1b17/scipy-1.16.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:03192a35e661470197556de24e7cb1330d84b35b94ead65c46ad6f16f6b28f2a", size = 21391244, upload-time = "2025-10-28T17:34:45.234Z" }, - { url = "https://files.pythonhosted.org/packages/a8/7e/779845db03dc1418e215726329674b40576879b91814568757ff0014ad65/scipy-1.16.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:57d01cb6f85e34f0946b33caa66e892aae072b64b034183f3d87c4025802a119", size = 23929753, upload-time = "2025-10-28T17:34:51.793Z" }, - { url = "https://files.pythonhosted.org/packages/4c/4b/f756cf8161d5365dcdef9e5f460ab226c068211030a175d2fc7f3f41ca64/scipy-1.16.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:96491a6a54e995f00a28a3c3badfff58fd093bf26cd5fb34a2188c8c756a3a2c", size = 33496912, upload-time = "2025-10-28T17:34:59.8Z" }, - { url = "https://files.pythonhosted.org/packages/09/b5/222b1e49a58668f23839ca1542a6322bb095ab8d6590d4f71723869a6c2c/scipy-1.16.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cd13e354df9938598af2be05822c323e97132d5e6306b83a3b4ee6724c6e522e", size = 35802371, upload-time = "2025-10-28T17:35:08.173Z" }, - { url = "https://files.pythonhosted.org/packages/c1/8d/5964ef68bb31829bde27611f8c9deeac13764589fe74a75390242b64ca44/scipy-1.16.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:63d3cdacb8a824a295191a723ee5e4ea7768ca5ca5f2838532d9f2e2b3ce2135", size = 36190477, upload-time = "2025-10-28T17:35:16.7Z" }, - { url = "https://files.pythonhosted.org/packages/ab/f2/b31d75cb9b5fa4dd39a0a931ee9b33e7f6f36f23be5ef560bf72e0f92f32/scipy-1.16.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e7efa2681ea410b10dde31a52b18b0154d66f2485328830e45fdf183af5aefc6", size = 38796678, upload-time = "2025-10-28T17:35:26.354Z" }, - { url = "https://files.pythonhosted.org/packages/b4/1e/b3723d8ff64ab548c38d87055483714fefe6ee20e0189b62352b5e015bb1/scipy-1.16.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2d1ae2cf0c350e7705168ff2429962a89ad90c2d49d1dd300686d8b2a5af22fc", size = 38640178, upload-time = "2025-10-28T17:35:35.304Z" }, - { url = "https://files.pythonhosted.org/packages/8e/f3/d854ff38789aca9b0cc23008d607ced9de4f7ab14fa1ca4329f86b3758ca/scipy-1.16.3-cp313-cp313t-win_arm64.whl", hash = "sha256:0c623a54f7b79dd88ef56da19bc2873afec9673a48f3b85b18e4d402bdd29a5a", size = 25803246, upload-time = "2025-10-28T17:35:42.155Z" }, - { url = "https://files.pythonhosted.org/packages/99/f6/99b10fd70f2d864c1e29a28bbcaa0c6340f9d8518396542d9ea3b4aaae15/scipy-1.16.3-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:875555ce62743e1d54f06cdf22c1e0bc47b91130ac40fe5d783b6dfa114beeb6", size = 36606469, upload-time = "2025-10-28T17:36:08.741Z" }, - { url = "https://files.pythonhosted.org/packages/4d/74/043b54f2319f48ea940dd025779fa28ee360e6b95acb7cd188fad4391c6b/scipy-1.16.3-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:bb61878c18a470021fb515a843dc7a76961a8daceaaaa8bad1332f1bf4b54657", size = 28872043, upload-time = "2025-10-28T17:36:16.599Z" }, - { url = "https://files.pythonhosted.org/packages/4d/e1/24b7e50cc1c4ee6ffbcb1f27fe9f4c8b40e7911675f6d2d20955f41c6348/scipy-1.16.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f2622206f5559784fa5c4b53a950c3c7c1cf3e84ca1b9c4b6c03f062f289ca26", size = 20862952, upload-time = "2025-10-28T17:36:22.966Z" }, - { url = "https://files.pythonhosted.org/packages/dd/3a/3e8c01a4d742b730df368e063787c6808597ccb38636ed821d10b39ca51b/scipy-1.16.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7f68154688c515cdb541a31ef8eb66d8cd1050605be9dcd74199cbd22ac739bc", size = 23508512, upload-time = "2025-10-28T17:36:29.731Z" }, - { url = "https://files.pythonhosted.org/packages/1f/60/c45a12b98ad591536bfe5330cb3cfe1850d7570259303563b1721564d458/scipy-1.16.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8b3c820ddb80029fe9f43d61b81d8b488d3ef8ca010d15122b152db77dc94c22", size = 33413639, upload-time = "2025-10-28T17:36:37.982Z" }, - { url = "https://files.pythonhosted.org/packages/71/bc/35957d88645476307e4839712642896689df442f3e53b0fa016ecf8a3357/scipy-1.16.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d3837938ae715fc0fe3c39c0202de3a8853aff22ca66781ddc2ade7554b7e2cc", size = 35704729, upload-time = "2025-10-28T17:36:46.547Z" }, - { url = "https://files.pythonhosted.org/packages/3b/15/89105e659041b1ca11c386e9995aefacd513a78493656e57789f9d9eab61/scipy-1.16.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aadd23f98f9cb069b3bd64ddc900c4d277778242e961751f77a8cb5c4b946fb0", size = 36086251, upload-time = "2025-10-28T17:36:55.161Z" }, - { url = "https://files.pythonhosted.org/packages/1a/87/c0ea673ac9c6cc50b3da2196d860273bc7389aa69b64efa8493bdd25b093/scipy-1.16.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b7c5f1bda1354d6a19bc6af73a649f8285ca63ac6b52e64e658a5a11d4d69800", size = 38716681, upload-time = "2025-10-28T17:37:04.1Z" }, - { url = "https://files.pythonhosted.org/packages/91/06/837893227b043fb9b0d13e4bd7586982d8136cb249ffb3492930dab905b8/scipy-1.16.3-cp314-cp314-win_amd64.whl", hash = "sha256:e5d42a9472e7579e473879a1990327830493a7047506d58d73fc429b84c1d49d", size = 39358423, upload-time = "2025-10-28T17:38:20.005Z" }, - { url = "https://files.pythonhosted.org/packages/95/03/28bce0355e4d34a7c034727505a02d19548549e190bedd13a721e35380b7/scipy-1.16.3-cp314-cp314-win_arm64.whl", hash = "sha256:6020470b9d00245926f2d5bb93b119ca0340f0d564eb6fbaad843eaebf9d690f", size = 26135027, upload-time = "2025-10-28T17:38:24.966Z" }, - { url = "https://files.pythonhosted.org/packages/b2/6f/69f1e2b682efe9de8fe9f91040f0cd32f13cfccba690512ba4c582b0bc29/scipy-1.16.3-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:e1d27cbcb4602680a49d787d90664fa4974063ac9d4134813332a8c53dbe667c", size = 37028379, upload-time = "2025-10-28T17:37:14.061Z" }, - { url = "https://files.pythonhosted.org/packages/7c/2d/e826f31624a5ebbab1cd93d30fd74349914753076ed0593e1d56a98c4fb4/scipy-1.16.3-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:9b9c9c07b6d56a35777a1b4cc8966118fb16cfd8daf6743867d17d36cfad2d40", size = 29400052, upload-time = "2025-10-28T17:37:21.709Z" }, - { url = "https://files.pythonhosted.org/packages/69/27/d24feb80155f41fd1f156bf144e7e049b4e2b9dd06261a242905e3bc7a03/scipy-1.16.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:3a4c460301fb2cffb7f88528f30b3127742cff583603aa7dc964a52c463b385d", size = 21391183, upload-time = "2025-10-28T17:37:29.559Z" }, - { url = "https://files.pythonhosted.org/packages/f8/d3/1b229e433074c5738a24277eca520a2319aac7465eea7310ea6ae0e98ae2/scipy-1.16.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:f667a4542cc8917af1db06366d3f78a5c8e83badd56409f94d1eac8d8d9133fa", size = 23930174, upload-time = "2025-10-28T17:37:36.306Z" }, - { url = "https://files.pythonhosted.org/packages/16/9d/d9e148b0ec680c0f042581a2be79a28a7ab66c0c4946697f9e7553ead337/scipy-1.16.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f379b54b77a597aa7ee5e697df0d66903e41b9c85a6dd7946159e356319158e8", size = 33497852, upload-time = "2025-10-28T17:37:42.228Z" }, - { url = "https://files.pythonhosted.org/packages/2f/22/4e5f7561e4f98b7bea63cf3fd7934bff1e3182e9f1626b089a679914d5c8/scipy-1.16.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4aff59800a3b7f786b70bfd6ab551001cb553244988d7d6b8299cb1ea653b353", size = 35798595, upload-time = "2025-10-28T17:37:48.102Z" }, - { url = "https://files.pythonhosted.org/packages/83/42/6644d714c179429fc7196857866f219fef25238319b650bb32dde7bf7a48/scipy-1.16.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:da7763f55885045036fabcebd80144b757d3db06ab0861415d1c3b7c69042146", size = 36186269, upload-time = "2025-10-28T17:37:53.72Z" }, - { url = "https://files.pythonhosted.org/packages/ac/70/64b4d7ca92f9cf2e6fc6aaa2eecf80bb9b6b985043a9583f32f8177ea122/scipy-1.16.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ffa6eea95283b2b8079b821dc11f50a17d0571c92b43e2b5b12764dc5f9b285d", size = 38802779, upload-time = "2025-10-28T17:37:59.393Z" }, - { url = "https://files.pythonhosted.org/packages/61/82/8d0e39f62764cce5ffd5284131e109f07cf8955aef9ab8ed4e3aa5e30539/scipy-1.16.3-cp314-cp314t-win_amd64.whl", hash = "sha256:d9f48cafc7ce94cf9b15c6bffdc443a81a27bf7075cf2dcd5c8b40f85d10c4e7", size = 39471128, upload-time = "2025-10-28T17:38:05.259Z" }, - { url = "https://files.pythonhosted.org/packages/64/47/a494741db7280eae6dc033510c319e34d42dd41b7ac0c7ead39354d1a2b5/scipy-1.16.3-cp314-cp314t-win_arm64.whl", hash = "sha256:21d9d6b197227a12dcbf9633320a4e34c6b0e51c57268df255a0942983bac562", size = 26464127, upload-time = "2025-10-28T17:38:11.34Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/56/3e/9cca699f3486ce6bc12ff46dc2031f1ec8eb9ccc9a320fdaf925f1417426/scipy-1.17.0.tar.gz", hash = "sha256:2591060c8e648d8b96439e111ac41fd8342fdeff1876be2e19dea3fe8930454e", size = 30396830, upload-time = "2026-01-10T21:34:23.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/4b/c89c131aa87cad2b77a54eb0fb94d633a842420fa7e919dc2f922037c3d8/scipy-1.17.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:2abd71643797bd8a106dff97894ff7869eeeb0af0f7a5ce02e4227c6a2e9d6fd", size = 31381316, upload-time = "2026-01-10T21:24:33.42Z" }, + { url = "https://files.pythonhosted.org/packages/5e/5f/a6b38f79a07d74989224d5f11b55267714707582908a5f1ae854cf9a9b84/scipy-1.17.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:ef28d815f4d2686503e5f4f00edc387ae58dfd7a2f42e348bb53359538f01558", size = 27966760, upload-time = "2026-01-10T21:24:38.911Z" }, + { url = "https://files.pythonhosted.org/packages/c1/20/095ad24e031ee8ed3c5975954d816b8e7e2abd731e04f8be573de8740885/scipy-1.17.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:272a9f16d6bb4667e8b50d25d71eddcc2158a214df1b566319298de0939d2ab7", size = 20138701, upload-time = "2026-01-10T21:24:43.249Z" }, + { url = "https://files.pythonhosted.org/packages/89/11/4aad2b3858d0337756f3323f8960755704e530b27eb2a94386c970c32cbe/scipy-1.17.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:7204fddcbec2fe6598f1c5fdf027e9f259106d05202a959a9f1aecf036adc9f6", size = 22480574, upload-time = "2026-01-10T21:24:47.266Z" }, + { url = "https://files.pythonhosted.org/packages/85/bd/f5af70c28c6da2227e510875cadf64879855193a687fb19951f0f44cfd6b/scipy-1.17.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc02c37a5639ee67d8fb646ffded6d793c06c5622d36b35cfa8fe5ececb8f042", size = 32862414, upload-time = "2026-01-10T21:24:52.566Z" }, + { url = "https://files.pythonhosted.org/packages/ef/df/df1457c4df3826e908879fe3d76bc5b6e60aae45f4ee42539512438cfd5d/scipy-1.17.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dac97a27520d66c12a34fd90a4fe65f43766c18c0d6e1c0a80f114d2260080e4", size = 35112380, upload-time = "2026-01-10T21:24:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/5f/bb/88e2c16bd1dd4de19d80d7c5e238387182993c2fb13b4b8111e3927ad422/scipy-1.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ebb7446a39b3ae0fe8f416a9a3fdc6fba3f11c634f680f16a239c5187bc487c0", size = 34922676, upload-time = "2026-01-10T21:25:04.287Z" }, + { url = "https://files.pythonhosted.org/packages/02/ba/5120242cc735f71fc002cff0303d536af4405eb265f7c60742851e7ccfe9/scipy-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:474da16199f6af66601a01546144922ce402cb17362e07d82f5a6cf8f963e449", size = 37507599, upload-time = "2026-01-10T21:25:09.851Z" }, + { url = "https://files.pythonhosted.org/packages/52/c8/08629657ac6c0da198487ce8cd3de78e02cfde42b7f34117d56a3fe249dc/scipy-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:255c0da161bd7b32a6c898e7891509e8a9289f0b1c6c7d96142ee0d2b114c2ea", size = 36380284, upload-time = "2026-01-10T21:25:15.632Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4a/465f96d42c6f33ad324a40049dfd63269891db9324aa66c4a1c108c6f994/scipy-1.17.0-cp311-cp311-win_arm64.whl", hash = "sha256:85b0ac3ad17fa3be50abd7e69d583d98792d7edc08367e01445a1e2076005379", size = 24370427, upload-time = "2026-01-10T21:25:20.514Z" }, + { url = "https://files.pythonhosted.org/packages/0b/11/7241a63e73ba5a516f1930ac8d5b44cbbfabd35ac73a2d08ca206df007c4/scipy-1.17.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:0d5018a57c24cb1dd828bcf51d7b10e65986d549f52ef5adb6b4d1ded3e32a57", size = 31364580, upload-time = "2026-01-10T21:25:25.717Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1d/5057f812d4f6adc91a20a2d6f2ebcdb517fdbc87ae3acc5633c9b97c8ba5/scipy-1.17.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:88c22af9e5d5a4f9e027e26772cc7b5922fab8bcc839edb3ae33de404feebd9e", size = 27969012, upload-time = "2026-01-10T21:25:30.921Z" }, + { url = "https://files.pythonhosted.org/packages/e3/21/f6ec556c1e3b6ec4e088da667d9987bb77cc3ab3026511f427dc8451187d/scipy-1.17.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:f3cd947f20fe17013d401b64e857c6b2da83cae567adbb75b9dcba865abc66d8", size = 20140691, upload-time = "2026-01-10T21:25:34.802Z" }, + { url = "https://files.pythonhosted.org/packages/7a/fe/5e5ad04784964ba964a96f16c8d4676aa1b51357199014dce58ab7ec5670/scipy-1.17.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e8c0b331c2c1f531eb51f1b4fc9ba709521a712cce58f1aa627bc007421a5306", size = 22463015, upload-time = "2026-01-10T21:25:39.277Z" }, + { url = "https://files.pythonhosted.org/packages/4a/69/7c347e857224fcaf32a34a05183b9d8a7aca25f8f2d10b8a698b8388561a/scipy-1.17.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5194c445d0a1c7a6c1a4a4681b6b7c71baad98ff66d96b949097e7513c9d6742", size = 32724197, upload-time = "2026-01-10T21:25:44.084Z" }, + { url = "https://files.pythonhosted.org/packages/d1/fe/66d73b76d378ba8cc2fe605920c0c75092e3a65ae746e1e767d9d020a75a/scipy-1.17.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9eeb9b5f5997f75507814ed9d298ab23f62cf79f5a3ef90031b1ee2506abdb5b", size = 35009148, upload-time = "2026-01-10T21:25:50.591Z" }, + { url = "https://files.pythonhosted.org/packages/af/07/07dec27d9dc41c18d8c43c69e9e413431d20c53a0339c388bcf72f353c4b/scipy-1.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:40052543f7bbe921df4408f46003d6f01c6af109b9e2c8a66dd1cf6cf57f7d5d", size = 34798766, upload-time = "2026-01-10T21:25:59.41Z" }, + { url = "https://files.pythonhosted.org/packages/81/61/0470810c8a093cdacd4ba7504b8a218fd49ca070d79eca23a615f5d9a0b0/scipy-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0cf46c8013fec9d3694dc572f0b54100c28405d55d3e2cb15e2895b25057996e", size = 37405953, upload-time = "2026-01-10T21:26:07.75Z" }, + { url = "https://files.pythonhosted.org/packages/92/ce/672ed546f96d5d41ae78c4b9b02006cedd0b3d6f2bf5bb76ea455c320c28/scipy-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:0937a0b0d8d593a198cededd4c439a0ea216a3f36653901ea1f3e4be949056f8", size = 36328121, upload-time = "2026-01-10T21:26:16.509Z" }, + { url = "https://files.pythonhosted.org/packages/9d/21/38165845392cae67b61843a52c6455d47d0cc2a40dd495c89f4362944654/scipy-1.17.0-cp312-cp312-win_arm64.whl", hash = "sha256:f603d8a5518c7426414d1d8f82e253e454471de682ce5e39c29adb0df1efb86b", size = 24314368, upload-time = "2026-01-10T21:26:23.087Z" }, + { url = "https://files.pythonhosted.org/packages/0c/51/3468fdfd49387ddefee1636f5cf6d03ce603b75205bf439bbf0e62069bfd/scipy-1.17.0-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:65ec32f3d32dfc48c72df4291345dae4f048749bc8d5203ee0a3f347f96c5ce6", size = 31344101, upload-time = "2026-01-10T21:26:30.25Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9a/9406aec58268d437636069419e6977af953d1e246df941d42d3720b7277b/scipy-1.17.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:1f9586a58039d7229ce77b52f8472c972448cded5736eaf102d5658bbac4c269", size = 27950385, upload-time = "2026-01-10T21:26:36.801Z" }, + { url = "https://files.pythonhosted.org/packages/4f/98/e7342709e17afdfd1b26b56ae499ef4939b45a23a00e471dfb5375eea205/scipy-1.17.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:9fad7d3578c877d606b1150135c2639e9de9cecd3705caa37b66862977cc3e72", size = 20122115, upload-time = "2026-01-10T21:26:42.107Z" }, + { url = "https://files.pythonhosted.org/packages/fd/0e/9eeeb5357a64fd157cbe0302c213517c541cc16b8486d82de251f3c68ede/scipy-1.17.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:423ca1f6584fc03936972b5f7c06961670dbba9f234e71676a7c7ccf938a0d61", size = 22442402, upload-time = "2026-01-10T21:26:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c9/10/be13397a0e434f98e0c79552b2b584ae5bb1c8b2be95db421533bbca5369/scipy-1.17.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe508b5690e9eaaa9467fc047f833af58f1152ae51a0d0aed67aa5801f4dd7d6", size = 32696338, upload-time = "2026-01-10T21:26:55.521Z" }, + { url = "https://files.pythonhosted.org/packages/63/1e/12fbf2a3bb240161651c94bb5cdd0eae5d4e8cc6eaeceb74ab07b12a753d/scipy-1.17.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6680f2dfd4f6182e7d6db161344537da644d1cf85cf293f015c60a17ecf08752", size = 34977201, upload-time = "2026-01-10T21:27:03.501Z" }, + { url = "https://files.pythonhosted.org/packages/19/5b/1a63923e23ccd20bd32156d7dd708af5bbde410daa993aa2500c847ab2d2/scipy-1.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eec3842ec9ac9de5917899b277428886042a93db0b227ebbe3a333b64ec7643d", size = 34777384, upload-time = "2026-01-10T21:27:11.423Z" }, + { url = "https://files.pythonhosted.org/packages/39/22/b5da95d74edcf81e540e467202a988c50fef41bd2011f46e05f72ba07df6/scipy-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d7425fcafbc09a03731e1bc05581f5fad988e48c6a861f441b7ab729a49a55ea", size = 37379586, upload-time = "2026-01-10T21:27:20.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b6/8ac583d6da79e7b9e520579f03007cb006f063642afd6b2eeb16b890bf93/scipy-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:87b411e42b425b84777718cc41516b8a7e0795abfa8e8e1d573bf0ef014f0812", size = 36287211, upload-time = "2026-01-10T21:28:43.122Z" }, + { url = "https://files.pythonhosted.org/packages/55/fb/7db19e0b3e52f882b420417644ec81dd57eeef1bd1705b6f689d8ff93541/scipy-1.17.0-cp313-cp313-win_arm64.whl", hash = "sha256:357ca001c6e37601066092e7c89cca2f1ce74e2a520ca78d063a6d2201101df2", size = 24312646, upload-time = "2026-01-10T21:28:49.893Z" }, + { url = "https://files.pythonhosted.org/packages/20/b6/7feaa252c21cc7aff335c6c55e1b90ab3e3306da3f048109b8b639b94648/scipy-1.17.0-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:ec0827aa4d36cb79ff1b81de898e948a51ac0b9b1c43e4a372c0508c38c0f9a3", size = 31693194, upload-time = "2026-01-10T21:27:27.454Z" }, + { url = "https://files.pythonhosted.org/packages/76/bb/bbb392005abce039fb7e672cb78ac7d158700e826b0515cab6b5b60c26fb/scipy-1.17.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:819fc26862b4b3c73a60d486dbb919202f3d6d98c87cf20c223511429f2d1a97", size = 28365415, upload-time = "2026-01-10T21:27:34.26Z" }, + { url = "https://files.pythonhosted.org/packages/37/da/9d33196ecc99fba16a409c691ed464a3a283ac454a34a13a3a57c0d66f3a/scipy-1.17.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:363ad4ae2853d88ebcde3ae6ec46ccca903ea9835ee8ba543f12f575e7b07e4e", size = 20537232, upload-time = "2026-01-10T21:27:40.306Z" }, + { url = "https://files.pythonhosted.org/packages/56/9d/f4b184f6ddb28e9a5caea36a6f98e8ecd2a524f9127354087ce780885d83/scipy-1.17.0-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:979c3a0ff8e5ba254d45d59ebd38cde48fce4f10b5125c680c7a4bfe177aab07", size = 22791051, upload-time = "2026-01-10T21:27:46.539Z" }, + { url = "https://files.pythonhosted.org/packages/9b/9d/025cccdd738a72140efc582b1641d0dd4caf2e86c3fb127568dc80444e6e/scipy-1.17.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:130d12926ae34399d157de777472bf82e9061c60cc081372b3118edacafe1d00", size = 32815098, upload-time = "2026-01-10T21:27:54.389Z" }, + { url = "https://files.pythonhosted.org/packages/48/5f/09b879619f8bca15ce392bfc1894bd9c54377e01d1b3f2f3b595a1b4d945/scipy-1.17.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e886000eb4919eae3a44f035e63f0fd8b651234117e8f6f29bad1cd26e7bc45", size = 35031342, upload-time = "2026-01-10T21:28:03.012Z" }, + { url = "https://files.pythonhosted.org/packages/f2/9a/f0f0a9f0aa079d2f106555b984ff0fbb11a837df280f04f71f056ea9c6e4/scipy-1.17.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:13c4096ac6bc31d706018f06a49abe0485f96499deb82066b94d19b02f664209", size = 34893199, upload-time = "2026-01-10T21:28:10.832Z" }, + { url = "https://files.pythonhosted.org/packages/90/b8/4f0f5cf0c5ea4d7548424e6533e6b17d164f34a6e2fb2e43ffebb6697b06/scipy-1.17.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cacbaddd91fcffde703934897c5cd2c7cb0371fac195d383f4e1f1c5d3f3bd04", size = 37438061, upload-time = "2026-01-10T21:28:19.684Z" }, + { url = "https://files.pythonhosted.org/packages/f9/cc/2bd59140ed3b2fa2882fb15da0a9cb1b5a6443d67cfd0d98d4cec83a57ec/scipy-1.17.0-cp313-cp313t-win_amd64.whl", hash = "sha256:edce1a1cf66298cccdc48a1bdf8fb10a3bf58e8b58d6c3883dd1530e103f87c0", size = 36328593, upload-time = "2026-01-10T21:28:28.007Z" }, + { url = "https://files.pythonhosted.org/packages/13/1b/c87cc44a0d2c7aaf0f003aef2904c3d097b422a96c7e7c07f5efd9073c1b/scipy-1.17.0-cp313-cp313t-win_arm64.whl", hash = "sha256:30509da9dbec1c2ed8f168b8d8aa853bc6723fede1dbc23c7d43a56f5ab72a67", size = 24625083, upload-time = "2026-01-10T21:28:35.188Z" }, + { url = "https://files.pythonhosted.org/packages/1a/2d/51006cd369b8e7879e1c630999a19d1fbf6f8b5ed3e33374f29dc87e53b3/scipy-1.17.0-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:c17514d11b78be8f7e6331b983a65a7f5ca1fd037b95e27b280921fe5606286a", size = 31346803, upload-time = "2026-01-10T21:28:57.24Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2e/2349458c3ce445f53a6c93d4386b1c4c5c0c540917304c01222ff95ff317/scipy-1.17.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:4e00562e519c09da34c31685f6acc3aa384d4d50604db0f245c14e1b4488bfa2", size = 27967182, upload-time = "2026-01-10T21:29:04.107Z" }, + { url = "https://files.pythonhosted.org/packages/5e/7c/df525fbfa77b878d1cfe625249529514dc02f4fd5f45f0f6295676a76528/scipy-1.17.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f7df7941d71314e60a481e02d5ebcb3f0185b8d799c70d03d8258f6c80f3d467", size = 20139125, upload-time = "2026-01-10T21:29:10.179Z" }, + { url = "https://files.pythonhosted.org/packages/33/11/fcf9d43a7ed1234d31765ec643b0515a85a30b58eddccc5d5a4d12b5f194/scipy-1.17.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:aabf057c632798832f071a8dde013c2e26284043934f53b00489f1773b33527e", size = 22443554, upload-time = "2026-01-10T21:29:15.888Z" }, + { url = "https://files.pythonhosted.org/packages/80/5c/ea5d239cda2dd3d31399424967a24d556cf409fbea7b5b21412b0fd0a44f/scipy-1.17.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a38c3337e00be6fd8a95b4ed66b5d988bac4ec888fd922c2ea9fe5fb1603dd67", size = 32757834, upload-time = "2026-01-10T21:29:23.406Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7e/8c917cc573310e5dc91cbeead76f1b600d3fb17cf0969db02c9cf92e3cfa/scipy-1.17.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00fb5f8ec8398ad90215008d8b6009c9db9fa924fd4c7d6be307c6f945f9cd73", size = 34995775, upload-time = "2026-01-10T21:29:31.915Z" }, + { url = "https://files.pythonhosted.org/packages/c5/43/176c0c3c07b3f7df324e7cdd933d3e2c4898ca202b090bd5ba122f9fe270/scipy-1.17.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f2a4942b0f5f7c23c7cd641a0ca1955e2ae83dedcff537e3a0259096635e186b", size = 34841240, upload-time = "2026-01-10T21:29:39.995Z" }, + { url = "https://files.pythonhosted.org/packages/44/8c/d1f5f4b491160592e7f084d997de53a8e896a3ac01cd07e59f43ca222744/scipy-1.17.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dbf133ced83889583156566d2bdf7a07ff89228fe0c0cb727f777de92092ec6b", size = 37394463, upload-time = "2026-01-10T21:29:48.723Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ec/42a6657f8d2d087e750e9a5dde0b481fd135657f09eaf1cf5688bb23c338/scipy-1.17.0-cp314-cp314-win_amd64.whl", hash = "sha256:3625c631a7acd7cfd929e4e31d2582cf00f42fcf06011f59281271746d77e061", size = 37053015, upload-time = "2026-01-10T21:30:51.418Z" }, + { url = "https://files.pythonhosted.org/packages/27/58/6b89a6afd132787d89a362d443a7bddd511b8f41336a1ae47f9e4f000dc4/scipy-1.17.0-cp314-cp314-win_arm64.whl", hash = "sha256:9244608d27eafe02b20558523ba57f15c689357c85bdcfe920b1828750aa26eb", size = 24951312, upload-time = "2026-01-10T21:30:56.771Z" }, + { url = "https://files.pythonhosted.org/packages/e9/01/f58916b9d9ae0112b86d7c3b10b9e685625ce6e8248df139d0fcb17f7397/scipy-1.17.0-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:2b531f57e09c946f56ad0b4a3b2abee778789097871fc541e267d2eca081cff1", size = 31706502, upload-time = "2026-01-10T21:29:56.326Z" }, + { url = "https://files.pythonhosted.org/packages/59/8e/2912a87f94a7d1f8b38aabc0faf74b82d3b6c9e22be991c49979f0eceed8/scipy-1.17.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:13e861634a2c480bd237deb69333ac79ea1941b94568d4b0efa5db5e263d4fd1", size = 28380854, upload-time = "2026-01-10T21:30:01.554Z" }, + { url = "https://files.pythonhosted.org/packages/bd/1c/874137a52dddab7d5d595c1887089a2125d27d0601fce8c0026a24a92a0b/scipy-1.17.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:eb2651271135154aa24f6481cbae5cc8af1f0dd46e6533fb7b56aa9727b6a232", size = 20552752, upload-time = "2026-01-10T21:30:05.93Z" }, + { url = "https://files.pythonhosted.org/packages/3f/f0/7518d171cb735f6400f4576cf70f756d5b419a07fe1867da34e2c2c9c11b/scipy-1.17.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:c5e8647f60679790c2f5c76be17e2e9247dc6b98ad0d3b065861e082c56e078d", size = 22803972, upload-time = "2026-01-10T21:30:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/7c/74/3498563a2c619e8a3ebb4d75457486c249b19b5b04a30600dfd9af06bea5/scipy-1.17.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fb10d17e649e1446410895639f3385fd2bf4c3c7dfc9bea937bddcbc3d7b9ba", size = 32829770, upload-time = "2026-01-10T21:30:16.359Z" }, + { url = "https://files.pythonhosted.org/packages/48/d1/7b50cedd8c6c9d6f706b4b36fa8544d829c712a75e370f763b318e9638c1/scipy-1.17.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8547e7c57f932e7354a2319fab613981cde910631979f74c9b542bb167a8b9db", size = 35051093, upload-time = "2026-01-10T21:30:22.987Z" }, + { url = "https://files.pythonhosted.org/packages/e2/82/a2d684dfddb87ba1b3ea325df7c3293496ee9accb3a19abe9429bce94755/scipy-1.17.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33af70d040e8af9d5e7a38b5ed3b772adddd281e3062ff23fec49e49681c38cf", size = 34909905, upload-time = "2026-01-10T21:30:28.704Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5e/e565bd73991d42023eb82bb99e51c5b3d9e2c588ca9d4b3e2cc1d3ca62a6/scipy-1.17.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb55bb97d00f8b7ab95cb64f873eb0bf54d9446264d9f3609130381233483f", size = 37457743, upload-time = "2026-01-10T21:30:34.819Z" }, + { url = "https://files.pythonhosted.org/packages/58/a8/a66a75c3d8f1fb2b83f66007d6455a06a6f6cf5618c3dc35bc9b69dd096e/scipy-1.17.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1ff269abf702f6c7e67a4b7aad981d42871a11b9dd83c58d2d2ea624efbd1088", size = 37098574, upload-time = "2026-01-10T21:30:40.782Z" }, + { url = "https://files.pythonhosted.org/packages/56/a5/df8f46ef7da168f1bc52cd86e09a9de5c6f19cc1da04454d51b7d4f43408/scipy-1.17.0-cp314-cp314t-win_arm64.whl", hash = "sha256:031121914e295d9791319a1875444d55079885bbae5bdc9c5e0f2ee5f09d34ff", size = 25246266, upload-time = "2026-01-10T21:30:45.923Z" }, ] [[package]] @@ -3260,51 +3234,56 @@ wheels = [ [[package]] name = "tomli" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, - { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, - { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, - { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, - { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, - { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, - { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, - { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, - { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, - { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, - { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, - { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, - { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, - { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, - { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, - { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, - { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, - { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, - { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, - { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, - { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, - { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, - { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, - { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, - { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, - { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, - { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, - { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, - { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, - { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, - { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, - { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, - { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, - { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, - { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, - { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, - { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, - { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, - { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, - { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, - { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, ] [[package]] From 0e7a8308d05aecc8f4766df8a19ab4f08a610b21 Mon Sep 17 00:00:00 2001 From: novis10813 Date: Tue, 17 Feb 2026 22:41:09 +0800 Subject: [PATCH 25/25] fix: restore dev code and fix pandas 3.0 timestamp compatibility - Restore all src/ and tests/ from dev branch (merge conflict resolution had incorrectly reverted to main's older code) - Fix 19 occurrences of dates.astype(np.int64) // 10**6 pattern in tests to use dates.astype('datetime64[ms]').astype(np.int64) for pandas 3.0 compatibility (DatetimeIndex default resolution changed from ns to us) --- src/factorium/__init__.py | 4 +- src/factorium/aggbar.py | 36 +-- src/factorium/backtest/__init__.py | 14 +- src/factorium/backtest/backtester.py | 27 +- src/factorium/backtest/constraints.py | 4 +- src/factorium/backtest/metrics.py | 5 +- src/factorium/backtest/portfolio.py | 7 +- src/factorium/backtest/utils.py | 17 +- src/factorium/backtest/vectorized.py | 23 +- src/factorium/data/__init__.py | 8 +- src/factorium/data/adapters/base.py | 2 +- src/factorium/data/adapters/binance.py | 6 +- src/factorium/data/aggregator.py | 64 +++-- src/factorium/data/cache.py | 12 +- src/factorium/data/downloader.py | 191 ++++++------- src/factorium/data/loader.py | 164 +++++++---- src/factorium/data/parquet.py | 45 ++- src/factorium/factors/__init__.py | 13 +- src/factorium/factors/analyzer.py | 303 ++++++++++++++++++--- src/factorium/factors/base.py | 22 +- src/factorium/factors/composite.py | 12 +- src/factorium/factors/core.py | 58 ++-- src/factorium/factors/engine.py | 16 +- src/factorium/factors/mixins/__init__.py | 2 +- src/factorium/factors/mixins/cs_ops.py | 10 +- src/factorium/factors/mixins/math_ops.py | 27 +- src/factorium/factors/mixins/ts_ops.py | 8 +- src/factorium/factors/operators.py | 64 ++++- src/factorium/factors/parser.py | 141 +++++----- src/factorium/factors/plotting.py | 46 ++-- src/factorium/factors/plotting_analyzer.py | 51 +++- src/factorium/research/__init__.py | 2 +- src/factorium/research/report.py | 25 +- src/factorium/research/session.py | 67 +++-- src/factorium/storage/__init__.py | 12 +- src/factorium/storage/base.py | 3 +- src/factorium/storage/local.py | 27 +- src/factorium/storage/s3.py | 157 +++++++++++ tests/backtest/test_backtester.py | 12 +- tests/conftest.py | 2 +- tests/data/test_loader_fast.py | 24 +- tests/data/test_loader_klines.py | 20 +- tests/factors/test_analyzer.py | 7 +- tests/factors/test_analyzer_polars.py | 18 ++ tests/factors/test_base_polars.py | 6 +- tests/factors/test_engine.py | 2 +- tests/factors/test_factor_eval.py | 67 +++++ tests/factors/test_math_ops_polars.py | 6 +- tests/factors/test_ts_ops_polars.py | 4 +- tests/mixins/test_ts_ops.py | 4 +- tests/storage/conftest.py | 74 +++++ tests/storage/test_factory.py | 20 +- tests/storage/test_integration.py | 94 +++++++ tests/storage/test_local.py | 50 ++++ tests/storage/test_s3.py | 119 ++++++++ tests/storage/test_s3_integration.py | 127 +++++++++ tests/test_data_loader.py | 20 +- tests/test_evaluation.py | 72 +++-- tests/test_plotting.py | 2 +- 59 files changed, 1799 insertions(+), 646 deletions(-) create mode 100644 src/factorium/storage/s3.py create mode 100644 tests/factors/test_factor_eval.py create mode 100644 tests/storage/conftest.py create mode 100644 tests/storage/test_integration.py create mode 100644 tests/storage/test_s3.py create mode 100644 tests/storage/test_s3_integration.py diff --git a/src/factorium/__init__.py b/src/factorium/__init__.py index 989f35c..bfed669 100644 --- a/src/factorium/__init__.py +++ b/src/factorium/__init__.py @@ -29,10 +29,10 @@ >>> ranked = momentum.rank() """ -from .factors.core import Factor -from .factors.base import BaseFactor from .aggbar import AggBar from .data import BinanceDataLoader +from .factors.base import BaseFactor +from .factors.core import Factor from .research import ResearchSession from .universe import ( Checklist, diff --git a/src/factorium/aggbar.py b/src/factorium/aggbar.py index bd1b7c6..e0a6241 100644 --- a/src/factorium/aggbar.py +++ b/src/factorium/aggbar.py @@ -5,14 +5,17 @@ across multiple symbols in long format. """ +from datetime import datetime +from pathlib import Path +from typing import TYPE_CHECKING, Optional, Union, cast + +import numpy as np import pandas as pd import polars as pl -import numpy as np -from typing import Union, List, Optional, TYPE_CHECKING -from pathlib import Path -from datetime import datetime if TYPE_CHECKING: + from .bar import BaseBar + from .data.metadata import AggBarMetadata from .factors.core import Factor from .universe.checklist import Checklist from .universe.universe import Universe @@ -39,11 +42,10 @@ class AggBar: def __init__( self, - data: Union[List["BaseBar"], pd.DataFrame, pl.DataFrame], + data: list["BaseBar"] | pd.DataFrame | pl.DataFrame, metadata: Optional["AggBarMetadata"] = None, ): # Import here to avoid circular imports - from .data.metadata import AggBarMetadata # Convert to Polars if isinstance(data, list): @@ -80,18 +82,18 @@ def _compute_metadata(self) -> "AggBarMetadata": return AggBarMetadata( symbols=sorted(self._data["symbol"].unique().to_list()), - min_time=self._data["start_time"].min(), - max_time=self._data["end_time"].max(), + min_time=cast(int, self._data["start_time"].min()), + max_time=cast(int, self._data["end_time"].max()), num_rows=len(self._data), ) @classmethod - def from_bars(cls, bars: List) -> "AggBar": + def from_bars(cls, bars: list) -> "AggBar": """Create AggBar from a list of BaseBar objects.""" return cls(bars) @classmethod - def from_df(cls, df: Union[pd.DataFrame, pl.DataFrame]) -> "AggBar": + def from_df(cls, df: pd.DataFrame | pl.DataFrame) -> "AggBar": """Create AggBar from a DataFrame.""" return cls(df) @@ -124,7 +126,7 @@ def to_parquet(self, path: Path) -> Path: self._data.write_parquet(path) return path - def __getitem__(self, key: Union[str, List[str]]) -> Union["Factor", "AggBar"]: + def __getitem__(self, key: str | list[str]) -> Union["Factor", "AggBar"]: """ Get a column as a Factor or multiple columns as a new AggBar. @@ -157,9 +159,9 @@ def __getitem__(self, key: Union[str, List[str]]) -> Union["Factor", "AggBar"]: def slice( self, - start: Optional[Union[datetime, int, str]] = None, - end: Optional[Union[datetime, int, str]] = None, - symbols: Optional[List[str]] = None, + start: datetime | int | str | None = None, + end: datetime | int | str | None = None, + symbols: list[str] | None = None, ) -> "AggBar": """ Slice data by time range and/or symbols. @@ -173,7 +175,7 @@ def slice( New AggBar with filtered data """ - def convert_timestamp(value: Optional[Union[datetime, int, str]]) -> Optional[int]: + def convert_timestamp(value: datetime | int | str | None) -> int | None: if value is None: return None if isinstance(value, str): @@ -228,12 +230,12 @@ def with_mask( return AggBar(new_data, metadata=self._metadata) @property - def cols(self) -> List[str]: + def cols(self) -> list[str]: """Return list of column names.""" return self._data.columns @property - def symbols(self) -> List[str]: + def symbols(self) -> list[str]: """Return list of unique symbols from metadata.""" return self._metadata.symbols diff --git a/src/factorium/backtest/__init__.py b/src/factorium/backtest/__init__.py index d07bcda..0b7ac77 100644 --- a/src/factorium/backtest/__init__.py +++ b/src/factorium/backtest/__init__.py @@ -1,17 +1,18 @@ from .backtester import ( IterativeBacktester as LegacyBacktester, +) +from .backtester import ( IterativeBacktestResult as LegacyBacktestResult, ) -from .metrics import calculate_metrics -from .portfolio import Portfolio -from .vectorized import VectorizedBacktester, BacktestResult from .constraints import ( - WeightConstraint, - MaxPositionConstraint, LongOnlyConstraint, - MaxGrossExposureConstraint, MarketNeutralConstraint, + MaxGrossExposureConstraint, + MaxPositionConstraint, + WeightConstraint, ) +from .metrics import calculate_metrics +from .portfolio import Portfolio from .utils import ( MAX_PERIODS_PER_YEAR, MIN_PERIODS_PER_YEAR, @@ -21,6 +22,7 @@ normalize_weights, parse_frequency_to_seconds, ) +from .vectorized import BacktestResult, VectorizedBacktester # Backward compatibility: Backtester is now an alias for VectorizedBacktester Backtester = VectorizedBacktester diff --git a/src/factorium/backtest/backtester.py b/src/factorium/backtest/backtester.py index 2c830e4..ea30ac6 100644 --- a/src/factorium/backtest/backtester.py +++ b/src/factorium/backtest/backtester.py @@ -2,10 +2,9 @@ from __future__ import annotations -from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, Dict, Literal, Optional, Union - import warnings +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any, Literal import numpy as np import pandas as pd @@ -27,7 +26,7 @@ class IterativeBacktestResult: equity_curve: pd.Series returns: pd.Series - metrics: Dict[str, float] + metrics: dict[str, float] trades: pd.DataFrame portfolio_history: pd.DataFrame @@ -64,10 +63,10 @@ class IterativeBacktester: def __init__( self, - prices: "AggBar", - signal: "Factor", + prices: AggBar, + signal: Factor, entry_price: str = "close", - transaction_cost: Union[float, tuple[float, float]] = 0.0003, + transaction_cost: float | tuple[float, float] = 0.0003, initial_capital: float = 10000.0, full_rebalance: bool = False, neutralization: Literal["market", "none"] = "market", @@ -98,8 +97,8 @@ def __init__( self._validate_inputs() - self._portfolio: Optional[Portfolio] = None - self._result: Optional[IterativeBacktestResult] = None + self._portfolio: Portfolio | None = None + self._result: IterativeBacktestResult | None = None self._price_map: dict[Any, Any] = {} self._signal_map: dict[Any, Any] = {} @@ -167,9 +166,9 @@ def _calculate_target_holdings( def _generate_orders( self, target_holdings: pd.Series, - current_holdings: Dict[str, float], - ) -> Dict[str, float]: - orders: Dict[str, float] = {} + current_holdings: dict[str, float], + ) -> dict[str, float]: + orders: dict[str, float] = {} all_symbols = set(target_holdings.index) | set(current_holdings.keys()) for symbol in all_symbols: @@ -266,7 +265,7 @@ def _build_result(self) -> IterativeBacktestResult: return self._result - def summary(self) -> Dict[str, Any]: + def summary(self) -> dict[str, Any]: if self._result is None: raise RuntimeError("Must call run() before summary()") @@ -277,7 +276,7 @@ def summary(self) -> Dict[str, Any]: **self._result.metrics, } - def plot_equity(self, figsize: tuple[float, float] = (12, 6)) -> "matplotlib.figure.Figure": + def plot_equity(self, figsize: tuple[float, float] = (12, 6)) -> matplotlib.figure.Figure: import matplotlib.pyplot as plt if self._result is None: diff --git a/src/factorium/backtest/constraints.py b/src/factorium/backtest/constraints.py index f46f11a..cda6aae 100644 --- a/src/factorium/backtest/constraints.py +++ b/src/factorium/backtest/constraints.py @@ -4,10 +4,10 @@ Provides constraints for position sizing and weight bounds. """ -from typing import Optional, Dict -import polars as pl from abc import ABC, abstractmethod +import polars as pl + class WeightConstraint(ABC): """ diff --git a/src/factorium/backtest/metrics.py b/src/factorium/backtest/metrics.py index b82157d..cffa222 100644 --- a/src/factorium/backtest/metrics.py +++ b/src/factorium/backtest/metrics.py @@ -1,6 +1,5 @@ """Performance metrics calculation for backtesting.""" -from typing import Dict import numpy as np import pandas as pd @@ -11,7 +10,7 @@ def calculate_metrics( returns: pd.Series, risk_free_rate: float = 0.0, periods_per_year: float = 365.0 * 24, -) -> Dict[str, float]: +) -> dict[str, float]: if not MIN_PERIODS_PER_YEAR <= periods_per_year <= MAX_PERIODS_PER_YEAR: raise ValueError( f"periods_per_year must be between {MIN_PERIODS_PER_YEAR} and {MAX_PERIODS_PER_YEAR}, " @@ -99,5 +98,5 @@ def calculate_metrics( "var_95": var_95, "cvar_95": cvar_95, "win_rate": float(win_rate), - "profit_factor": profit_factor, + "profit_factor": float(profit_factor), } diff --git a/src/factorium/backtest/portfolio.py b/src/factorium/backtest/portfolio.py index de3f212..e7aa884 100644 --- a/src/factorium/backtest/portfolio.py +++ b/src/factorium/backtest/portfolio.py @@ -3,7 +3,6 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import Dict, List import pandas as pd @@ -16,9 +15,9 @@ class Portfolio: initial_capital: float = 10000.0 cash: float = field(init=False) - positions: Dict[str, float] = field(default_factory=dict) - history: List[Dict] = field(default_factory=list) - trade_log: List[Dict] = field(default_factory=list) + positions: dict[str, float] = field(default_factory=dict) + history: list[dict] = field(default_factory=list) + trade_log: list[dict] = field(default_factory=list) def __post_init__(self) -> None: self.cash = self.initial_capital diff --git a/src/factorium/backtest/utils.py b/src/factorium/backtest/utils.py index 012966c..9ca52aa 100644 --- a/src/factorium/backtest/utils.py +++ b/src/factorium/backtest/utils.py @@ -1,7 +1,6 @@ """Utility functions for backtesting.""" import re -from typing import Union import numpy as np import pandas as pd @@ -14,6 +13,14 @@ SECONDS_PER_YEAR, ) +# Re-export constants for backward compatibility +__all__ = [ + "MAX_PERIODS_PER_YEAR", + "MIN_PERIODS_PER_YEAR", + "POSITION_EPSILON", + "SECONDS_PER_YEAR", +] + # Backward compatibility alias POSITION_EPSILON = EPSILON @@ -153,10 +160,10 @@ def neutralize_weights_polars( def safe_divide( - a: Union[float, np.ndarray, pd.Series], - b: Union[float, np.ndarray, pd.Series], + a: float | np.ndarray | pd.Series, + b: float | np.ndarray | pd.Series, default: float = np.nan, -) -> Union[float, np.ndarray, pd.Series]: +) -> float | np.ndarray | pd.Series: """ Safe division that returns default when denominator is near zero. @@ -172,7 +179,7 @@ def safe_divide( """ # Handle scalar if isinstance(b, (int, float, np.floating, np.integer)): - if np.isnan(b) or abs(b) <= EPSILON: + if np.isnan(float(b)) or abs(float(b)) <= EPSILON: return default return a / b diff --git a/src/factorium/backtest/vectorized.py b/src/factorium/backtest/vectorized.py index 540f9a6..52a30d7 100644 --- a/src/factorium/backtest/vectorized.py +++ b/src/factorium/backtest/vectorized.py @@ -1,16 +1,17 @@ """Vectorized backtester using Polars for performance.""" from dataclasses import dataclass -from typing import Optional, Literal, Dict, Any, Union +from typing import Any, Literal + import numpy as np import pandas as pd import polars as pl from ..aggbar import AggBar -from ..factors.core import Factor from ..constants import EPSILON -from .utils import frequency_to_periods_per_year +from ..factors.core import Factor from .metrics import calculate_metrics +from .utils import frequency_to_periods_per_year @dataclass @@ -19,7 +20,7 @@ class BacktestResult: equity_curve: pl.DataFrame # columns: [end_time, total_value] returns: pl.DataFrame # columns: [end_time, return] - metrics: Dict[str, float] + metrics: dict[str, float] trades: pl.DataFrame # columns: [end_time, symbol, qty, price, cost] portfolio_history: pl.DataFrame # columns: [end_time, cash, market_value, total_value] @@ -40,7 +41,7 @@ class BacktestResultPandas: equity_curve: pd.DataFrame returns: pd.DataFrame - metrics: Dict[str, float] + metrics: dict[str, float] trades: pd.DataFrame portfolio_history: pd.DataFrame @@ -50,10 +51,10 @@ class VectorizedBacktester: def __init__( self, - prices: Union[AggBar, pl.DataFrame], - signal: Union[Factor, pl.DataFrame], + prices: AggBar | pl.DataFrame, + signal: Factor | pl.DataFrame, entry_price: str = "close", - transaction_cost: Union[float, tuple[float, float]] = 0.0003, + transaction_cost: float | tuple[float, float] = 0.0003, initial_capital: float = 10000.0, neutralization: Literal["market", "none"] = "market", frequency: str = "1h", @@ -108,7 +109,7 @@ def __init__( else: self.signal_df = signal - self._result: Optional[BacktestResult] = None + self._result: BacktestResult | None = None def run(self) -> BacktestResult: """ @@ -133,7 +134,7 @@ def run(self) -> BacktestResult: self._result = self._build_result(portfolio_history, combined) return self._result - def summary(self) -> Dict[str, Any]: + def summary(self) -> dict[str, Any]: """Return a summary of backtest results.""" if self._result is None: raise RuntimeError("Must call run() before summary()") @@ -270,7 +271,7 @@ def _calculate_equity(self, df: pl.DataFrame) -> pl.DataFrame: return equity.select(["end_time", "cash", "market_value", "total_value"]) - def _calculate_metrics(self, equity_history: pl.DataFrame) -> Dict[str, float]: + def _calculate_metrics(self, equity_history: pl.DataFrame) -> dict[str, float]: """Calculate performance metrics.""" # Convert to pandas for metrics calculation equity_pd = equity_history.to_pandas() diff --git a/src/factorium/data/__init__.py b/src/factorium/data/__init__.py index 9cc77a3..63ba728 100644 --- a/src/factorium/data/__init__.py +++ b/src/factorium/data/__init__.py @@ -11,15 +11,15 @@ from .aggregator import BarAggregator from .cache import BarCache -from .loader import BinanceDataLoader from .downloader import BinanceDataDownloader +from .loader import BinanceDataLoader from .metadata import AggBarMetadata from .parquet import ( - csv_to_parquet, - read_hive_parquet, + BINANCE_COLUMNS, build_hive_path, + csv_to_parquet, get_market_string, - BINANCE_COLUMNS, + read_hive_parquet, ) from .utils import calculate_date_range diff --git a/src/factorium/data/adapters/base.py b/src/factorium/data/adapters/base.py index ed667ef..f11bea3 100644 --- a/src/factorium/data/adapters/base.py +++ b/src/factorium/data/adapters/base.py @@ -52,7 +52,7 @@ def column_mappings(self) -> dict[str, ColumnMapping]: @abstractmethod def build_parquet_glob( self, - base_path: Path, + base_path: str, symbols: list[str], data_type: str, market_type: str, diff --git a/src/factorium/data/adapters/binance.py b/src/factorium/data/adapters/binance.py index 63d45f8..312b636 100644 --- a/src/factorium/data/adapters/binance.py +++ b/src/factorium/data/adapters/binance.py @@ -1,7 +1,5 @@ """Binance exchange adapter.""" -from pathlib import Path - from .base import BaseExchangeAdapter, ColumnMapping @@ -51,7 +49,7 @@ def _get_market_string(self, market_type: str, futures_type: str = "") -> str: def build_parquet_glob( self, - base_path: Path, + base_path: str, symbols: list[str], data_type: str, market_type: str, @@ -71,7 +69,7 @@ def build_parquet_glob( # For multiple symbols, we'll filter in SQL instead symbol_pattern = "symbol=*" - return str(base_path / f"market={market_str}" / f"data_type={data_type}" / symbol_pattern / "**/*.parquet") + return f"{base_path}/market={market_str}/data_type={data_type}/{symbol_pattern}/**/*.parquet" def get_download_url( self, diff --git a/src/factorium/data/aggregator.py b/src/factorium/data/aggregator.py index b019947..1e3d6ae 100644 --- a/src/factorium/data/aggregator.py +++ b/src/factorium/data/aggregator.py @@ -1,14 +1,22 @@ """DuckDB-based bar aggregator for high-performance OHLCV aggregation.""" import logging +from collections.abc import Callable +from typing import TYPE_CHECKING, cast import duckdb import polars as pl from .adapters.base import ColumnMapping +if TYPE_CHECKING: + from .metadata import AggBarMetadata + logger = logging.getLogger(__name__) +# Type alias for DuckDB connection configurator +DuckDBConfigurator = Callable[[duckdb.DuckDBPyConnection], None] + class BarAggregator: """High-performance bar aggregator using DuckDB SQL. @@ -20,8 +28,32 @@ class BarAggregator: - Memory efficient: aggregation happens in DuckDB - Fast: uses DuckDB's vectorized execution engine - Scalable: handles large datasets that don't fit in memory + + Args: + duckdb_configurator: Optional callback to configure DuckDB connection + (e.g., for S3 credentials). Called before queries. """ + def __init__(self, duckdb_configurator: DuckDBConfigurator | None = None): + """Initialize the aggregator. + + Args: + duckdb_configurator: Optional function to configure DuckDB connection. + Used for S3 backends to set credentials/endpoint. + """ + self._duckdb_configurator = duckdb_configurator + + def _get_connection(self) -> duckdb.DuckDBPyConnection: + """Get a configured DuckDB connection. + + Returns: + A DuckDB connection, optionally configured via the configurator. + """ + conn = duckdb.connect() + if self._duckdb_configurator: + self._duckdb_configurator(conn) + return conn + def _compute_metadata(self, df: pl.DataFrame) -> "AggBarMetadata": """Compute metadata from aggregated DataFrame. @@ -43,8 +75,8 @@ def _compute_metadata(self, df: pl.DataFrame) -> "AggBarMetadata": return AggBarMetadata( symbols=symbols, - min_time=min_time, - max_time=max_time, + min_time=cast(int, min_time), + max_time=cast(int, max_time), num_rows=num_rows, ) @@ -158,7 +190,7 @@ def aggregate_time_bars( """ try: - with duckdb.connect() as conn: + with self._get_connection() as conn: df = conn.execute(query).pl() metadata = self._compute_metadata(df) return df, metadata @@ -268,7 +300,7 @@ def aggregate_tick_bars( """ try: - with duckdb.connect() as conn: + with self._get_connection() as conn: df = conn.execute(query).pl() metadata = self._compute_metadata(df) return df, metadata @@ -355,25 +387,25 @@ def aggregate_volume_bars( -- Recursive CTE to compute greedy bar assignments (Greedy Packing algorithm) greedy AS ( -- Base case: first row - SELECT + SELECT seq, volume, volume AS running_volume, CAST(0 AS BIGINT) AS bar_id FROM numbered WHERE seq = 1 - + UNION ALL - + -- Recursive case: process remaining rows SELECT r.seq, r.volume, - CASE + CASE WHEN g.running_volume >= {interval_volume} THEN r.volume -- Reset after threshold ELSE g.running_volume + r.volume -- Continue accumulating END AS running_volume, - CASE + CASE WHEN g.running_volume >= {interval_volume} THEN g.bar_id + 1 -- New bar ELSE g.bar_id -- Same bar END AS bar_id @@ -413,7 +445,7 @@ def aggregate_volume_bars( """ try: - with duckdb.connect() as conn: + with self._get_connection() as conn: df = conn.execute(query).pl() metadata = self._compute_metadata(df) return df, metadata @@ -498,25 +530,25 @@ def aggregate_dollar_bars( -- Recursive CTE to compute greedy bar assignments (Greedy Packing algorithm) greedy AS ( -- Base case: first row - SELECT + SELECT seq, dollar_volume, dollar_volume AS running_dollar, CAST(0 AS BIGINT) AS bar_id FROM numbered WHERE seq = 1 - + UNION ALL - + -- Recursive case: process remaining rows SELECT r.seq, r.dollar_volume, - CASE + CASE WHEN g.running_dollar >= {interval_dollar} THEN r.dollar_volume -- Reset after threshold ELSE g.running_dollar + r.dollar_volume -- Continue accumulating END AS running_dollar, - CASE + CASE WHEN g.running_dollar >= {interval_dollar} THEN g.bar_id + 1 -- New bar ELSE g.bar_id -- Same bar END AS bar_id @@ -556,7 +588,7 @@ def aggregate_dollar_bars( """ try: - with duckdb.connect() as conn: + with self._get_connection() as conn: df = conn.execute(query).pl() metadata = self._compute_metadata(df) return df, metadata diff --git a/src/factorium/data/cache.py b/src/factorium/data/cache.py index 520c88d..f93016b 100644 --- a/src/factorium/data/cache.py +++ b/src/factorium/data/cache.py @@ -4,9 +4,11 @@ import json from datetime import datetime, timedelta from pathlib import Path +from typing import cast + import polars as pl -from ..storage import StorageBackend, LocalStorageBackend +from ..storage import LocalStorageBackend, StorageBackend class BarCache: @@ -23,6 +25,9 @@ class BarCache: Each day is stored as a separate Parquet file for efficient partial updates. """ + storage: StorageBackend + cache_dir: Path | None # for backward compatibility only + def __init__( self, storage: "StorageBackend | None" = None, @@ -36,6 +41,7 @@ def __init__( storage: StorageBackend instance. If None, creates LocalStorageBackend. cache_prefix: Prefix path for cache files within storage. cache_dir: DEPRECATED. Use storage parameter instead. + When using storage (or default), cache_dir attribute is set to None. """ if cache_dir is not None: # Backward compatibility @@ -52,9 +58,11 @@ def __init__( elif storage is None: self.storage = LocalStorageBackend("./Data") self.cache_prefix = cache_prefix + self.cache_dir = None # not used when storage is the source of truth else: self.storage = storage self.cache_prefix = cache_prefix + self.cache_dir = None # not used when storage is the source of truth if self.cache_prefix: self.storage.makedirs(self.cache_prefix) @@ -111,7 +119,7 @@ def get( cache_path = self._get_cache_path(exchange, symbols, interval_ms, data_type, market_type, date) if self.storage.exists(cache_path): - return self.storage.read_parquet(cache_path) + return cast(pl.DataFrame, self.storage.read_parquet(cache_path)) return None def get_range( diff --git a/src/factorium/data/downloader.py b/src/factorium/data/downloader.py index 77e2d72..356270a 100644 --- a/src/factorium/data/downloader.py +++ b/src/factorium/data/downloader.py @@ -4,18 +4,14 @@ Provides async download functionality for Binance Vision historical data. """ +import argparse import asyncio -import aiohttp -import aiofiles -import os -from datetime import datetime, timedelta -from pathlib import Path -import logging -from typing import Optional, Tuple, List import hashlib -import zipfile -import argparse +import logging import tempfile +import zipfile +from datetime import datetime, timedelta +from pathlib import Path import aiofiles # type: ignore[import-untyped] import aiohttp @@ -27,13 +23,13 @@ class BinanceDataDownloader: """ Asynchronous downloader for Binance Vision historical data. - + Args: base_path: Base directory for data storage max_concurrent_downloads: Maximum number of concurrent downloads retry_attempts: Number of retry attempts for failed downloads retry_delay: Delay between retries in seconds - + Example: >>> downloader = BinanceDataDownloader(base_path="./Data") >>> await downloader.download_data( @@ -45,41 +41,38 @@ class BinanceDataDownloader: ... end_date="2024-01-31" ... ) """ - + def __init__( self, base_path: str = "./Data", max_concurrent_downloads: int = 5, retry_attempts: int = 3, - retry_delay: int = 1 + retry_delay: int = 1, ): self.base_path = Path(base_path) self.max_concurrent_downloads = max_concurrent_downloads self.retry_attempts = retry_attempts self.retry_delay = retry_delay self._setup_logging() - + def _setup_logging(self): """Setup logging configuration.""" - logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(levelname)s - %(message)s' - ) + logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") self.logger = logging.getLogger(__name__) - + async def download_data( self, symbol: str, data_type: str, market_type: str, - futures_type: str = 'cm', - start_date: Optional[str] = None, - end_date: Optional[str] = None, - days: Optional[int] = None + futures_type: str = "cm", + start_date: str | None = None, + end_date: str | None = None, + days: int | None = None, ) -> None: """ Download data for specified parameters. - + Args: symbol: Trading symbol (e.g., BTCUSDT, BTCUSD_PERP) data_type: Data type (trades/klines/aggTrades) @@ -93,30 +86,23 @@ async def download_data( start_date_dt, end_date_dt = calculate_date_range(start_date, end_date, days) download_dir = self._setup_download_dir(symbol, data_type, market_type, futures_type) dates = self._generate_date_list(start_date_dt, end_date_dt) - + tasks = [] semaphore = asyncio.Semaphore(self.max_concurrent_downloads) - + async with aiohttp.ClientSession() as session: for date in dates: task = asyncio.create_task( self._download_single_day( - session, - symbol, - data_type, - market_type, - futures_type, - date, - download_dir, - semaphore + session, symbol, data_type, market_type, futures_type, date, download_dir, semaphore ) ) tasks.append(task) - + await asyncio.gather(*tasks) - + self._update_readme() - + async def _download_single_day( self, session: aiohttp.ClientSession, @@ -126,105 +112,101 @@ async def _download_single_day( futures_type: str, date: datetime, download_dir: Path, - semaphore: asyncio.Semaphore + semaphore: asyncio.Semaphore, ) -> None: """Download single day data and convert to Parquet with Hive partitioning.""" async with semaphore: date_str = date.strftime("%Y-%m-%d") self.logger.info(f"Processing data for {date_str}") - + filename = self._build_filename(symbol, data_type, date_str) checksum_filename = f"{filename}.CHECKSUM" base_url = self._build_base_url(market_type, data_type, symbol, futures_type) - + for attempt in range(self.retry_attempts): try: # Use temp directory for download with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) data_file_path = temp_path / filename - + if await self._download_file(session, f"{base_url}/{filename}", data_file_path): checksum_file_path = temp_path / checksum_filename - if await self._download_file(session, f"{base_url}/{checksum_filename}", checksum_file_path): + if await self._download_file( + session, f"{base_url}/{checksum_filename}", checksum_file_path + ): if await self._verify_checksum(data_file_path, checksum_file_path): # Extract ZIP to temp directory await self._extract_zip(data_file_path) - + # Find extracted CSV file - csv_filename = filename.replace('.zip', '.csv') + csv_filename = filename.replace(".zip", ".csv") csv_path = temp_path / csv_filename - + if csv_path.exists(): # Build Hive partition path and convert to Parquet market = get_market_string(market_type, futures_type) hive_path = build_hive_path( - self.base_path, market, data_type, symbol, - date.year, date.month, date.day + self.base_path, market, data_type, symbol, date.year, date.month, date.day ) csv_to_parquet(csv_path, hive_path, data_type=data_type) self.logger.info(f"✓ Successfully processed {filename} -> {hive_path}") return else: self.logger.error(f"CSV file not found after extraction: {csv_path}") - + self.logger.warning(f"Attempt {attempt + 1} failed for {date_str}") await asyncio.sleep(self.retry_delay) - + except Exception as e: self.logger.error(f"Error processing {date_str}: {str(e)}") await asyncio.sleep(self.retry_delay) - + self.logger.error(f"Failed to download data for {date_str} after {self.retry_attempts} attempts") - - async def _download_file( - self, - session: aiohttp.ClientSession, - url: str, - file_path: Path - ) -> bool: + + async def _download_file(self, session: aiohttp.ClientSession, url: str, file_path: Path) -> bool: """Download a single file.""" try: async with session.get(url) as response: if response.status == 200: - async with aiofiles.open(file_path, 'wb') as f: + async with aiofiles.open(file_path, "wb") as f: await f.write(await response.read()) return True return False except Exception as e: self.logger.error(f"Error downloading {url}: {str(e)}") return False - + async def _verify_checksum(self, data_file: Path, checksum_file: Path) -> bool: """Verify file checksum.""" try: - async with aiofiles.open(checksum_file, 'r') as f: + async with aiofiles.open(checksum_file) as f: expected_checksum = (await f.read()).split()[0] - - async with aiofiles.open(data_file, 'rb') as f: + + async with aiofiles.open(data_file, "rb") as f: file_content = await f.read() actual_checksum = hashlib.sha256(file_content).hexdigest() - - return expected_checksum == actual_checksum + + return bool(expected_checksum == actual_checksum) except Exception as e: self.logger.error(f"Error verifying checksum: {str(e)}") return False - + async def _extract_zip(self, zip_file: Path) -> None: """Extract ZIP file.""" try: - with zipfile.ZipFile(zip_file, 'r') as zip_ref: + with zipfile.ZipFile(zip_file, "r") as zip_ref: zip_ref.extractall(zip_file.parent) except Exception as e: self.logger.error(f"Error extracting {zip_file}: {str(e)}") - + def _validate_params(self, data_type: str, market_type: str, futures_type: str) -> None: """Validate parameters.""" - if data_type not in ['trades', 'klines', 'aggTrades', 'bookTicker', 'bookDepth']: + if data_type not in ["trades", "klines", "aggTrades", "bookTicker", "bookDepth"]: raise ValueError("Invalid data type") - if market_type not in ['spot', 'futures']: + if market_type not in ["spot", "futures"]: raise ValueError("Invalid market type") - if market_type == 'futures' and futures_type not in ['cm', 'um']: + if market_type == "futures" and futures_type not in ["cm", "um"]: raise ValueError("Invalid futures type") @@ -235,8 +217,8 @@ def _setup_download_dir(self, symbol: str, data_type: str, market_type: str, fut temp_dir = self.base_path / ".temp" temp_dir.mkdir(parents=True, exist_ok=True) return temp_dir - - def _generate_date_list(self, start_date: datetime, end_date: datetime) -> List[datetime]: + + def _generate_date_list(self, start_date: datetime, end_date: datetime) -> list[datetime]: """Generate list of dates.""" dates = [] current = start_date @@ -244,28 +226,28 @@ def _generate_date_list(self, start_date: datetime, end_date: datetime) -> List[ dates.append(current) current += timedelta(days=1) return dates - + def _build_filename(self, symbol: str, data_type: str, date_str: str) -> str: """Build filename.""" if data_type == "klines": return f"{symbol}-1m-{date_str}.zip" return f"{symbol}-{data_type}-{date_str}.zip" - - def _build_base_url(self, market_type: str, data_type: str, symbol: str, futures_type: str = 'cm') -> str: + + def _build_base_url(self, market_type: str, data_type: str, symbol: str, futures_type: str = "cm") -> str: """Build base URL.""" if market_type == "futures": market_path = f"futures/{futures_type}" else: market_path = "spot" - + base_url = f"https://data.binance.vision/data/{market_path}/daily/{data_type}/{symbol}" - + # Klines have an additional interval subdirectory if data_type == "klines": base_url = f"{base_url}/1m" - + return base_url - + def _update_readme(self) -> None: """Update README file.""" readme_content = f"""# Binance Market Data @@ -300,7 +282,7 @@ def _update_readme(self) -> None: - futures: Futures market data (CM/UM) - spot: Spot market data -Last updated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} +Last updated: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} """ with open(self.base_path / "README.md", "w") as f: f.write(readme_content) @@ -309,58 +291,59 @@ def _update_readme(self) -> None: def parse_args(): """Parse command line arguments.""" parser = argparse.ArgumentParser( - description='Download Binance market data', + description="Download Binance market data", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: # Download 7 days of futures trades data (CM) python -m factorium.data.downloader -s BTCUSD_PERP -t trades -m futures -f cm -d 7 - + # Download specific date range of futures trades data (UM) python -m factorium.data.downloader -s BTCUSDT -t trades -m futures -f um -r 2024-01-01:2024-01-31 - + # Download spot market data python -m factorium.data.downloader -s BTCUSDT -t klines -m spot -r 2024-01-01:2024-01-31 - """ + """, ) - - parser.add_argument('-s', '--symbol', default='BTCUSD_PERP', help='Trading symbol') - parser.add_argument('-t', '--data-type', default='trades', - choices=['trades', 'klines', 'aggTrades', 'bookTicker', 'bookDepth']) - parser.add_argument('-m', '--market-type', default='futures', choices=['spot', 'futures']) - parser.add_argument('-f', '--futures-type', default='cm', choices=['cm', 'um']) - parser.add_argument('-d', '--days', type=int, default=7) - parser.add_argument('-p', '--path', default='./Data') - parser.add_argument('-r', '--date-range', help='Date range YYYY-MM-DD:YYYY-MM-DD') - parser.add_argument('--max-concurrent', type=int, default=5) - parser.add_argument('--retry-attempts', type=int, default=3) - parser.add_argument('--retry-delay', type=int, default=1) - + + parser.add_argument("-s", "--symbol", default="BTCUSD_PERP", help="Trading symbol") + parser.add_argument( + "-t", "--data-type", default="trades", choices=["trades", "klines", "aggTrades", "bookTicker", "bookDepth"] + ) + parser.add_argument("-m", "--market-type", default="futures", choices=["spot", "futures"]) + parser.add_argument("-f", "--futures-type", default="cm", choices=["cm", "um"]) + parser.add_argument("-d", "--days", type=int, default=7) + parser.add_argument("-p", "--path", default="./Data") + parser.add_argument("-r", "--date-range", help="Date range YYYY-MM-DD:YYYY-MM-DD") + parser.add_argument("--max-concurrent", type=int, default=5) + parser.add_argument("--retry-attempts", type=int, default=3) + parser.add_argument("--retry-delay", type=int, default=1) + return parser.parse_args() async def main(): args = parse_args() - + try: downloader = BinanceDataDownloader( base_path=args.path, max_concurrent_downloads=args.max_concurrent, retry_attempts=args.retry_attempts, - retry_delay=args.retry_delay + retry_delay=args.retry_delay, ) - + start_date = None end_date = None if args.date_range: try: - start_date, end_date = args.date_range.split(':') + start_date, end_date = args.date_range.split(":") datetime.strptime(start_date, "%Y-%m-%d") datetime.strptime(end_date, "%Y-%m-%d") except ValueError: print("Error: Invalid date range format. Use YYYY-MM-DD:YYYY-MM-DD") return - + await downloader.download_data( symbol=args.symbol, data_type=args.data_type, @@ -368,9 +351,9 @@ async def main(): futures_type=args.futures_type, start_date=start_date, end_date=end_date, - days=args.days if not args.date_range else None + days=args.days if not args.date_range else None, ) - + except Exception as e: print(f"Error: {str(e)}") return diff --git a/src/factorium/data/loader.py b/src/factorium/data/loader.py index cfb3651..fb93aa4 100644 --- a/src/factorium/data/loader.py +++ b/src/factorium/data/loader.py @@ -8,12 +8,13 @@ import logging from datetime import datetime, timedelta from pathlib import Path -from typing import Optional, List, Literal +from typing import Literal, cast import duckdb -import pandas as pd import polars as pl +from ..storage import get_storage_backend + def _run_async(coro): """ @@ -22,7 +23,7 @@ def _run_async(coro): This is necessary for Jupyter notebooks which already have a running event loop. """ try: - loop = asyncio.get_running_loop() + asyncio.get_running_loop() except RuntimeError: # No running loop, use asyncio.run() return asyncio.run(coro) @@ -132,40 +133,71 @@ def _normalize_timestamps_to_ms(df: pl.DataFrame, ts_unit: str) -> pl.DataFrame: class BinanceDataLoader: """ - Data loader for Binance market data with automatic download. + Data loader for Binance market data with automatic download. - Uses DuckDB to query Parquet files stored in Hive partition format. + Uses DuckDB to query Parquet files stored in Hive partition format. - Args: - base_path: Base directory for data storage - max_concurrent_downloads: Maximum number of concurrent downloads - retry_attempts: Number of retry attempts for failed downloads - retry_delay: Delay between retries in seconds - - Example: - >>> loader = BinanceDataLoader() - >>> agg = loader.load_aggbar( - ... symbols=["BTCUSDT"], - ... data_type="aggTrades", - ... market_type="futures", - ... futures_type="um", - ... start_date="2024-01-01", - ... days=7, - ... bar_type="time", - ... interval=60_000, - ... ) + Args: + backend: Storage backend type - "local" or "s3" + path: For local: base directory. For S3: "bucket/prefix" + base_path: DEPRECATED. Use backend='local' and path instead. + max_concurrent_downloads: Maximum number of concurrent downloads + retry_attempts: Number of retry attempts for failed downloads + retry_delay: Delay between retries in seconds + + Example: + >>> loader = BinanceDataLoader() + >>> agg = loader.load_aggbar( + ... symbols=["BTCUSDT"], + ... data_type="aggTrades", + ... market_type="futures", + ... futures_type="um", + ... start_date="2024-01-01", + ... days=7, + ... bar_type="time", + ... interval=60_000, + ... ) + + + Note: + When using backend='s3', data must already exist in S3. + Automatic download to S3 is not yet supported. """ def __init__( self, - base_path: str = "./Data", + backend: str = "local", + path: str = "./Data", + *, + base_path: str | None = None, # Deprecated max_concurrent_downloads: int = 5, retry_attempts: int = 3, retry_delay: int = 1, ): - self.base_path = Path(base_path) + """... + + Args: + backend: Storage backend type - "local" or "s3" + path: For local: base directory. For S3: "bucket/prefix" + base_path: DEPRECATED. Use backend='local' and path instead. + ... + """ + if base_path is not None: + import warnings + + warnings.warn( + "base_path is deprecated, use backend='local' and path instead", + DeprecationWarning, + stacklevel=2, + ) + backend = "local" + path = base_path + + self.storage = get_storage_backend(backend, path) + self.backend = backend + self.base_path = Path(path) if backend == "local" else None self.downloader = BinanceDataDownloader( - base_path=base_path, + base_path=path, max_concurrent_downloads=max_concurrent_downloads, retry_attempts=retry_attempts, retry_delay=retry_delay, @@ -177,6 +209,23 @@ def _setup_logging(self): logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") self.logger = logging.getLogger(__name__) + def _build_parquet_relative_path(self, market: str, data_type: str, symbol: str, date: datetime) -> str: + """Build the relative path for a Parquet file. + + Args: + market: Market string (e.g., "futures_um", "spot") + data_type: Data type (e.g., "aggTrades", "klines") + symbol: Trading symbol + date: Date for the partition + + Returns: + Relative path to the Parquet file (e.g., "market=.../data.parquet") + """ + return ( + f"market={market}/data_type={data_type}/symbol={symbol}/" + f"year={date.year}/month={date.month:02d}/day={date.day:02d}/data.parquet" + ) + def _check_all_files_exist( self, symbol: str, @@ -191,24 +240,21 @@ def _check_all_files_exist( current = start_dt while current < end_dt: - hive_path = build_hive_path( - self.base_path, market, data_type, symbol, current.year, current.month, current.day - ) - parquet_file = hive_path / "data.parquet" - if not parquet_file.exists(): + relative_path = self._build_parquet_relative_path(market, data_type, symbol, current) + if not self.storage.exists(relative_path): return False current += timedelta(days=1) return True def load_aggbar( self, - symbols: str | List[str], + symbols: str | list[str], data_type: str, market_type: str, futures_type: str = "um", - start_date: Optional[str] = None, - end_date: Optional[str] = None, - days: Optional[int] = None, + start_date: str | None = None, + end_date: str | None = None, + days: int | None = None, bar_type: Literal["time", "tick", "volume", "dollar"] = "time", interval: float = 60_000, force_download: bool = False, @@ -326,8 +372,8 @@ def load_aggbar( market_str = get_market_string(market_type, futures_type) # Collect aggregated data - all_dfs: List[pl.DataFrame] = [] - all_metadata: List[AggBarMetadata] = [] + all_dfs: list[pl.DataFrame] = [] + all_metadata: list[AggBarMetadata] = [] current = start_dt # For non-time bars, process entire date range at once (no daily chunking) @@ -336,7 +382,7 @@ def load_aggbar( end_ts = int(end_dt.timestamp() * 1000) parquet_pattern = adapter.build_parquet_glob( - base_path=self.base_path, + base_path=self.storage.full_path(""), symbols=symbols, data_type=data_type, market_type=market_type, @@ -402,8 +448,8 @@ def load_aggbar( # Compute metadata for cached data cached_meta = AggBarMetadata( symbols=cached_df["symbol"].unique().sort().to_list(), - min_time=cached_df["start_time"].min(), - max_time=cached_df["end_time"].max(), + min_time=cast(int, cached_df["start_time"].min()), + max_time=cast(int, cached_df["end_time"].max()), num_rows=len(cached_df), ) all_metadata.append(cached_meta) @@ -417,7 +463,7 @@ def load_aggbar( day_end_ts = int((current + timedelta(days=1)).timestamp() * 1000) parquet_pattern = adapter.build_parquet_glob( - base_path=self.base_path, + base_path=self.storage.full_path(""), symbols=symbols, data_type=data_type, market_type=market_type, @@ -472,13 +518,13 @@ def load_aggbar( def load_aggbar_fast( self, - symbols: List[str], + symbols: list[str], data_type: str, market_type: str, futures_type: str = "um", - start_date: Optional[str] = None, - end_date: Optional[str] = None, - days: Optional[int] = None, + start_date: str | None = None, + end_date: str | None = None, + days: int | None = None, interval_ms: int = 60_000, force_download: bool = False, use_cache: bool = True, @@ -527,7 +573,7 @@ def load_aggbar_fast( def _check_all_symbols_exist( self, - symbols: List[str], + symbols: list[str], data_type: str, market_type: str, futures_type: str, @@ -542,7 +588,7 @@ def _check_all_symbols_exist( def _find_missing_files( self, - symbols: List[str], + symbols: list[str], data_type: str, market_type: str, futures_type: str, @@ -561,11 +607,8 @@ def _find_missing_files( symbol_missing: list[datetime] = [] current = start_dt while current < end_dt: - hive_path = build_hive_path( - self.base_path, market, data_type, symbol, current.year, current.month, current.day - ) - parquet_file = hive_path / "data.parquet" - if not parquet_file.exists(): + relative_path = self._build_parquet_relative_path(market, data_type, symbol, current) + if not self.storage.exists(relative_path): symbol_missing.append(current) current += timedelta(days=1) @@ -634,7 +677,7 @@ def _group_consecutive_dates(self, dates: list[datetime]) -> list[tuple[datetime def _download_all_symbols( self, - symbols: List[str], + symbols: list[str], data_type: str, market_type: str, futures_type: str, @@ -664,7 +707,7 @@ async def download_all(): def _load_klines_direct( self, - symbols: List[str], + symbols: list[str], data_type: str, market_type: str, futures_type: str, @@ -698,14 +741,13 @@ def _load_klines_direct( Returns: AggBar object containing klines OHLCV data with timestamps in milliseconds """ - import duckdb adapter = BinanceAdapter() market_str = get_market_string(market_type, futures_type) # Build parquet glob pattern parquet_pattern = adapter.build_parquet_glob( - base_path=self.base_path, + base_path=self.storage.full_path(""), symbols=symbols, data_type=data_type, market_type=market_type, @@ -716,9 +758,13 @@ def _load_klines_direct( # Use a single DuckDB connection for all queries con = duckdb.connect(":memory:") + # Configure S3 settings if using S3 backend + if hasattr(self.storage, "configure_duckdb_s3"): + self.storage.configure_duckdb_s3(con) + sample_query = f""" - SELECT open_time - FROM read_parquet('{parquet_pattern}', hive_partitioning=true) + SELECT open_time + FROM read_parquet('{parquet_pattern}', hive_partitioning=true) LIMIT 1 """ sample_result = con.execute(sample_query).fetchone() @@ -753,7 +799,7 @@ def _load_klines_direct( # quote_volume, count, taker_buy_volume, taker_buy_quote_volume query = f""" - SELECT + SELECT open_time as start_time, close_time as end_time, symbol, diff --git a/src/factorium/data/parquet.py b/src/factorium/data/parquet.py index 62eca70..c59bfd8 100644 --- a/src/factorium/data/parquet.py +++ b/src/factorium/data/parquet.py @@ -4,14 +4,13 @@ Provides functions for CSV to Parquet conversion and optimized reading via DuckDB. """ -import pyarrow as pa -import pyarrow.csv as pv -import pyarrow.parquet as pq -import duckdb +import logging from pathlib import Path -from typing import Optional, List + +import duckdb import pandas as pd -import logging +import pyarrow.csv as pv +import pyarrow.parquet as pq logger = logging.getLogger(__name__) @@ -37,7 +36,7 @@ def _detect_has_header(csv_path: Path) -> bool: Detect if CSV has a header by checking if first row contains numeric values. Binance data without headers will have numeric first column (trade id). """ - with open(csv_path, 'r') as f: + with open(csv_path) as f: first_line = f.readline().strip() if not first_line: return False @@ -55,28 +54,28 @@ def csv_to_parquet( output_dir: Path, compression: str = 'zstd', filename: str = 'data.parquet', - data_type: Optional[str] = None, + data_type: str | None = None, ) -> Path: """ Convert CSV file to Parquet format in target directory. - + Automatically detects if CSV has headers. For headerless Binance CSVs, uses predefined column names based on data_type. - + Args: csv_path: Path to input CSV file output_dir: Directory to write Parquet file (will be created if needed) compression: Compression codec ('zstd', 'snappy', 'gzip', or None) filename: Output filename data_type: Binance data type (aggTrades, trades, klines) for headerless CSVs - + Returns: Path to created Parquet file """ output_dir.mkdir(parents=True, exist_ok=True) - + has_header = _detect_has_header(csv_path) - + if has_header: # CSV has headers, read normally table = pv.read_csv(csv_path) @@ -91,7 +90,7 @@ def csv_to_parquet( # Fallback: read with auto-generated column names logger.warning(f"No column definitions for data_type={data_type}, using auto-generated names") table = pv.read_csv(csv_path) - + out_path = output_dir / filename pq.write_table(table, out_path, compression=compression) logger.debug(f"Converted {csv_path} -> {out_path}") @@ -100,20 +99,20 @@ def csv_to_parquet( def read_hive_parquet( base_path: str, - columns: Optional[List[str]] = None, - where: Optional[str] = None, + columns: list[str] | None = None, + where: str | None = None, ) -> pd.DataFrame: """ Read Parquet files with Hive partitioning via DuckDB. - + Args: base_path: Glob pattern to Parquet files (e.g., 'Data/market=*/**/*.parquet') columns: Optional list of columns to select where: Optional WHERE clause (without 'WHERE' keyword) - + Returns: DataFrame with query results - + Example: >>> df = read_hive_parquet( ... 'Data/market=futures_um/data_type=klines/**/*.parquet', @@ -141,7 +140,7 @@ def build_hive_path( ) -> Path: """ Build Hive-style partition path. - + Args: base_path: Base data directory market: Market type (futures_cm, futures_um, spot) @@ -150,7 +149,7 @@ def build_hive_path( year: Year month: Month (1-12) day: Day (1-31) - + Returns: Path to partition directory """ @@ -168,11 +167,11 @@ def build_hive_path( def get_market_string(market_type: str, futures_type: str = '') -> str: """ Get combined market string for Hive partition. - + Args: market_type: 'spot' or 'futures' futures_type: 'cm' or 'um' (only for futures) - + Returns: Market string: 'spot', 'futures_cm', or 'futures_um' """ diff --git a/src/factorium/factors/__init__.py b/src/factorium/factors/__init__.py index 7e3eaa1..8f4bf02 100644 --- a/src/factorium/factors/__init__.py +++ b/src/factorium/factors/__init__.py @@ -9,15 +9,14 @@ - operators: Functional operators for factor expressions """ -from .core import Factor -from .base import BaseFactor -from .parser import FactorExpressionParser -from .analyzer import FactorAnalyzer, FactorAnalysisResult -from .engine import PolarsEngine -from .composite import CompositeFactor - # Import all operators from . import operators +from .analyzer import FactorAnalysisResult, FactorAnalyzer +from .base import BaseFactor +from .composite import CompositeFactor +from .core import Factor +from .engine import PolarsEngine +from .parser import FactorExpressionParser __all__ = [ "Factor", diff --git a/src/factorium/factors/analyzer.py b/src/factorium/factors/analyzer.py index cae1179..2488959 100644 --- a/src/factorium/factors/analyzer.py +++ b/src/factorium/factors/analyzer.py @@ -1,12 +1,14 @@ -import pandas as pd -import polars as pl -import numpy as np import logging from dataclasses import dataclass -from typing import Union, List, Optional, Dict, Any -from .core import Factor -from ..aggbar import AggBar +from typing import Any + import matplotlib.figure as mpl_figure +import numpy as np +import pandas as pd +import polars as pl + +from ..aggbar import AggBar +from .core import Factor logger = logging.getLogger(__name__) @@ -18,23 +20,30 @@ class FactorAnalysisResult: Attributes: factor_name: Name of the analyzed factor - periods: Analysis periods (forward return horizons) + periods: Analysis periods (forward return horizons) - always a list quantiles: Number of quantiles used ic_series: Information Coefficient time series - ic_summary: Summary statistics of IC (mean, std, ir, t-stat) - quantile_returns: Mean returns by quantile + ic_summary: Summary statistics of IC, keyed by period + Dict[int, Dict[str, float]] with mean_ic, ic_std, ic_ir, t-stat + turnover_series: Turnover time series (1 - rank autocorrelation) + turnover_mean: Average turnover across all periods + quantile_returns: Mean returns by quantile, keyed by period + Dict[int, pd.DataFrame] cumulative_returns: Cumulative returns by quantile (if available) + Dict[int, pd.DataFrame] or None """ factor_name: str - periods: int + periods: list[int] quantiles: int ic_series: pd.DataFrame - ic_summary: Dict[str, float] - quantile_returns: pd.DataFrame - cumulative_returns: Optional[pd.DataFrame] = None + ic_summary: dict[int, dict[str, float]] + turnover_series: pd.Series + turnover_mean: float + quantile_returns: dict[int, pd.DataFrame] + cumulative_returns: dict[int, pd.DataFrame] | None = None - def to_dict(self) -> Dict[str, Any]: + def to_dict(self) -> dict[str, Any]: """Convert to dictionary for backward compatibility.""" return { "factor_name": self.factor_name, @@ -42,18 +51,168 @@ def to_dict(self) -> Dict[str, Any]: "quantiles": self.quantiles, "ic_series": self.ic_series, "ic_summary": self.ic_summary, + "turnover_series": self.turnover_series, + "turnover_mean": self.turnover_mean, "quantile_returns": self.quantile_returns, "cumulative_returns": self.cumulative_returns, } def __repr__(self) -> str: - ic = self.ic_summary - return f"""FactorAnalysisResult: {self.factor_name} - Periods: {self.periods}, Quantiles: {self.quantiles} - Mean IC: {ic.get("mean_ic", 0):.4f} - IC Std: {ic.get("ic_std", 0):.4f} - IC IR: {ic.get("ic_ir", 0):.4f} -""" + lines = [f"FactorAnalysisResult: {self.factor_name}"] + lines.append(f" Periods: {self.periods}, Quantiles: {self.quantiles}") + for p in self.periods: + ic = self.ic_summary.get(p, {}) + lines.append(f" Period {p}: IC={ic.get('mean_ic', 0):.4f}, IR={ic.get('ic_ir', 0):.4f}") + lines.append(f" Turnover: {self.turnover_mean:.4f}") + return "\n".join(lines) + "\n" + + def save(self, output_dir: str) -> None: + """ + Save analysis results to directory with timestamp. + + Creates structure (single horizon): + {output_dir}/ + └── YYYYMMDD_HHMMSS_{factor_name}/ + ├── config.json + ├── ic_series.csv + ├── ic_summary.csv + ├── turnover.csv + ├── quantile_returns.csv + ├── cumulative_returns.csv + └── plots/ + ├── ic_distribution.png + ├── ic_timeseries.png + ├── quantile_returns.png + └── cumulative_returns.png + + Multi-horizon structure (periods=[1, 5, 20]): + {output_dir}/ + └── YYYYMMDD_HHMMSS_{factor_name}/ + ├── config.json + ├── ic_series.csv # columns: period_1, period_5, period_20 + ├── ic_summary.csv # rows indexed by period + ├── turnover.csv + ├── quantile_returns_period_1.csv # per-period files + ├── quantile_returns_period_5.csv + ├── quantile_returns_period_20.csv + ├── cumulative_returns_period_1.csv + ├── cumulative_returns_period_5.csv + ├── cumulative_returns_period_20.csv + └── plots/ + ├── ic_distribution.png + ├── ic_timeseries.png + ├── ic_decay.png # IC decay curve (multi-horizon only) + ├── quantile_returns_period_1.png + ├── quantile_returns_period_5.png + ├── quantile_returns_period_20.png + ├── cumulative_returns_period_1.png + ├── cumulative_returns_period_5.png + └── cumulative_returns_period_20.png + + Args: + output_dir: Base directory for experiment outputs + """ + import json + from datetime import datetime + from pathlib import Path + + import matplotlib.pyplot as plt + + from .plotting_analyzer import FactorAnalyzerPlotter + + # Create timestamped folder + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + folder_name = f"{timestamp}_{self.factor_name}" + exp_path = Path(output_dir) / folder_name + exp_path.mkdir(parents=True, exist_ok=True) + + # Create plots subdirectory + plots_path = exp_path / "plots" + plots_path.mkdir(exist_ok=True) + + # Save CSV files + self.ic_series.to_csv(exp_path / "ic_series.csv") + + # Convert ic_summary to DataFrame for CSV (always dict[int, dict[str, float]] now) + ic_summary_df = pd.DataFrame(self.ic_summary).T + ic_summary_df.index.name = "period" + ic_summary_df.to_csv(exp_path / "ic_summary.csv") + + self.turnover_series.to_csv(exp_path / "turnover.csv", header=True) + + # Handle quantile_returns (always dict[int, pd.DataFrame] now) + for p, df in self.quantile_returns.items(): + df.to_csv(exp_path / f"quantile_returns_period_{p}.csv") + + if self.cumulative_returns is not None: + # Always dict[int, pd.DataFrame] now + for p, df in self.cumulative_returns.items(): + df.to_csv(exp_path / f"cumulative_returns_period_{p}.csv") + + # Save plots + plotter = FactorAnalyzerPlotter() + + # IC time series plot + try: + fig_ic_ts = plotter.plot_ic_ts(self.ic_series) + fig_ic_ts.savefig(plots_path / "ic_timeseries.png", dpi=150, bbox_inches="tight") + plt.close(fig_ic_ts) + except Exception as e: + logger.warning(f"Failed to generate IC timeseries plot: {e}") + + # IC distribution plot + try: + fig_ic_hist = plotter.plot_ic_hist(self.ic_series) + fig_ic_hist.savefig(plots_path / "ic_distribution.png", dpi=150, bbox_inches="tight") + plt.close(fig_ic_hist) + except Exception as e: + logger.warning(f"Failed to generate IC distribution plot: {e}") + + # Quantile returns plot (always per-period now) + for p, df in self.quantile_returns.items(): + try: + fig_qret = plotter.plot_quantile_returns(df) + fig_qret.savefig(plots_path / f"quantile_returns_period_{p}.png", dpi=150, bbox_inches="tight") + plt.close(fig_qret) + except Exception as e: + logger.warning(f"Failed to generate quantile returns plot for period {p}: {e}") + + # Cumulative returns plot (if available, always per-period now) + if self.cumulative_returns is not None: + for p, df in self.cumulative_returns.items(): + try: + fig_cumret = plotter.plot_cumulative_returns(df) + fig_cumret.savefig(plots_path / f"cumulative_returns_period_{p}.png", dpi=150, bbox_inches="tight") + plt.close(fig_cumret) + except Exception as e: + logger.warning(f"Failed to generate cumulative returns plot for period {p}: {e}") + + # IC decay plot (multi-horizon only) + if isinstance(self.periods, list) and len(self.periods) > 1: + try: + fig_decay = plotter.plot_ic_decay(self.ic_summary) + fig_decay.savefig(plots_path / "ic_decay.png", dpi=150, bbox_inches="tight") + plt.close(fig_decay) + except Exception as e: + logger.warning(f"Failed to generate IC decay plot: {e}") + + # Save config.json + config = { + "factor_name": self.factor_name, + "periods": self.periods, + "quantiles": self.quantiles, + "created_at": datetime.now().isoformat(), + "data_range": { + "start": str(self.ic_series.index.min()), + "end": str(self.ic_series.index.max()), + "n_observations": len(self.ic_series), + }, + } + + with open(exp_path / "config.json", "w") as f: + json.dump(config, f, indent=2) + + logger.info(f"Results saved to {exp_path}") class FactorAnalyzer: @@ -70,7 +229,12 @@ def __init__(self, factor: Factor, prices: AggBar | Factor, quantiles: int = 5, self._mask = mask if isinstance(prices, AggBar): try: - self.prices = prices["close"] + close_col = prices["close"] + if isinstance(close_col, Factor): + self.prices = close_col + else: + # close column is not a Factor (AggBar), skip + self.prices = None except KeyError: # If 'close' is not there, we'll wait for price_col in prepare_data self.prices = None @@ -92,45 +256,71 @@ def analyze(self, price_col: str = "close", periods: int | list[int] = 1) -> Fac """ Run full factor analysis. + Args: + price_col: Column name for prices. + periods: Single period (int) or list of periods for multi-horizon analysis. + Returns: FactorAnalysisResult with IC series, summary, and quantile returns + + Raises: + ValueError: If periods is an empty list. """ + # Normalize periods to list for internal processing + periods_list = [periods] if isinstance(periods, int) else periods + + # Validate periods + if not periods_list: + raise ValueError("Periods list cannot be empty.") + # Prepare data - self.prepare_data(price_col=price_col, periods=[periods]) + self.prepare_data(price_col=price_col, periods=periods_list) # Calculate IC ic_series = self.calculate_ic() ic_summary_df = self.calculate_ic_summary() - # Convert IC summary to dict for single period as expected by FactorAnalysisResult - col = f"period_{periods}" - ic_summary = { - "mean_ic": ic_summary_df.loc["mean", col] if col in ic_summary_df.columns else 0.0, - "ic_std": ic_summary_df.loc["std", col] if col in ic_summary_df.columns else 0.0, - "ic_ir": ic_summary_df.loc["ic_ir", col] if col in ic_summary_df.columns else 0.0, - "t-stat": ic_summary_df.loc["t-stat", col] if col in ic_summary_df.columns else 0.0, - } + # Build ic_summary - always use dict[int, dict[str, float]] format + ic_summary: dict[int, dict[str, float]] = {} + for p in periods_list: + col = f"period_{p}" + ic_summary[p] = { + "mean_ic": float(ic_summary_df.loc["mean", col]) if col in ic_summary_df.columns else 0.0, + "ic_std": float(ic_summary_df.loc["std", col]) if col in ic_summary_df.columns else 0.0, + "ic_ir": float(ic_summary_df.loc["ic_ir", col]) if col in ic_summary_df.columns else 0.0, + "t-stat": float(ic_summary_df.loc["t-stat", col]) if col in ic_summary_df.columns else 0.0, + } - # Calculate quantile returns - quantile_returns = self.calculate_quantile_returns(quantiles=self.quantiles, period=periods) + # Calculate quantile returns - always use dict[int, pd.DataFrame] format + quantile_returns: dict[int, pd.DataFrame] = { + p: self.calculate_quantile_returns(quantiles=self.quantiles, period=p) for p in periods_list + } - # Calculate cumulative returns (optional) + # Calculate cumulative returns (optional) - always use dict[int, pd.DataFrame] format try: - cumulative_returns = self.calculate_cumulative_returns(quantiles=self.quantiles, period=periods) + cumulative_returns: dict[int, pd.DataFrame] | None = { + p: self.calculate_cumulative_returns(quantiles=self.quantiles, period=p) for p in periods_list + } except Exception: cumulative_returns = None + # Calculate turnover + turnover_series = self.calculate_turnover() + turnover_mean = float(turnover_series.mean()) + return FactorAnalysisResult( factor_name=self.factor.name, - periods=periods, + periods=periods_list, quantiles=self.quantiles, ic_series=ic_series, ic_summary=ic_summary, + turnover_series=turnover_series, + turnover_mean=turnover_mean, quantile_returns=quantile_returns, cumulative_returns=cumulative_returns, ) - def prepare_data(self, periods: Optional[List[int]] = None, price_col: Optional[str] = None) -> pl.DataFrame: + def prepare_data(self, periods: list[int] | None = None, price_col: str | None = None) -> pl.DataFrame: """ Prepare data for analysis by aligning factor values with future returns. @@ -164,7 +354,7 @@ def prepare_data(self, periods: Optional[List[int]] = None, price_col: Optional[ prices_lf = self._raw_prices.to_polars().lazy().select(select_cols) price_col_name = price_col elif self.prices is not None: - # self.prices is a Factor + # self.prices is a Factor (we've narrowed the type above) prices_lf = self.prices.lazy.rename({"factor": "__price__"}) price_col_name = "__price__" else: @@ -218,7 +408,7 @@ def calculate_ic(self, method: str = "rank") -> pd.DataFrame: ic_df = ( self._clean_data.group_by("start_time") - .agg([pl.corr("factor", col, method=corr_method).alias(col) for col in period_cols]) + .agg([pl.corr("factor", col, method=corr_method).alias(col) for col in period_cols]) # type: ignore[call-overload] .sort("start_time") ) @@ -402,3 +592,38 @@ def plot_cumulative_returns( cum_ret = self.calculate_cumulative_returns(quantiles=quantiles, period=period, long_short=long_short) plotter = FactorAnalyzerPlotter() return plotter.plot_cumulative_returns(cum_ret) + + def plot_ic_decay(self, periods: list[int] | None = None, method: str = "rank") -> mpl_figure.Figure: + """ + Plot IC decay curve across multiple horizons. + + Args: + periods: List of periods to plot. If None, uses all available periods. + method: 'rank' for Spearman, 'normal' for Pearson. + + Returns: + matplotlib Figure + """ + from .plotting_analyzer import FactorAnalyzerPlotter + + ic_summary_df = self.calculate_ic_summary(method=method) + + # Build ic_summary dict for plotting + if periods is None: + # Extract periods from available columns + periods = [int(c.replace("period_", "")) for c in ic_summary_df.columns if c.startswith("period_")] + + ic_summary = {} + for p in periods: + col = f"period_{p}" + if col in ic_summary_df.columns: + ic_summary[p] = { + "mean_ic": float(ic_summary_df.loc["mean", col]), + "ic_ir": float(ic_summary_df.loc["ic_ir", col]), + } + + if not ic_summary: + raise ValueError("No IC data available for the specified periods.") + + plotter = FactorAnalyzerPlotter() + return plotter.plot_ic_decay(ic_summary) diff --git a/src/factorium/factors/base.py b/src/factorium/factors/base.py index 6773722..2bcd548 100644 --- a/src/factorium/factors/base.py +++ b/src/factorium/factors/base.py @@ -1,14 +1,16 @@ -from typing import Union, Optional, Callable, TYPE_CHECKING +from collections.abc import Callable +from typing import TYPE_CHECKING, Union try: from typing import Self except ImportError: - from typing_extensions import Self + from typing import Self from abc import ABC -import pandas as pd -import polars as pl from pathlib import Path + import numpy as np +import pandas as pd +import polars as pl from ..constants import EPSILON @@ -17,9 +19,7 @@ class BaseFactor(ABC): - def __init__( - self, data: Union["AggBar", pd.DataFrame, pl.DataFrame, pl.LazyFrame, Path], name: Optional[str] = None - ): + def __init__(self, data: Union["AggBar", pd.DataFrame, pl.DataFrame, pl.LazyFrame, Path], name: str | None = None): self._name = name or "factor" self._lf = self._to_lazy(data) @@ -153,7 +153,7 @@ def _from_polars(self, pl_df: pl.DataFrame, name: str) -> Self: return self.__class__(pl_df, name) def _binary_op( - self, other: Union["BaseFactor", float], op_func: Callable, op_name: str, scalar_suffix: Optional[str] = None + self, other: Union["BaseFactor", float], op_func: Callable, op_name: str, scalar_suffix: str | None = None ) -> Self: if isinstance(other, self.__class__): # Use Polars LazyFrame join for factor-factor operations @@ -343,10 +343,10 @@ def __gt__(self, other: Union["BaseFactor", float]) -> Self: def __ge__(self, other: Union["BaseFactor", float]) -> Self: return self._comparison_op(other, lambda x, y: x >= y, ">=") - def __eq__(self, other: Union["BaseFactor", float]) -> Self: + def __eq__(self, other: Union["BaseFactor", float]) -> Self: # type: ignore[override] return self._comparison_op(other, lambda x, y: x == y, "==") - def __ne__(self, other: Union["BaseFactor", float]) -> Self: + def __ne__(self, other: Union["BaseFactor", float]) -> Self: # type: ignore[override] return self._comparison_op(other, lambda x, y: x != y, "!=") def __len__(self) -> int: @@ -356,4 +356,4 @@ def __len__(self) -> int: which is much faster than collecting the full dataset but still requires execution. Avoid calling in tight loops. """ - return self._lf.select(pl.len()).collect().item() + return int(self._lf.select(pl.len()).collect().item()) diff --git a/src/factorium/factors/composite.py b/src/factorium/factors/composite.py index 57e5980..b08347a 100644 --- a/src/factorium/factors/composite.py +++ b/src/factorium/factors/composite.py @@ -4,7 +4,6 @@ Allows combining multiple factors using weighted combinations. """ -from typing import List, Dict, Optional import polars as pl from .core import Factor @@ -28,8 +27,8 @@ class CompositeFactor: def __init__( self, - factors: List[Factor], - weights: Optional[List[float]] = None, + factors: list[Factor], + weights: list[float] | None = None, name: str = "composite", ): if len(factors) == 0: @@ -46,7 +45,7 @@ def __init__( self.name = name @classmethod - def from_equal_weights(cls, factors: List[Factor], name: str = "composite") -> "CompositeFactor": + def from_equal_weights(cls, factors: list[Factor], name: str = "composite") -> "CompositeFactor": """ Create composite with equal weights. @@ -60,7 +59,7 @@ def from_equal_weights(cls, factors: List[Factor], name: str = "composite") -> " return cls(factors, weights=None, name=name) @classmethod - def from_weights(cls, factors: List[Factor], weights: List[float], name: str = "composite") -> "CompositeFactor": + def from_weights(cls, factors: list[Factor], weights: list[float], name: str = "composite") -> "CompositeFactor": """ Create composite with custom weights. @@ -80,7 +79,7 @@ def from_weights(cls, factors: List[Factor], weights: List[float], name: str = " return cls(factors, weights=weights, name=name) @classmethod - def from_zscore(cls, factors: List[Factor], name: str = "composite_zscore") -> "CompositeFactor": + def from_zscore(cls, factors: list[Factor], name: str = "composite_zscore") -> "CompositeFactor": """ Create composite by standardizing factors first (z-score). @@ -94,7 +93,6 @@ def from_zscore(cls, factors: List[Factor], name: str = "composite_zscore") -> " Returns: CompositeFactor with z-score normalized factors """ - import numpy as np # Standardize each factor standardized = [] diff --git a/src/factorium/factors/core.py b/src/factorium/factors/core.py index de51b6d..2168056 100644 --- a/src/factorium/factors/core.py +++ b/src/factorium/factors/core.py @@ -1,17 +1,18 @@ -import pandas as pd -import matplotlib.figure as mpl_figure - -from typing import Union, Optional, List, Tuple, Dict, Any, TYPE_CHECKING -from pathlib import Path from datetime import datetime +from pathlib import Path +from typing import TYPE_CHECKING, Union + +import matplotlib.figure as mpl_figure +import pandas as pd from .base import BaseFactor +from .mixins.cs_ops import CrossSectionalOpsMixin from .mixins.math_ops import MathOpsMixin from .mixins.ts_ops import TimeSeriesOpsMixin -from .mixins.cs_ops import CrossSectionalOpsMixin if TYPE_CHECKING: from ..aggbar import AggBar + from .analyzer import FactorAnalysisResult class Factor(CrossSectionalOpsMixin, TimeSeriesOpsMixin, MathOpsMixin, BaseFactor): @@ -33,11 +34,11 @@ class Factor(CrossSectionalOpsMixin, TimeSeriesOpsMixin, MathOpsMixin, BaseFacto >>> ranked.plot(plot_type='timeseries') """ - def __init__(self, data: Union["AggBar", pd.DataFrame, Path], name: Optional[str] = None): + def __init__(self, data: Union["AggBar", pd.DataFrame, Path], name: str | None = None): super().__init__(data, name) @classmethod - def from_expression(cls, expr: str, context: Dict[str, "Factor"]) -> "Factor": + def from_expression(cls, expr: str, context: dict[str, "Factor"]) -> "Factor": """ Create a Factor from an expression string. @@ -62,36 +63,35 @@ def from_expression(cls, expr: str, context: Dict[str, "Factor"]) -> "Factor": def eval( self, - prices: "Factor", - periods: List[int] = [1, 5, 10], + prices: Union["Factor", "AggBar"], + periods: int = 1, # MVP 僅支援單一窗口 quantiles: int = 5, output_dir: str | None = None, price_col: str = "close", mask: str | None = None, **kwargs, - ) -> Dict[str, Any]: + ) -> "FactorAnalysisResult": """ - Run a full evaluation report for the factor. + Evaluate factor's predictive power (Evaluation Layer). Args: - prices: A Factor object containing price data (e.g., close prices) - periods: List of holding periods to evaluate (e.g., [1, 5, 10] days) - quantiles: Number of quantiles for layer testing - save_path: Path to save the evaluation report plot (e.g., 'report.png') - **kwargs: Additional arguments passed to the evaluator + prices: Price data (Factor or AggBar) + periods: Prediction horizon (currently only supports single int) + quantiles: Number of quantiles for layer analysis (default 5) + output_dir: Experiment output directory (creates timestamped folder if specified) + price_col: Price column name (default "close") Returns: - Dictionary containing evaluation metrics: - - ic_mean: Mean IC for each period - - ic_ir: IC Information Ratio for each period - - turnover_mean: Average factor turnover - - layer_returns: Average returns for each quantile - - spread: Long-short spread (Top - Bottom quantile) + FactorAnalysisResult: Complete evaluation metrics including IC, ICIR, t-stat, + turnover, quantile returns, and cumulative returns Example: - >>> factor.eval(prices=close_factor, periods=[1, 5, 20], save_path='eval.png') + >>> momentum = ts_returns(close, 20) + >>> result = momentum.eval(prices, output_dir="./experiments") + >>> print(result.ic_summary) + {'mean_ic': 0.05, 'ic_ir': 1.2, 't_stat': 3.5, ...} """ - from .evaluation import FactorEvaluator + from .analyzer import FactorAnalyzer analyzer = FactorAnalyzer(factor=self, prices=prices, quantiles=quantiles, mask=mask) @@ -106,10 +106,10 @@ def eval( def plot( self, plot_type: str = "timeseries", - symbols: Optional[List[str]] = None, - start_time: Optional[datetime] = None, - end_time: Optional[datetime] = None, - figsize: Tuple[int, int] = (12, 6), + symbols: list[str] | None = None, + start_time: datetime | None = None, + end_time: datetime | None = None, + figsize: tuple[int, int] = (12, 6), **kwargs, ) -> mpl_figure.Figure: """ diff --git a/src/factorium/factors/engine.py b/src/factorium/factors/engine.py index cde2914..84825d4 100644 --- a/src/factorium/factors/engine.py +++ b/src/factorium/factors/engine.py @@ -1,8 +1,8 @@ """Polars-based computation engine for high-performance factor operations.""" -import polars as pl -import pandas as pd import numpy as np +import pandas as pd +import polars as pl from ..constants import EPSILON @@ -171,7 +171,7 @@ def cs_winsorize( upper_bound = pl.col(value_col).quantile(1 - limits).over(time_col) # Clip values between bounds - winsorize_expr = pl.col(value_col).clip(min_bound=lower_bound, max_bound=upper_bound) + winsorize_expr = pl.col(value_col).clip(lower_bound=lower_bound, upper_bound=upper_bound) return df.with_columns(pl.when(has_nan).then(None).otherwise(winsorize_expr).alias(value_col)) @@ -240,17 +240,17 @@ def least_squares_batch(batch_df): try: # Solve least squares: y = [x 1] * [beta alpha]^T - A = np.vstack([x, np.ones(len(x))]).T - beta_alpha, _, _, _ = np.linalg.lstsq(A, y, rcond=None) - residuals = y - A @ beta_alpha + design_matrix = np.vstack([x, np.ones(len(x))]).T + beta_alpha, _, _, _ = np.linalg.lstsq(design_matrix, y, rcond=None) + residuals = y - design_matrix @ beta_alpha batch_pd["residual"] = residuals except Exception: batch_pd["residual"] = np.nan return batch_pd - # Apply least-squares per time period - result = df.map_batches(least_squares_batch, schema={**df.schema, "residual": pl.Float64}) + # Apply least-squares per time period using group_by + map_groups + result = df.group_by("end_time", maintain_order=True).map_groups(least_squares_batch) # Select and rename back to factor result = result.select(["start_time", "end_time", "symbol", "residual"]).rename({"residual": value_col}) diff --git a/src/factorium/factors/mixins/__init__.py b/src/factorium/factors/mixins/__init__.py index 0d1a07f..c32fd37 100644 --- a/src/factorium/factors/mixins/__init__.py +++ b/src/factorium/factors/mixins/__init__.py @@ -7,8 +7,8 @@ - CrossSectionalOpsMixin: Cross-sectional operations (rank, mean, etc.) """ +from .cs_ops import CrossSectionalOpsMixin from .math_ops import MathOpsMixin from .ts_ops import TimeSeriesOpsMixin -from .cs_ops import CrossSectionalOpsMixin __all__ = ["MathOpsMixin", "TimeSeriesOpsMixin", "CrossSectionalOpsMixin"] diff --git a/src/factorium/factors/mixins/cs_ops.py b/src/factorium/factors/mixins/cs_ops.py index 7e5d864..fe89f0f 100644 --- a/src/factorium/factors/mixins/cs_ops.py +++ b/src/factorium/factors/mixins/cs_ops.py @@ -1,12 +1,10 @@ -from typing import List, Union - try: from typing import Self except ImportError: - from typing_extensions import Self + from typing import Self -import numbers import polars as pl + from ...constants import EPSILON @@ -46,12 +44,12 @@ def cs_demean(self) -> Self: ) return self.__class__(result_lf, "cs_demean") - def cs_winsorize(self, limits: Union[float, List[float]] = 0.025) -> Self: + def cs_winsorize(self, limits: float | list[float] = 0.025) -> Self: """ Cross-sectional winsorization. Strict: Returns NaN if any input is NaN. Limits can be a single float (applied to both sides) or [lower, upper]. """ - if isinstance(limits, numbers.Real): + if isinstance(limits, (int, float)): lower_lim = upper_lim = limits else: lower_lim, upper_lim = limits diff --git a/src/factorium/factors/mixins/math_ops.py b/src/factorium/factors/mixins/math_ops.py index 441e880..9af05e5 100644 --- a/src/factorium/factors/mixins/math_ops.py +++ b/src/factorium/factors/mixins/math_ops.py @@ -1,12 +1,11 @@ -import polars as pl - from math import nan -from typing import Optional, Union + +import polars as pl try: from typing import Self except ImportError: - from typing_extensions import Self + from typing import Self from ...constants import EPSILON @@ -29,7 +28,7 @@ def inverse(self) -> Self: ) return self.__class__(result_lf, f"inverse({self.name})") - def log(self, base: Optional[float] = None) -> Self: + def log(self, base: float | None = None) -> Self: if base is None: result_lf = self._lf.with_columns( pl.when(pl.col("factor") > 0).then(pl.col("factor").log()).otherwise(None).alias("factor") @@ -62,7 +61,7 @@ def signed_log1p(self) -> Self: result_lf = self._lf.with_columns((pl.col("factor").sign() * pl.col("factor").abs().log1p()).alias("factor")) return self.__class__(result_lf, f"signed_log1p({self.name})") - def signed_pow(self, exponent: Union[Self, float]) -> Self: + def signed_pow(self, exponent: Self | float) -> Self: if isinstance(exponent, self.__class__): # Factor-factor path result_lf = self._lf.join(exponent._lf, on=["start_time", "end_time", "symbol"], suffix="_exp") @@ -78,7 +77,7 @@ def signed_pow(self, exponent: Union[Self, float]) -> Self: ) return self.__class__(result_lf, f"signed_pow({self.name},{exponent})") - def pow(self, exponent: Union[Self, float]) -> Self: + def pow(self, exponent: Self | float) -> Self: if isinstance(exponent, self.__class__): # Factor-factor path result_lf = self._lf.join(exponent._lf, on=["start_time", "end_time", "symbol"], suffix="_exp") @@ -90,19 +89,19 @@ def pow(self, exponent: Union[Self, float]) -> Self: result_lf = self._lf.with_columns(pl.col("factor").pow(pl.lit(exponent)).alias("factor")) return self.__class__(result_lf, f"pow({self.name},{exponent})") - def add(self, other: Union[Self, float]) -> Self: + def add(self, other: Self | float) -> Self: return self.__add__(other) - def sub(self, other: Union[Self, float]) -> Self: + def sub(self, other: Self | float) -> Self: return self.__sub__(other) - def mul(self, other: Union[Self, float]) -> Self: + def mul(self, other: Self | float) -> Self: return self.__mul__(other) - def div(self, other: Union[Self, float]) -> Self: + def div(self, other: Self | float) -> Self: return self.__truediv__(other) - def where(self, cond: Self, other: Union[Self, float] = nan) -> Self: + def where(self, cond: Self, other: Self | float = nan) -> Self: if not isinstance(cond, self.__class__): raise ValueError(f"Condition must be a Factor, got {type(cond)}") @@ -127,7 +126,7 @@ def where(self, cond: Self, other: Union[Self, float] = nan) -> Self: result_lf = result_lf.select(["start_time", "end_time", "symbol", "factor"]) return self.__class__(result_lf, f"where({self.name})") - def max(self, other: Union[Self, float]) -> Self: + def max(self, other: Self | float) -> Self: if isinstance(other, self.__class__): # Factor-factor path result_lf = self._lf.join(other._lf, on=["start_time", "end_time", "symbol"], suffix="_other") @@ -141,7 +140,7 @@ def max(self, other: Union[Self, float]) -> Self: result_lf = self._lf.with_columns(pl.max_horizontal(pl.col("factor"), pl.lit(other)).alias("factor")) return self.__class__(result_lf, f"max({self.name},{other})") - def min(self, other: Union[Self, float]) -> Self: + def min(self, other: Self | float) -> Self: if isinstance(other, self.__class__): # Factor-factor path result_lf = self._lf.join(other._lf, on=["start_time", "end_time", "symbol"], suffix="_other") diff --git a/src/factorium/factors/mixins/ts_ops.py b/src/factorium/factors/mixins/ts_ops.py index b388af8..02abc29 100644 --- a/src/factorium/factors/mixins/ts_ops.py +++ b/src/factorium/factors/mixins/ts_ops.py @@ -1,10 +1,9 @@ try: from typing import Self except ImportError: - from typing_extensions import Self + from typing import Self import numpy as np -import pandas as pd import polars as pl from scipy.stats import cauchy, norm, uniform @@ -496,8 +495,9 @@ def ts_corr(self, other: Self, window: int) -> Self: std_x = pl.col("factor").rolling_std(window_size=window, min_samples=window, ddof=1).over("symbol") std_y = pl.col("factor_y").rolling_std(window_size=window, min_samples=window, ddof=1).over("symbol") - corr_expr = pl.when((std_x <= EPSILON) | (std_y <= EPSILON)) - corr_expr = corr_expr.then(pl.lit(None)).otherwise(cov_xy / (std_x * std_y)) + corr_expr: pl.Expr = ( + pl.when((std_x <= EPSILON) | (std_y <= EPSILON)).then(pl.lit(None)).otherwise(cov_xy / (std_x * std_y)) + ) corr_expr = pl.when(nan_in_window > 0).then(pl.lit(None)).otherwise(corr_expr) result_lf = joined.with_columns(corr_expr.alias("factor")).drop("factor_y") diff --git a/src/factorium/factors/operators.py b/src/factorium/factors/operators.py index f7e88ef..e707824 100644 --- a/src/factorium/factors/operators.py +++ b/src/factorium/factors/operators.py @@ -5,7 +5,7 @@ enabling expression-based factor construction similar to alpha101. """ -from typing import TYPE_CHECKING, Union, List +from typing import TYPE_CHECKING, Union, cast import numpy as np @@ -178,7 +178,7 @@ def cs_demean(factor: "Factor") -> "Factor": return factor.cs_demean() -def cs_winsorize(factor: "Factor", limits: Union[float, List[float]] = 0.025) -> "Factor": +def cs_winsorize(factor: "Factor", limits: float | list[float] = 0.025) -> "Factor": """Functional version of factor.cs_winsorize(limits)""" return factor.cs_winsorize(limits) @@ -253,7 +253,7 @@ def pow(factor: "Factor", exponent: Union["Factor", float]) -> "Factor": return factor.pow(exponent) -def where(factor: "Factor", cond: "Factor", other: Union["Factor", float] = None) -> "Factor": +def where(factor: "Factor", cond: "Factor", other: Union["Factor", float, None] = None) -> "Factor": """Functional version of factor.where(cond, other)""" if other is None: other = np.nan @@ -281,20 +281,62 @@ def reverse(factor: "Factor") -> "Factor": def add(factor1: Union["Factor", float], factor2: Union["Factor", float]) -> "Factor": - """Functional version of factor1 + factor2""" - return factor1 + factor2 + """Functional version of factor1 + factor2. + + At least one argument must be a Factor. The result is always a Factor. + """ + from .core import Factor as FactorClass + + if isinstance(factor1, FactorClass): + return cast("Factor", factor1 + factor2) + elif isinstance(factor2, FactorClass): + return cast("Factor", factor2 + factor1) + else: + raise TypeError("At least one argument must be a Factor") def sub(factor1: Union["Factor", float], factor2: Union["Factor", float]) -> "Factor": - """Functional version of factor1 - factor2""" - return factor1 - factor2 + """Functional version of factor1 - factor2. + + At least one argument must be a Factor. The result is always a Factor. + """ + from .core import Factor as FactorClass + + if isinstance(factor1, FactorClass): + return cast("Factor", factor1 - factor2) + elif isinstance(factor2, FactorClass): + # float - Factor = -(Factor - float) + return cast("Factor", (factor2 - factor1).reverse()) + else: + raise TypeError("At least one argument must be a Factor") def mul(factor1: Union["Factor", float], factor2: Union["Factor", float]) -> "Factor": - """Functional version of factor1 * factor2""" - return factor1 * factor2 + """Functional version of factor1 * factor2. + + At least one argument must be a Factor. The result is always a Factor. + """ + from .core import Factor as FactorClass + + if isinstance(factor1, FactorClass): + return cast("Factor", factor1 * factor2) + elif isinstance(factor2, FactorClass): + return cast("Factor", factor2 * factor1) + else: + raise TypeError("At least one argument must be a Factor") def div(factor1: Union["Factor", float], factor2: Union["Factor", float]) -> "Factor": - """Functional version of factor1 / factor2""" - return factor1 / factor2 + """Functional version of factor1 / factor2. + + At least one argument must be a Factor. The result is always a Factor. + """ + from .core import Factor as FactorClass + + if isinstance(factor1, FactorClass): + return cast("Factor", factor1 / factor2) + elif isinstance(factor2, FactorClass): + # float / Factor = (1/Factor) * float = Factor.pow(-1) * float + return cast("Factor", factor2.pow(-1) * factor1) + else: + raise TypeError("At least one argument must be a Factor") diff --git a/src/factorium/factors/parser.py b/src/factorium/factors/parser.py index e9f3b7e..b800383 100644 --- a/src/factorium/factors/parser.py +++ b/src/factorium/factors/parser.py @@ -5,23 +5,23 @@ enabling string-based factor construction similar to alpha101. """ -from typing import Dict, Union, Any, List, TYPE_CHECKING +from typing import TYPE_CHECKING, Any, Union, cast if TYPE_CHECKING: from .core import Factor from pyparsing import ( - Word, - alphas, - alphanums, - nums, - Optional, - Group, + Combine, Forward, - infix_notation, + Group, OpAssoc, + Optional, ParseException, Suppress, - Combine, + Word, + alphanums, + alphas, + infix_notation, + nums, one_of, ) @@ -34,59 +34,62 @@ class FactorExpressionParser: """ Parser for functional-style factor expressions. - + Supports: - Function calls: ts_delta(close, 20) - Variables: close, volume (resolved from context) - Numbers: 20, 3.14 - Binary operators: +, -, *, / (with proper precedence) - Parentheses: (expression) - + Example: >>> parser = FactorExpressionParser() - >>> result = parser.parse("ts_delta(close, 20) / ts_shift(close, 20)", + >>> result = parser.parse("ts_delta(close, 20) / ts_shift(close, 20)", ... context={'close': close_factor}) """ - + def __init__(self): """Initialize the parser with grammar rules.""" # Define basic tokens identifier = Word(alphas + "_", alphanums + "_") - + # Numbers (integers and floats) integer = Combine(Optional("-") + Word(nums)) float_number = Combine( - Optional("-") + Word(nums) + "." + Word(nums) + - Optional(one_of("e E") + Optional(one_of("+ -")) + Word(nums)) + Optional("-") + + Word(nums) + + "." + + Word(nums) + + Optional(one_of("e E") + Optional(one_of("+ -")) + Word(nums)) ) number = float_number | integer - + # Function call: function_name(arg1, arg2, ...) function_call = Forward() - + # Expression (forward declaration for recursion) expression = Forward() - + # Argument list - each argument is its own Group to isolate results arg_list = Group(expression) + (Suppress(",") + Group(expression))[...] - + # Function call definition function_call <<= Group( - identifier.set_results_name("func_name") + - Suppress("(") + - Optional(arg_list).set_results_name("args") + - Suppress(")") + identifier.set_results_name("func_name") + + Suppress("(") + + Optional(arg_list).set_results_name("args") + + Suppress(")") ) - + # Variable or number - wrap in Group to isolate names atom = function_call | Group(identifier.set_results_name("variable")) | Group(number.set_results_name("number")) - + # Parenthesized expression paren_expr = Suppress("(") + expression + Suppress(")") - + # Primary factor (atom or parenthesized expression) factor = paren_expr | atom - + # Define operator precedence expression <<= infix_notation( factor, @@ -95,26 +98,26 @@ def __init__(self): (one_of("+ -"), 2, OpAssoc.LEFT, self._make_binary_op), ], ) - + self.parser = expression - + def _make_binary_op(self, tokens): """Action for infix operators - returns a dict structure""" # tokens[0] is a list: [left, op, right, op, right...] # Because we used OpAssoc.LEFT, tokens[0] contains the matched tokens - matched = tokens[0] + matched = tokens[0] res = matched[0] for i in range(1, len(matched), 2): op = matched[i] - right = matched[i+1] + right = matched[i + 1] res = {"type": "binary_op", "op": op, "left": res, "right": right} return res - - - def _evaluate(self, node: Any, context: Dict[str, "Factor"]) -> Union["Factor", float, int]: + + def _evaluate(self, node: Any, context: dict[str, "Factor"]) -> Union["Factor", float, int]: """Evaluate a parsed expression node.""" # Handle Factor objects directly from .core import Factor + if isinstance(node, Factor): return node @@ -122,22 +125,24 @@ def _evaluate(self, node: Any, context: Dict[str, "Factor"]) -> Union["Factor", if hasattr(node, "as_dict"): try: node_dict = node.as_dict() - except: + except Exception: node_dict = {} - + # If it's empty but has content, it might be a list-like ParseResults if not node_dict and hasattr(node, "__iter__") and not isinstance(node, (str, dict)): if len(node) == 1: return self._evaluate(node[0], context) - + # Check if it's a binary operation (from infix_notation) node_type = node_dict.get("type") if node_type == "binary_op": op = node_dict["op"] left = self._evaluate(node_dict["left"], context) right = self._evaluate(node_dict["right"], context) - print(f"DEBUG: binary_op {op}, left: {getattr(left, 'name', left)}, right: {getattr(right, 'name', right)}") - + print( + f"DEBUG: binary_op {op}, left: {getattr(left, 'name', left)}, right: {getattr(right, 'name', right)}" + ) + if op == "+": return operators.add(left, right) elif op == "-": @@ -148,17 +153,17 @@ def _evaluate(self, node: Any, context: Dict[str, "Factor"]) -> Union["Factor", return operators.div(left, right) else: raise ValueError(f"Unknown binary operator: {op}") - + # Check if it's a function call if "func_name" in node_dict: func_name = node_dict["func_name"] args_val = node_dict.get("args") - + if not hasattr(operators, func_name): raise ValueError(f"Unknown function: {func_name}") - + op_func = getattr(operators, func_name) - + # Evaluate arguments if args_val is None: eval_args = [] @@ -166,16 +171,16 @@ def _evaluate(self, node: Any, context: Dict[str, "Factor"]) -> Union["Factor", eval_args = [self._evaluate(arg, context) for arg in args_val] else: eval_args = [self._evaluate(args_val, context)] - - return op_func(*eval_args) - + + return cast("Factor | float | int", op_func(*eval_args)) + # Check if it's a variable if "variable" in node_dict: var_name = node_dict["variable"] if var_name not in context: raise ValueError(f"Undefined variable: {var_name}") return context[var_name] - + # Check if it's a number if "number" in node_dict: num_str = str(node_dict["number"]) @@ -193,7 +198,7 @@ def _evaluate(self, node: Any, context: Dict[str, "Factor"]) -> Union["Factor", op = node["op"] left = self._evaluate(node["left"], context) right = self._evaluate(node["right"], context) - + if op == "+": return operators.add(left, right) elif op == "-": @@ -204,22 +209,26 @@ def _evaluate(self, node: Any, context: Dict[str, "Factor"]) -> Union["Factor", return operators.div(left, right) else: raise ValueError(f"Unknown binary operator: {op}") - + if "func_name" in node: func_name = node["func_name"] args_list = node.get("args", []) if not hasattr(operators, func_name): raise ValueError(f"Unknown function: {func_name}") op_func = getattr(operators, func_name) - eval_args = [self._evaluate(arg, context) for arg in args_list] if isinstance(args_list, list) else [self._evaluate(args_list, context)] - return op_func(*eval_args) - + eval_args = ( + [self._evaluate(arg, context) for arg in args_list] + if isinstance(args_list, list) + else [self._evaluate(args_list, context)] + ) + return cast("Factor | float | int", op_func(*eval_args)) + if "variable" in node: var_name = node["variable"] if var_name not in context: raise ValueError(f"Undefined variable: {var_name}") return context[var_name] - + if "number" in node: num_str = str(node["number"]) try: @@ -232,15 +241,15 @@ def _evaluate(self, node: Any, context: Dict[str, "Factor"]) -> Union["Factor", node_list = list(node) if len(node_list) == 1: return self._evaluate(node_list[0], context) - + # If we have a list that didn't match anything above, it might be raw tokens of a function call # This shouldn't happen with our current grammar but let's be safe return self._evaluate(node_list[0], context) - + # Handle direct values if isinstance(node, (int, float, Factor)): return node - + if isinstance(node, str): if node in context: return context[node] @@ -252,21 +261,21 @@ def _evaluate(self, node: Any, context: Dict[str, "Factor"]) -> Union["Factor", if hasattr(operators, node): raise ValueError(f"Function '{node}' used without parentheses") raise ValueError(f"Undefined variable: {node}") - + # If we get here, it's an unexpected type raise ValueError(f"Unexpected node type: {type(node)}, value: {node}") - - def parse(self, expr: str, context: Dict[str, "Factor"]) -> "Factor": + + def parse(self, expr: str, context: dict[str, "Factor"]) -> "Factor": """ Parse and evaluate a factor expression. - + Args: expr: Expression string (e.g., "ts_delta(close, 20) / ts_shift(close, 20)") context: Dictionary mapping variable names to Factor objects - + Returns: Factor: The resulting factor from the expression - + Raises: ParseException: If the expression cannot be parsed ValueError: If there's an error in evaluation (undefined variable, etc.) @@ -274,12 +283,12 @@ def parse(self, expr: str, context: Dict[str, "Factor"]) -> "Factor": try: parsed = self.parser.parse_string(expr, parse_all=True) result = self._evaluate(parsed, context) - + from .core import Factor + if not isinstance(result, Factor): raise ValueError(f"Expression did not evaluate to a Factor, got {type(result)}") - + return result except ParseException as e: raise ValueError(f"Failed to parse expression '{expr}': {e}") from e - diff --git a/src/factorium/factors/plotting.py b/src/factorium/factors/plotting.py index 34f3fa4..8555634 100644 --- a/src/factorium/factors/plotting.py +++ b/src/factorium/factors/plotting.py @@ -4,12 +4,12 @@ Provides FactorPlotter class for visualizing factor data with various plot types. """ -from typing import Optional, List, Tuple, TYPE_CHECKING from datetime import datetime -import pandas as pd -import numpy as np -import matplotlib.pyplot as plt +from typing import TYPE_CHECKING + import matplotlib.figure as mpl_figure +import matplotlib.pyplot as plt +import pandas as pd if TYPE_CHECKING: from .core import Factor @@ -37,9 +37,9 @@ def __init__(self, factor: "Factor"): def _filter_data( self, - symbols: Optional[List[str]] = None, - start_time: Optional[datetime] = None, - end_time: Optional[datetime] = None, + symbols: list[str] | None = None, + start_time: datetime | None = None, + end_time: datetime | None = None, ) -> pd.DataFrame: """ Filter data by symbols and time range. @@ -71,10 +71,10 @@ def _filter_data( def plot_timeseries( self, - symbols: Optional[List[str]] = None, - start_time: Optional[datetime] = None, - end_time: Optional[datetime] = None, - figsize: Tuple[int, int] = (12, 6), + symbols: list[str] | None = None, + start_time: datetime | None = None, + end_time: datetime | None = None, + figsize: tuple[int, int] = (12, 6), **kwargs, ) -> mpl_figure.Figure: """ @@ -117,10 +117,10 @@ def plot_timeseries( def plot_heatmap( self, - symbols: Optional[List[str]] = None, - start_time: Optional[datetime] = None, - end_time: Optional[datetime] = None, - figsize: Tuple[int, int] = (14, 8), + symbols: list[str] | None = None, + start_time: datetime | None = None, + end_time: datetime | None = None, + figsize: tuple[int, int] = (14, 8), **kwargs, ) -> mpl_figure.Figure: """ @@ -186,10 +186,10 @@ def plot_heatmap( def plot_distribution( self, - symbols: Optional[List[str]] = None, - start_time: Optional[datetime] = None, - end_time: Optional[datetime] = None, - figsize: Tuple[int, int] = (12, 6), + symbols: list[str] | None = None, + start_time: datetime | None = None, + end_time: datetime | None = None, + figsize: tuple[int, int] = (12, 6), dist_type: str = "histogram", **kwargs, ) -> mpl_figure.Figure: @@ -260,10 +260,10 @@ def plot_distribution( def plot( self, plot_type: str = "timeseries", - symbols: Optional[List[str]] = None, - start_time: Optional[datetime] = None, - end_time: Optional[datetime] = None, - figsize: Tuple[int, int] = (12, 6), + symbols: list[str] | None = None, + start_time: datetime | None = None, + end_time: datetime | None = None, + figsize: tuple[int, int] = (12, 6), **kwargs, ) -> mpl_figure.Figure: """ diff --git a/src/factorium/factors/plotting_analyzer.py b/src/factorium/factors/plotting_analyzer.py index 21b1fcd..3815025 100644 --- a/src/factorium/factors/plotting_analyzer.py +++ b/src/factorium/factors/plotting_analyzer.py @@ -2,10 +2,10 @@ Plotting utilities for FactorAnalyzer. """ -from typing import Tuple -import pandas as pd -import matplotlib.pyplot as plt + import matplotlib.figure as mpl_figure +import matplotlib.pyplot as plt +import pandas as pd class FactorAnalyzerPlotter: @@ -13,7 +13,7 @@ class FactorAnalyzerPlotter: Plotting utility for FactorAnalyzer results. """ - def plot_ic_ts(self, ic_data: pd.DataFrame, figsize: Tuple[int, int] = (12, 6)) -> mpl_figure.Figure: + def plot_ic_ts(self, ic_data: pd.DataFrame, figsize: tuple[int, int] = (12, 6)) -> mpl_figure.Figure: """ Plot time series of IC. @@ -33,7 +33,7 @@ def plot_ic_ts(self, ic_data: pd.DataFrame, figsize: Tuple[int, int] = (12, 6)) plt.tight_layout() return fig - def plot_ic_hist(self, ic_data: pd.DataFrame, figsize: Tuple[int, int] = (10, 6)) -> mpl_figure.Figure: + def plot_ic_hist(self, ic_data: pd.DataFrame, figsize: tuple[int, int] = (10, 6)) -> mpl_figure.Figure: """ Plot histogram of IC. @@ -54,7 +54,7 @@ def plot_ic_hist(self, ic_data: pd.DataFrame, figsize: Tuple[int, int] = (10, 6) return fig def plot_quantile_returns( - self, quantile_stats: pd.DataFrame, figsize: Tuple[int, int] = (10, 6) + self, quantile_stats: pd.DataFrame, figsize: tuple[int, int] = (10, 6) ) -> mpl_figure.Figure: """ Plot bar chart of mean returns per quantile. @@ -79,7 +79,7 @@ def plot_quantile_returns( plt.tight_layout() return fig - def plot_cumulative_returns(self, cum_ret: pd.DataFrame, figsize: Tuple[int, int] = (12, 6)) -> mpl_figure.Figure: + def plot_cumulative_returns(self, cum_ret: pd.DataFrame, figsize: tuple[int, int] = (12, 6)) -> mpl_figure.Figure: """ Plot cumulative returns of quantiles. @@ -98,3 +98,40 @@ def plot_cumulative_returns(self, cum_ret: pd.DataFrame, figsize: Tuple[int, int ax.grid(True, alpha=0.3) plt.tight_layout() return fig + + def plot_ic_decay(self, ic_summary: dict[int, dict[str, float]]) -> mpl_figure.Figure: + """ + Plot IC decay curve across horizons. + + Args: + ic_summary: Dict mapping period -> {"mean_ic": float, "ic_ir": float, ...} + + Returns: + matplotlib Figure + """ + periods = sorted(ic_summary.keys()) + mean_ics = [ic_summary[p]["mean_ic"] for p in periods] + ic_irs = [ic_summary[p]["ic_ir"] for p in periods] + + fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) + + # Mean IC decay + ax1.bar(range(len(periods)), mean_ics, color="steelblue", alpha=0.7) + ax1.set_xticks(range(len(periods))) + ax1.set_xticklabels([str(p) for p in periods]) + ax1.set_xlabel("Horizon (periods)") + ax1.set_ylabel("Mean IC") + ax1.set_title("IC Decay by Horizon") + ax1.axhline(y=0, color="gray", linestyle="--", alpha=0.5) + + # IC IR decay + ax2.bar(range(len(periods)), ic_irs, color="darkorange", alpha=0.7) + ax2.set_xticks(range(len(periods))) + ax2.set_xticklabels([str(p) for p in periods]) + ax2.set_xlabel("Horizon (periods)") + ax2.set_ylabel("IC IR") + ax2.set_title("IC Information Ratio by Horizon") + ax2.axhline(y=0, color="gray", linestyle="--", alpha=0.5) + + fig.tight_layout() + return fig diff --git a/src/factorium/research/__init__.py b/src/factorium/research/__init__.py index 8b7ccee..8290fd3 100644 --- a/src/factorium/research/__init__.py +++ b/src/factorium/research/__init__.py @@ -1,4 +1,4 @@ -from .session import ResearchSession from .report import FactorReport +from .session import ResearchSession __all__ = ["ResearchSession", "FactorReport"] diff --git a/src/factorium/research/report.py b/src/factorium/research/report.py index 1de77d6..dd72b07 100644 --- a/src/factorium/research/report.py +++ b/src/factorium/research/report.py @@ -4,16 +4,14 @@ Combines factor analysis and backtest results into comprehensive reports. """ -from typing import Dict, Any, Optional, Union, TYPE_CHECKING +from typing import TYPE_CHECKING, Any, Union if TYPE_CHECKING: - from .session import ResearchSession from ..factors.analyzer import FactorAnalysisResult -import polars as pl -import pandas as pd + from .session import ResearchSession -from ..factors.core import Factor from ..backtest.vectorized import BacktestResult +from ..factors.core import Factor class FactorReport: @@ -73,14 +71,14 @@ def generate( def __init__( self, factor: Factor, - analysis: Union[Dict[str, Any], "FactorAnalysisResult"], + analysis: Union[dict[str, Any], "FactorAnalysisResult"], backtest: BacktestResult, ): self.factor = factor self.analysis = analysis self.backtest = backtest - def summary(self) -> Dict[str, Any]: + def summary(self) -> dict[str, Any]: """ Generate summary combining analysis and backtest metrics. @@ -98,7 +96,7 @@ def summary(self) -> Dict[str, Any]: "backtest_metrics": self.backtest.metrics, } - def to_dict(self) -> Dict[str, Any]: + def to_dict(self) -> dict[str, Any]: """Convert report to dictionary.""" if hasattr(self.analysis, "to_dict"): analysis_dict = self.analysis.to_dict() @@ -116,7 +114,7 @@ def to_dict(self) -> Dict[str, Any]: def __repr__(self) -> str: """String representation of report.""" summary = self.summary() - ic = summary["ic_summary"] + ic_summary = summary["ic_summary"] metrics = summary["backtest_metrics"] def fmt_float(val: Any, fmt: str) -> str: @@ -124,11 +122,18 @@ def fmt_float(val: Any, fmt: str) -> str: return f"{val:{fmt}}" return "N/A" + # ic_summary is now dict[int, dict[str, float]], get first period's stats + if ic_summary: + first_period = next(iter(ic_summary.keys())) + ic = ic_summary.get(first_period, {}) + else: + ic = {} + return f"""FactorReport: {summary["factor_name"]} IC Summary: Mean IC: {fmt_float(ic.get("mean_ic"), ".4f")} IC Std: {fmt_float(ic.get("ic_std"), ".4f")} - + Backtest Metrics: Total Return: {fmt_float(metrics.get("total_return"), ".2%")} Annual Return: {fmt_float(metrics.get("annual_return"), ".2%")} diff --git a/src/factorium/research/session.py b/src/factorium/research/session.py index f85f7b8..3b54bd2 100644 --- a/src/factorium/research/session.py +++ b/src/factorium/research/session.py @@ -9,15 +9,20 @@ >>> print(result.metrics) """ -from typing import Optional, Union, Dict, Any, List, Callable -import polars as pl -import pandas as pd +from collections.abc import Callable from pathlib import Path +from typing import TYPE_CHECKING + +import pandas as pd +import polars as pl from ..aggbar import AggBar +from ..backtest.vectorized import BacktestResult, VectorizedBacktester from ..factors.core import Factor from ..factors.parser import FactorExpressionParser -from ..backtest.vectorized import VectorizedBacktester, BacktestResult + +if TYPE_CHECKING: + from ..factors.analyzer import FactorAnalysisResult class ResearchSession: @@ -42,7 +47,7 @@ class ResearchSession: def __init__( self, - data: Union[AggBar, pd.DataFrame, pl.DataFrame], + data: AggBar | pd.DataFrame | pl.DataFrame, default_frequency: str = "1h", default_initial_capital: float = 10000.0, default_transaction_cost: float = 0.0003, @@ -55,20 +60,20 @@ def __init__( self.default_frequency = default_frequency self.default_initial_capital = default_initial_capital self.default_transaction_cost = default_transaction_cost - self._factors: Dict[str, Factor] = {} # Cache for created factors + self._factors: dict[str, Factor] = {} # Cache for created factors self._parser = FactorExpressionParser() @property - def symbols(self) -> List[str]: + def symbols(self) -> list[str]: """Return list of symbols in the data.""" return self.data.symbols @property - def cols(self) -> List[str]: + def cols(self) -> list[str]: """Return list of columns in the data.""" return self.data.cols - def create_factor(self, expr: Union[str, Callable[[AggBar], Factor]], name: Optional[str] = None) -> Factor: + def create_factor(self, expr: str | Callable[[AggBar], Factor], name: str | None = None) -> Factor: """ Create and cache a factor from expression or callable. @@ -84,8 +89,8 @@ def create_factor(self, expr: Union[str, Callable[[AggBar], Factor]], name: Opti >>> session.create_factor("ts_mean(close, 20)", "ma20") >>> session.create_factor(lambda agg: agg["close"].ts_return(20), "ret_20d") """ - # Generate cache key - cache_key = name or (expr if isinstance(expr, str) else id(expr)) + # Generate cache key (always use str) + cache_key: str = name if name else (expr if isinstance(expr, str) else str(id(expr))) # Return cached if exists if cache_key in self._factors: @@ -100,10 +105,13 @@ def create_factor(self, expr: Union[str, Callable[[AggBar], Factor]], name: Opti raise TypeError(f"Expected Factor for column {expr}, got {type(res)}") factor = res else: - # Build context for parser - context = { - col: self.data[col] for col in self.data.cols if col not in ["start_time", "end_time", "symbol"] - } + # Build context for parser (only include Factor types) + context: dict[str, Factor] = {} + for col in self.data.cols: + if col not in ["start_time", "end_time", "symbol"]: + item = self.data[col] + if isinstance(item, Factor): + context[col] = item factor = self._parser.parse(expr, context) if name: @@ -121,26 +129,26 @@ def create_factor(self, expr: Union[str, Callable[[AggBar], Factor]], name: Opti return factor @classmethod - def from_csv(cls, path: Union[str, Path], **kwargs) -> "ResearchSession": + def from_csv(cls, path: str | Path, **kwargs) -> "ResearchSession": """Create ResearchSession from CSV file.""" aggbar = AggBar.from_csv(Path(path)) return cls(aggbar, **kwargs) @classmethod - def from_parquet(cls, path: Union[str, Path], **kwargs) -> "ResearchSession": + def from_parquet(cls, path: str | Path, **kwargs) -> "ResearchSession": """Create ResearchSession from Parquet file.""" df = pl.read_parquet(path) aggbar = AggBar.from_df(df) return cls(aggbar, **kwargs) @classmethod - def from_df(cls, df: Union[pd.DataFrame, pl.DataFrame], **kwargs) -> "ResearchSession": + def from_df(cls, df: pd.DataFrame | pl.DataFrame, **kwargs) -> "ResearchSession": """Create ResearchSession from DataFrame.""" aggbar = AggBar.from_df(df) return cls(aggbar, **kwargs) @classmethod - def load(cls, path: Union[str, Path], **kwargs) -> "ResearchSession": + def load(cls, path: str | Path, **kwargs) -> "ResearchSession": """ Auto-detect format and load data. @@ -209,7 +217,8 @@ def quick_report( backtest = self.backtest(factor) # Format report - ic_summary = analysis.ic_summary + # ic_summary is now always dict[int, dict[str, float]] + ic_stats = analysis.ic_summary.get(periods, {}) metrics = backtest.metrics report = f""" @@ -217,9 +226,9 @@ def quick_report( {"=" * 60} IC Analysis (periods={periods}): - Mean IC: {ic_summary.get("mean_ic", 0):.4f} - IC Std: {ic_summary.get("ic_std", 0):.4f} - IC IR: {ic_summary.get("ic_ir", 0):.4f} + Mean IC: {ic_stats.get("mean_ic", 0):.4f} + IC Std: {ic_stats.get("ic_std", 0):.4f} + IC IR: {ic_stats.get("ic_ir", 0):.4f} Backtest Performance: Total Return: {metrics.get("total_return", 0):.2%} @@ -260,9 +269,9 @@ def backtest( signal: Factor, neutralization: str = "market", entry_price: str = "close", - frequency: Optional[str] = None, - initial_capital: Optional[float] = None, - transaction_cost: Optional[float] = None, + frequency: str | None = None, + initial_capital: float | None = None, + transaction_cost: float | None = None, ) -> BacktestResult: """ Run backtest with given signal. @@ -301,9 +310,9 @@ def backtest( def slice( self, - start: Optional[Union[int, str]] = None, - end: Optional[Union[int, str]] = None, - symbols: Optional[List[str]] = None, + start: int | str | None = None, + end: int | str | None = None, + symbols: list[str] | None = None, ) -> "ResearchSession": """ Create new session with subset of data. diff --git a/src/factorium/storage/__init__.py b/src/factorium/storage/__init__.py index f2bb7fa..bca6a5e 100644 --- a/src/factorium/storage/__init__.py +++ b/src/factorium/storage/__init__.py @@ -1,9 +1,18 @@ # src/factorium/storage/__init__.py """Storage backend abstraction layer.""" +from typing import TYPE_CHECKING + from .base import StorageBackend from .local import LocalStorageBackend +# Optional S3 backend +S3StorageBackend: type | None = None +try: + from .s3 import S3StorageBackend # type: ignore[no-redef] +except ImportError: + pass + def get_storage_backend(backend: str = "local", path: str = "./Data") -> StorageBackend: """Factory function to create storage backend instances. @@ -27,7 +36,6 @@ def get_storage_backend(backend: str = "local", path: str = "./Data") -> Storage if backend == "local": return LocalStorageBackend(path) elif backend == "s3": - # S3 backend will be implemented in Task 4 try: from .s3 import S3StorageBackend except ImportError: @@ -41,4 +49,4 @@ def get_storage_backend(backend: str = "local", path: str = "./Data") -> Storage raise ValueError(f"Unknown backend: {backend}. Supported: 'local', 's3'") -__all__ = ["StorageBackend", "LocalStorageBackend", "get_storage_backend"] +__all__ = ["StorageBackend", "LocalStorageBackend", "S3StorageBackend", "get_storage_backend"] diff --git a/src/factorium/storage/base.py b/src/factorium/storage/base.py index 3ab0d95..3be082a 100644 --- a/src/factorium/storage/base.py +++ b/src/factorium/storage/base.py @@ -2,7 +2,6 @@ """Storage backend abstraction layer.""" from abc import ABC, abstractmethod -from typing import List import polars as pl @@ -35,7 +34,7 @@ def exists(self, path: str) -> bool: ... @abstractmethod - def glob(self, pattern: str) -> List[str]: + def glob(self, pattern: str) -> list[str]: """List files matching a glob pattern.""" ... diff --git a/src/factorium/storage/local.py b/src/factorium/storage/local.py index 51de597..b081c1d 100644 --- a/src/factorium/storage/local.py +++ b/src/factorium/storage/local.py @@ -2,7 +2,6 @@ """Local filesystem storage backend.""" from pathlib import Path -from typing import List import polars as pl @@ -17,11 +16,29 @@ def __init__(self, base_path: str): self.base_path.mkdir(parents=True, exist_ok=True) def _resolve_path(self, path: str) -> Path: - return self.base_path / path + """Resolve relative path to absolute path within base_path. + + Raises: + ValueError: If path is absolute or attempts directory traversal + """ + # Prevent absolute path override + if Path(path).is_absolute(): + raise ValueError(f"Path must be relative, got: {path}") + + resolved = (self.base_path / path).resolve() + base_resolved = self.base_path.resolve() + + # Ensure the resolved path is within the base path (cross-platform) + try: + resolved.relative_to(base_resolved) + except ValueError: + raise ValueError(f"Path traversal detected: {path}") + + return resolved def full_path(self, path: str) -> str: """Get absolute path string for DuckDB queries.""" - return str(self._resolve_path(path)) + return str(self._resolve_path(path).resolve()) def read_parquet(self, path: str) -> pl.DataFrame: return pl.read_parquet(self._resolve_path(path)) @@ -34,12 +51,12 @@ def write_parquet(self, df: pl.DataFrame, path: str) -> None: def exists(self, path: str) -> bool: return self._resolve_path(path).exists() - def glob(self, pattern: str) -> List[str]: + def glob(self, pattern: str) -> list[str]: matches = list(self.base_path.glob(pattern)) return [str(m.relative_to(self.base_path)) for m in matches] def delete(self, path: str) -> None: - self._resolve_path(path).unlink() + self._resolve_path(path).unlink(missing_ok=True) def makedirs(self, path: str) -> None: self._resolve_path(path).mkdir(parents=True, exist_ok=True) diff --git a/src/factorium/storage/s3.py b/src/factorium/storage/s3.py new file mode 100644 index 0000000..c407920 --- /dev/null +++ b/src/factorium/storage/s3.py @@ -0,0 +1,157 @@ +# src/factorium/storage/s3.py +"""S3 storage backend using DuckDB for Parquet operations.""" + +import io +import os + +import polars as pl + +from .base import StorageBackend + +# Check for boto3 at import time +try: + import boto3 + from botocore.exceptions import ClientError +except ImportError: + raise ImportError("S3 backend requires boto3. Install with: pip install factorium[s3]") + + +class S3StorageBackend(StorageBackend): + """Storage backend for Amazon S3. + + Supports custom S3-compatible endpoints (MinIO, LocalStack) via AWS_ENDPOINT_URL. + """ + + def __init__(self, bucket: str, prefix: str = ""): + self.bucket = bucket + self.prefix = prefix.strip("/") + + # Support custom endpoint for MinIO/LocalStack + self._endpoint_url = os.environ.get("AWS_ENDPOINT_URL") + if self._endpoint_url: + self._s3_client = boto3.client("s3", endpoint_url=self._endpoint_url) + else: + self._s3_client = boto3.client("s3") + + def _build_key(self, path: str) -> str: + """Build full S3 key from relative path.""" + if not path: + return self.prefix + if self.prefix: + return f"{self.prefix}/{path}" + return path + + def full_path(self, path: str) -> str: + """Get full S3 URI for DuckDB queries.""" + key = self._build_key(path) + return f"s3://{self.bucket}/{key}" + + def configure_duckdb_s3(self, con) -> None: + """Configure DuckDB connection for S3 access. + + DuckDB requires explicit S3 configuration, it doesn't read AWS_ENDPOINT_URL. + + This method should be called on any DuckDB connection that will access S3 URIs + before executing queries, especially when using custom endpoints like MinIO. + + Args: + con: A DuckDB connection object + + Example: + >>> storage = S3StorageBackend("my-bucket") + >>> con = duckdb.connect(":memory:") + >>> storage.configure_duckdb_s3(con) + >>> df = con.execute("SELECT * FROM read_parquet('s3://...')").pl() + """ + access_key = os.environ.get("AWS_ACCESS_KEY_ID", "") + secret_key = os.environ.get("AWS_SECRET_ACCESS_KEY", "") + region = os.environ.get("AWS_REGION", "us-east-1") + + con.execute(f"SET s3_access_key_id='{access_key}'") + con.execute(f"SET s3_secret_access_key='{secret_key}'") + con.execute(f"SET s3_region='{region}'") + + if self._endpoint_url: + # For MinIO/LocalStack: configure custom endpoint + # Remove http:// or https:// prefix for DuckDB + endpoint = self._endpoint_url.replace("http://", "").replace("https://", "") + con.execute(f"SET s3_endpoint='{endpoint}'") + con.execute("SET s3_use_ssl=false") + con.execute("SET s3_url_style='path'") + + def read_parquet(self, path: str) -> pl.DataFrame: + """Read a Parquet file from S3 using DuckDB. + + Note: + Requires AWS credentials to be configured via environment variables + (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION) or ~/.aws/credentials. + For MinIO/LocalStack, set AWS_ENDPOINT_URL. + """ + import duckdb + + uri = self.full_path(path) + con = duckdb.connect(":memory:") + try: + self.configure_duckdb_s3(con) + result = con.execute(f"SELECT * FROM read_parquet('{uri}')").pl() + finally: + con.close() + return result + + def write_parquet(self, df: pl.DataFrame, path: str) -> None: + """Write a Polars DataFrame to S3.""" + key = self._build_key(path) + + buffer = io.BytesIO() + df.write_parquet(buffer) + buffer.seek(0) + + self._s3_client.put_object(Bucket=self.bucket, Key=key, Body=buffer.getvalue()) + + def exists(self, path: str) -> bool: + """Check if object exists in S3.""" + key = self._build_key(path) + try: + self._s3_client.head_object(Bucket=self.bucket, Key=key) + return True + except ClientError as e: + if e.response["Error"]["Code"] == "404": + return False + raise + + def glob(self, pattern: str) -> list[str]: + """List objects matching pattern in S3.""" + import fnmatch + + prefix_parts = [] + for part in pattern.split("/"): + if "*" in part or "?" in part: + break + prefix_parts.append(part) + + list_prefix = self._build_key("/".join(prefix_parts)) if prefix_parts else self.prefix + + paginator = self._s3_client.get_paginator("list_objects_v2") + matches = [] + + for page in paginator.paginate(Bucket=self.bucket, Prefix=list_prefix): + for obj in page.get("Contents", []): + key = obj["Key"] + if self.prefix: + rel_path = key[len(self.prefix) + 1 :] if key.startswith(self.prefix + "/") else key + else: + rel_path = key + + if fnmatch.fnmatch(rel_path, pattern): + matches.append(rel_path) + + return matches + + def delete(self, path: str) -> None: + """Delete an object from S3.""" + key = self._build_key(path) + self._s3_client.delete_object(Bucket=self.bucket, Key=key) + + def makedirs(self, path: str) -> None: + """No-op for S3 (directories are virtual).""" + pass diff --git a/tests/backtest/test_backtester.py b/tests/backtest/test_backtester.py index 2b28773..ccd450c 100644 --- a/tests/backtest/test_backtester.py +++ b/tests/backtest/test_backtester.py @@ -140,7 +140,7 @@ class TestBacktester: @pytest.fixture def sample_data(self): dates = pd.date_range(start="2025-01-01", periods=20, freq="1h") - timestamps = dates.astype(np.int64) // 10**6 + timestamps = dates.astype("datetime64[ms]").astype(np.int64) rows = [] for i, ts in enumerate(timestamps): @@ -246,7 +246,7 @@ class TestEdgeCases: def test_single_symbol_backtest(self): """Single asset should work without cross-sectional operations failing.""" dates = pd.date_range(start="2025-01-01", periods=20, freq="1h") - timestamps = dates.astype(np.int64) // 10**6 + timestamps = dates.astype("datetime64[ms]").astype(np.int64) rows = [] for i, ts in enumerate(timestamps): @@ -306,7 +306,7 @@ class TestVectorizedBacktesterIntegration: def sample_data(self): np.random.seed(42) dates = pd.date_range(start="2025-01-01", periods=20, freq="1h") - timestamps = dates.astype(np.int64) // 10**6 + timestamps = dates.astype("datetime64[ms]").astype(np.int64) rows = [] for i, ts in enumerate(timestamps): @@ -398,7 +398,7 @@ def test_cash_never_negative(self): # BTC starts cheap, becomes very expensive # ETH stays cheap dates = pd.date_range(start="2025-01-01", periods=10, freq="1h") - timestamps = dates.astype(np.int64) // 10**6 + timestamps = dates.astype("datetime64[ms]").astype(np.int64) rows = [] for i, ts in enumerate(timestamps): for symbol in ["BTC", "ETH"]: @@ -446,7 +446,7 @@ class TestMissingPriceHandling: def test_missing_price_symbol_excluded_from_holdings(self): """Symbols with missing prices should be excluded from target holdings.""" dates = pd.date_range(start="2025-01-01", periods=10, freq="1h") - timestamps = dates.astype(np.int64) // 10**6 + timestamps = dates.astype("datetime64[ms]").astype(np.int64) rows = [] for i, ts in enumerate(timestamps): @@ -504,7 +504,7 @@ class TestLegacyBacktester: @pytest.fixture def sample_data(self): dates = pd.date_range(start="2025-01-01", periods=20, freq="1h") - timestamps = dates.astype(np.int64) // 10**6 + timestamps = dates.astype("datetime64[ms]").astype(np.int64) rows = [] for i, ts in enumerate(timestamps): diff --git a/tests/conftest.py b/tests/conftest.py index 679cd9a..247dfd3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,7 +19,7 @@ def sample_aggbar(): Includes positive, negative, and zero values to test various math edge cases. """ dates = pd.date_range(start="2025-01-01", periods=10, freq="1min") - timestamps = dates.astype(np.int64) // 10**6 + timestamps = dates.astype("datetime64[ms]").astype(np.int64) common_cols = { "start_time": timestamps, diff --git a/tests/data/test_loader_fast.py b/tests/data/test_loader_fast.py index 3c12a32..65a9fb2 100644 --- a/tests/data/test_loader_fast.py +++ b/tests/data/test_loader_fast.py @@ -58,7 +58,7 @@ class TestLoadAggbarTimeBars: def test_returns_aggbar(self, sample_hive_data): """Test that load_aggbar returns AggBar instance.""" - loader = BinanceDataLoader(base_path=sample_hive_data) + loader = BinanceDataLoader(path=sample_hive_data) with patch.object(loader, "_check_all_symbols_exist", return_value=True): result = loader.load_aggbar( @@ -78,7 +78,7 @@ def test_returns_aggbar(self, sample_hive_data): def test_loads_multiple_symbols(self, sample_hive_data): """Test loading multiple symbols.""" - loader = BinanceDataLoader(base_path=sample_hive_data) + loader = BinanceDataLoader(path=sample_hive_data) with patch.object(loader, "_check_all_symbols_exist", return_value=True): result = loader.load_aggbar( @@ -98,7 +98,7 @@ def test_loads_multiple_symbols(self, sample_hive_data): def test_uses_cache_when_enabled(self, sample_hive_data, tmp_path): """Test that cache is used when enabled.""" - loader = BinanceDataLoader(base_path=sample_hive_data) + loader = BinanceDataLoader(path=sample_hive_data) with patch("factorium.data.loader.BarCache") as MockCache: mock_cache_instance = MagicMock() @@ -123,7 +123,7 @@ def test_uses_cache_when_enabled(self, sample_hive_data, tmp_path): def test_cache_hit_skips_aggregation(self, sample_hive_data): """Test that cache hit skips DuckDB aggregation.""" - loader = BinanceDataLoader(base_path=sample_hive_data) + loader = BinanceDataLoader(path=sample_hive_data) cached_df = pl.DataFrame( { @@ -167,7 +167,7 @@ def test_cache_hit_skips_aggregation(self, sample_hive_data): def test_different_intervals(self, sample_hive_data): """Test that different intervals produce different bar counts.""" - loader = BinanceDataLoader(base_path=sample_hive_data) + loader = BinanceDataLoader(path=sample_hive_data) with patch.object(loader, "_check_all_symbols_exist", return_value=True): result_1m = loader.load_aggbar( @@ -198,7 +198,7 @@ def test_different_intervals(self, sample_hive_data): def test_raises_on_no_data(self, sample_hive_data): """Test that ValueError is raised when no data is found.""" - loader = BinanceDataLoader(base_path=sample_hive_data) + loader = BinanceDataLoader(path=sample_hive_data) with patch.object(loader, "_check_all_symbols_exist", return_value=True): with pytest.raises(ValueError, match="No data found"): @@ -217,7 +217,7 @@ def test_raises_on_no_data(self, sample_hive_data): class TestIncrementalDownload: def test_find_missing_files_all_exist(self, sample_hive_data): - loader = BinanceDataLoader(base_path=sample_hive_data) + loader = BinanceDataLoader(path=sample_hive_data) missing = loader._find_missing_files( symbols=["BTCUSDT", "ETHUSDT"], data_type="aggTrades", @@ -229,7 +229,7 @@ def test_find_missing_files_all_exist(self, sample_hive_data): assert missing == {} def test_find_missing_files_partial_missing(self, sample_hive_data): - loader = BinanceDataLoader(base_path=sample_hive_data) + loader = BinanceDataLoader(path=sample_hive_data) missing = loader._find_missing_files( symbols=["BTCUSDT"], data_type="aggTrades", @@ -242,7 +242,7 @@ def test_find_missing_files_partial_missing(self, sample_hive_data): assert len(missing["BTCUSDT"]) == 6 def test_find_missing_files_new_symbol(self, sample_hive_data): - loader = BinanceDataLoader(base_path=sample_hive_data) + loader = BinanceDataLoader(path=sample_hive_data) missing = loader._find_missing_files( symbols=["BTCUSDT", "NEWCOIN"], data_type="aggTrades", @@ -256,7 +256,7 @@ def test_find_missing_files_new_symbol(self, sample_hive_data): assert "BTCUSDT" not in missing def test_group_consecutive_dates(self, sample_hive_data): - loader = BinanceDataLoader(base_path=sample_hive_data) + loader = BinanceDataLoader(path=sample_hive_data) dates = [datetime(2024, 1, 1), datetime(2024, 1, 2), datetime(2024, 1, 5), datetime(2024, 1, 6)] ranges = loader._group_consecutive_dates(dates) @@ -266,11 +266,11 @@ def test_group_consecutive_dates(self, sample_hive_data): assert ranges[1] == (datetime(2024, 1, 5), datetime(2024, 1, 6)) def test_group_consecutive_dates_empty(self, sample_hive_data): - loader = BinanceDataLoader(base_path=sample_hive_data) + loader = BinanceDataLoader(path=sample_hive_data) assert loader._group_consecutive_dates([]) == [] def test_download_missing_only_called(self, sample_hive_data): - loader = BinanceDataLoader(base_path=sample_hive_data) + loader = BinanceDataLoader(path=sample_hive_data) with patch.object(loader, "_find_missing_files") as mock_find: mock_find.return_value = {"NEWCOIN": [datetime(2024, 1, 5)]} diff --git a/tests/data/test_loader_klines.py b/tests/data/test_loader_klines.py index a2193ad..21ad9af 100644 --- a/tests/data/test_loader_klines.py +++ b/tests/data/test_loader_klines.py @@ -114,7 +114,7 @@ class TestLoadKlines: def test_returns_aggbar_for_klines(self, sample_klines_data): """Test that load_aggbar returns AggBar instance for klines.""" - loader = BinanceDataLoader(base_path=sample_klines_data) + loader = BinanceDataLoader(path=sample_klines_data) with patch.object(loader, "_check_all_symbols_exist", return_value=True): result = loader.load_aggbar( @@ -132,7 +132,7 @@ def test_returns_aggbar_for_klines(self, sample_klines_data): def test_klines_has_all_columns(self, sample_klines_data): """Test that klines data has all expected columns.""" - loader = BinanceDataLoader(base_path=sample_klines_data) + loader = BinanceDataLoader(path=sample_klines_data) with patch.object(loader, "_check_all_symbols_exist", return_value=True): result = loader.load_aggbar( @@ -164,7 +164,7 @@ def test_klines_has_all_columns(self, sample_klines_data): def test_klines_bypasses_aggregation(self, sample_klines_data): """Test that klines loading bypasses BarAggregator.""" - loader = BinanceDataLoader(base_path=sample_klines_data) + loader = BinanceDataLoader(path=sample_klines_data) with patch("factorium.data.loader.BarAggregator") as MockAggregator: mock_agg_instance = MockAggregator.return_value @@ -192,7 +192,7 @@ def test_klines_bypasses_aggregation(self, sample_klines_data): def test_klines_loads_multiple_symbols(self, sample_klines_data): """Test loading klines for multiple symbols.""" - loader = BinanceDataLoader(base_path=sample_klines_data) + loader = BinanceDataLoader(path=sample_klines_data) with patch.object(loader, "_check_all_symbols_exist", return_value=True): result = loader.load_aggbar( @@ -210,7 +210,7 @@ def test_klines_loads_multiple_symbols(self, sample_klines_data): def test_klines_resample_to_5m(self, sample_klines_data): """Test resampling 1m klines to 5m.""" - loader = BinanceDataLoader(base_path=sample_klines_data) + loader = BinanceDataLoader(path=sample_klines_data) with patch.object(loader, "_check_all_symbols_exist", return_value=True): result_1m = loader.load_aggbar( @@ -240,7 +240,7 @@ def test_klines_resample_to_5m(self, sample_klines_data): def test_klines_resample_to_1h(self, sample_klines_data): """Test resampling 1m klines to 1h.""" - loader = BinanceDataLoader(base_path=sample_klines_data) + loader = BinanceDataLoader(path=sample_klines_data) with patch.object(loader, "_check_all_symbols_exist", return_value=True): result_1m = loader.load_aggbar( @@ -270,7 +270,7 @@ def test_klines_resample_to_1h(self, sample_klines_data): def test_klines_raises_on_non_time_bar_type(self, sample_klines_data): """Test that klines raises error for non-time bar types.""" - loader = BinanceDataLoader(base_path=sample_klines_data) + loader = BinanceDataLoader(path=sample_klines_data) with patch.object(loader, "_check_all_symbols_exist", return_value=True): with pytest.raises(ValueError, match="only supports bar_type='time'"): @@ -289,7 +289,7 @@ def test_klines_raises_on_non_time_bar_type(self, sample_klines_data): def test_load_aggbar_klines_auto_detects_timestamp_unit(self, sample_klines_data_microseconds): """Verify klines loading auto-detects and handles microsecond timestamps.""" tmpdir = sample_klines_data_microseconds - loader = BinanceDataLoader(base_path=tmpdir) + loader = BinanceDataLoader(path=tmpdir) with patch.object(loader, "_check_all_symbols_exist", return_value=True): result = loader.load_aggbar( @@ -310,7 +310,7 @@ def test_load_aggbar_klines_microseconds_with_resample(self, sample_klines_data_ before resampling, which assumes millisecond timestamps. """ tmpdir = sample_klines_data_microseconds - loader = BinanceDataLoader(base_path=tmpdir) + loader = BinanceDataLoader(path=tmpdir) with patch.object(loader, "_check_all_symbols_exist", return_value=True): # Load 1m data @@ -346,7 +346,7 @@ def test_load_aggbar_klines_microseconds_with_resample(self, sample_klines_data_ def test_load_aggbar_klines_timestamps_normalized_to_ms(self, sample_klines_data_microseconds): """Verify that output timestamps are normalized to milliseconds.""" tmpdir = sample_klines_data_microseconds - loader = BinanceDataLoader(base_path=tmpdir) + loader = BinanceDataLoader(path=tmpdir) with patch.object(loader, "_check_all_symbols_exist", return_value=True): result = loader.load_aggbar( diff --git a/tests/factors/test_analyzer.py b/tests/factors/test_analyzer.py index 37c9160..fba2582 100644 --- a/tests/factors/test_analyzer.py +++ b/tests/factors/test_analyzer.py @@ -198,8 +198,11 @@ def test_analyze_returns_dataclass(sample_data): assert isinstance(result, FactorAnalysisResult) assert result.factor_name == "my_factor" - assert result.periods == 1 - assert "mean_ic" in result.ic_summary + # periods is always a list now + assert result.periods == [1] + # ic_summary is always dict[int, dict[str, float]] now + assert 1 in result.ic_summary + assert "mean_ic" in result.ic_summary[1] assert hasattr(result, "to_dict") diff --git a/tests/factors/test_analyzer_polars.py b/tests/factors/test_analyzer_polars.py index c038c3e..02af699 100644 --- a/tests/factors/test_analyzer_polars.py +++ b/tests/factors/test_analyzer_polars.py @@ -72,3 +72,21 @@ def test_calculate_quantile_returns_uses_polars_internally(sample_data): assert isinstance(q_ret, pd.DataFrame) # Should not use pandas groupby mock_groupby.assert_not_called() + + +def test_prepare_data_validates_price_col(sample_data): + """Test that prepare_data raises clear error when price_col doesn't exist in AggBar.""" + agg = AggBar(sample_data) + factor = agg["my_factor"] + + # Create analyzer with AggBar as prices + analyzer = FactorAnalyzer(factor, agg) + + # Try to use non-existent column + with pytest.raises(ValueError, match="Price column 'nonexistent' not found"): + analyzer.prepare_data(periods=[1], price_col="nonexistent") + + # Should work with existing column + df = analyzer.prepare_data(periods=[1], price_col="close") + assert isinstance(df, pl.DataFrame) + assert len(df) > 0 diff --git a/tests/factors/test_base_polars.py b/tests/factors/test_base_polars.py index 54b5e3f..213250b 100644 --- a/tests/factors/test_base_polars.py +++ b/tests/factors/test_base_polars.py @@ -20,7 +20,7 @@ def sample_pandas_df(): """Create sample factor data as pandas DataFrame.""" np.random.seed(42) dates = pd.date_range("2024-01-01", periods=20, freq="1min") - timestamps = dates.astype(np.int64) // 10**6 + timestamps = dates.astype("datetime64[ms]").astype(np.int64) data = [] for symbol in ["BTCUSDT", "ETHUSDT"]: @@ -537,8 +537,8 @@ def test_len_avoids_full_collection(self): n_rows = 100_000 df = pd.DataFrame( { - "start_time": pd.date_range("2020-01-01", periods=n_rows, freq="1min").astype(np.int64) // 10**6, - "end_time": (pd.date_range("2020-01-01", periods=n_rows, freq="1min").astype(np.int64) // 10**6) + "start_time": pd.date_range("2020-01-01", periods=n_rows, freq="1min").astype("datetime64[ms]").astype(np.int64), + "end_time": (pd.date_range("2020-01-01", periods=n_rows, freq="1min").astype("datetime64[ms]").astype(np.int64)) + 60000, "symbol": ["A"] * n_rows, "factor": np.arange(n_rows, dtype=float), diff --git a/tests/factors/test_engine.py b/tests/factors/test_engine.py index 1050193..314b77b 100644 --- a/tests/factors/test_engine.py +++ b/tests/factors/test_engine.py @@ -15,7 +15,7 @@ def sample_factor_df(): n_rows = 100 dates = pd.date_range("2024-01-01", periods=50, freq="1min") - timestamps = dates.astype(np.int64) // 10**6 + timestamps = dates.astype("datetime64[ms]").astype(np.int64) return pl.DataFrame( { diff --git a/tests/factors/test_factor_eval.py b/tests/factors/test_factor_eval.py new file mode 100644 index 0000000..83aa6cc --- /dev/null +++ b/tests/factors/test_factor_eval.py @@ -0,0 +1,67 @@ +import pytest +import tempfile +from pathlib import Path +from factorium import AggBar +from factorium.factors import Factor +from factorium.factors.analyzer import FactorAnalysisResult + + +@pytest.fixture +def sample_factor_and_prices(): + """Fixture providing sample factor and price data for testing.""" + import pandas as pd + import numpy as np + + dates = pd.date_range("2023-01-01", periods=20, freq="D") + symbols = ["AAPL", "GOOGL", "MSFT"] + + data = [] + for date in dates: + for symbol in symbols: + data.append( + { + "start_time": int(date.timestamp() * 1000), + "end_time": int((date + pd.Timedelta(days=1)).timestamp() * 1000), + "symbol": symbol, + "close": np.random.randn() * 10 + 100, # Random prices around 100 + "my_factor": np.random.randn(), # Random factor values + } + ) + + df = pd.DataFrame(data) + agg = AggBar(df) + factor = agg["my_factor"] + prices = agg["close"] + + return factor, prices + + +def test_factor_eval_returns_analysis_result(sample_factor_and_prices): + """Test that Factor.eval() returns FactorAnalysisResult.""" + factor, prices = sample_factor_and_prices + + result = factor.eval(prices, periods=1, quantiles=5) + + assert isinstance(result, FactorAnalysisResult) + assert result.factor_name == factor.name + # periods is now always list[int] + assert result.periods == [1] + assert result.quantiles == 5 + assert hasattr(result, "turnover_series") + assert hasattr(result, "turnover_mean") + + +def test_factor_eval_with_output_dir(sample_factor_and_prices): + """Test that Factor.eval() creates output when output_dir is specified.""" + factor, prices = sample_factor_and_prices + + with tempfile.TemporaryDirectory() as tmpdir: + result = factor.eval(prices, periods=1, output_dir=tmpdir) + + # Check that experiment folder was created + exp_dirs = list(Path(tmpdir).glob("*_*")) + assert len(exp_dirs) == 1 + + # Check config.json exists + config_path = exp_dirs[0] / "config.json" + assert config_path.exists() diff --git a/tests/factors/test_math_ops_polars.py b/tests/factors/test_math_ops_polars.py index ef88814..7f4d994 100644 --- a/tests/factors/test_math_ops_polars.py +++ b/tests/factors/test_math_ops_polars.py @@ -19,7 +19,7 @@ def sample_pandas_df(): """Create sample factor data as pandas DataFrame.""" np.random.seed(42) dates = pd.date_range("2024-01-01", periods=20, freq="1min") - timestamps = dates.astype(np.int64) // 10**6 + timestamps = dates.astype("datetime64[ms]").astype(np.int64) data = [] for symbol in ["BTCUSDT", "ETHUSDT"]: @@ -59,7 +59,7 @@ def positive_factor_pandas(): """Create factor data with positive values only.""" np.random.seed(42) dates = pd.date_range("2024-01-01", periods=20, freq="1min") - timestamps = dates.astype(np.int64) // 10**6 + timestamps = dates.astype("datetime64[ms]").astype(np.int64) data = [] for symbol in ["BTCUSDT", "ETHUSDT"]: @@ -87,7 +87,7 @@ def mixed_factor_pandas(): """Create factor with mixed positive/negative values and some zeros.""" np.random.seed(42) dates = pd.date_range("2024-01-01", periods=20, freq="1min") - timestamps = dates.astype(np.int64) // 10**6 + timestamps = dates.astype("datetime64[ms]").astype(np.int64) data = [] for symbol in ["BTCUSDT", "ETHUSDT"]: diff --git a/tests/factors/test_ts_ops_polars.py b/tests/factors/test_ts_ops_polars.py index 84a85ef..1eca15a 100644 --- a/tests/factors/test_ts_ops_polars.py +++ b/tests/factors/test_ts_ops_polars.py @@ -33,7 +33,7 @@ def sample_polars_factor_data(): - Mix of regular and edge case values """ dates = pd.date_range("2025-01-01", periods=10, freq="1min") - timestamps = dates.astype(np.int64) // 10**6 + timestamps = dates.astype("datetime64[ms]").astype(np.int64) common_cols = { "start_time": timestamps, @@ -73,7 +73,7 @@ def sample_polars_factor_with_nan(sample_polars_factor_data): def sample_polars_factor_constant(): """Create factor data with constant values for std/var edge cases.""" dates = pd.date_range("2025-01-01", periods=10, freq="1min") - timestamps = dates.astype(np.int64) // 10**6 + timestamps = dates.astype("datetime64[ms]").astype(np.int64) common_cols = { "start_time": timestamps, diff --git a/tests/mixins/test_ts_ops.py b/tests/mixins/test_ts_ops.py index be032f3..588ee1e 100644 --- a/tests/mixins/test_ts_ops.py +++ b/tests/mixins/test_ts_ops.py @@ -317,8 +317,8 @@ def _factory(series: pd.Series, symbol: str = "BTCUSDT"): df = pd.DataFrame( { - "start_time": series.index.view(np.int64) // 10**6, - "end_time": (series.index.view(np.int64) // 10**6) + 60000, + "start_time": series.index.astype("datetime64[ms]").astype(np.int64), + "end_time": (series.index.astype("datetime64[ms]").astype(np.int64)) + 60000, "symbol": symbol, "factor": series.values, } diff --git a/tests/storage/conftest.py b/tests/storage/conftest.py new file mode 100644 index 0000000..444389d --- /dev/null +++ b/tests/storage/conftest.py @@ -0,0 +1,74 @@ +"""Fixtures for storage tests.""" + +import os +import pytest + +# MinIO test settings +MINIO_ENDPOINT = os.environ.get("MINIO_ENDPOINT", "localhost:9000") +MINIO_ACCESS_KEY = os.environ.get("MINIO_ACCESS_KEY", "minioadmin") +MINIO_SECRET_KEY = os.environ.get("MINIO_SECRET_KEY", "minioadmin") +MINIO_TEST_BUCKET = os.environ.get("MINIO_TEST_BUCKET", "factorium-test") + + +try: + from botocore.exceptions import EndpointConnectionError +except ImportError: + EndpointConnectionError = Exception + +def is_minio_available() -> bool: + """Check if MinIO is available for testing.""" + try: + import boto3 + from botocore.exceptions import ClientError + + s3 = boto3.client( + "s3", + endpoint_url=f"http://{MINIO_ENDPOINT}", + aws_access_key_id=MINIO_ACCESS_KEY, + aws_secret_access_key=MINIO_SECRET_KEY, + ) + s3.list_buckets() + return True + except (ImportError, EndpointConnectionError, Exception): + return False + + +# Skip marker for tests requiring MinIO +requires_minio = pytest.mark.skipif(not is_minio_available(), reason="MinIO not available") + + +@pytest.fixture(scope="session") +def minio_client(): + """Create a boto3 client connected to MinIO.""" + import boto3 + + return boto3.client( + "s3", + endpoint_url=f"http://{MINIO_ENDPOINT}", + aws_access_key_id=MINIO_ACCESS_KEY, + aws_secret_access_key=MINIO_SECRET_KEY, + ) + + +@pytest.fixture(scope="session") +def minio_test_bucket(minio_client): + """Create and return test bucket, clean up after tests.""" + bucket = MINIO_TEST_BUCKET + + # Create bucket if not exists + try: + minio_client.head_bucket(Bucket=bucket) + except: + minio_client.create_bucket(Bucket=bucket) + + yield bucket + + # Cleanup: delete all objects and bucket + try: + paginator = minio_client.get_paginator("list_objects_v2") + for page in paginator.paginate(Bucket=bucket): + for obj in page.get("Contents", []): + minio_client.delete_object(Bucket=bucket, Key=obj["Key"]) + minio_client.delete_bucket(Bucket=bucket) + except: + pass # Ignore cleanup errors diff --git a/tests/storage/test_factory.py b/tests/storage/test_factory.py index de3dc71..483ea7b 100644 --- a/tests/storage/test_factory.py +++ b/tests/storage/test_factory.py @@ -1,8 +1,14 @@ # tests/storage/test_factory.py import tempfile from pathlib import Path +from unittest.mock import patch import pytest +try: + import boto3 + has_boto3 = True +except ImportError: + has_boto3 = False from factorium.storage import get_storage_backend, LocalStorageBackend @@ -31,9 +37,11 @@ def test_unknown_backend_raises_error(self, temp_dir): with pytest.raises(ValueError, match="Unknown backend"): get_storage_backend("unknown", str(temp_dir)) - def test_s3_backend_placeholder(self): - """S3 backend should raise ImportError until implemented.""" - # This test will be updated when S3 backend is implemented - # For now, just test error handling - with pytest.raises((ValueError, ImportError)): - get_storage_backend("s3", "my-bucket/path") + @pytest.mark.skipif(not has_boto3, reason="boto3 not installed") + def test_s3_backend_creates_instance(self): + """S3 backend should create S3StorageBackend instance.""" + with patch("factorium.storage.s3.boto3"): + from factorium.storage.s3 import S3StorageBackend + + backend = get_storage_backend("s3", "my-bucket/path") + assert isinstance(backend, S3StorageBackend) diff --git a/tests/storage/test_integration.py b/tests/storage/test_integration.py new file mode 100644 index 0000000..bedf6e7 --- /dev/null +++ b/tests/storage/test_integration.py @@ -0,0 +1,94 @@ +"""Integration tests for storage backends with data loading.""" + +import tempfile +from datetime import datetime +from pathlib import Path + +import polars as pl +import pytest + +from factorium.storage import get_storage_backend, LocalStorageBackend +from factorium.data.cache import BarCache + + +class TestStorageIntegration: + """Integration tests for storage with cache.""" + + @pytest.fixture + def temp_dir(self): + with tempfile.TemporaryDirectory() as tmpdir: + yield Path(tmpdir) + + def test_cache_with_local_backend(self, temp_dir): + """BarCache should work with LocalStorageBackend.""" + backend = LocalStorageBackend(str(temp_dir)) + cache = BarCache(storage=backend, cache_prefix=".cache") + + # Create test data + df = pl.DataFrame( + { + "start_time": [1704067200000], + "symbol": ["BTCUSDT"], + "open": [42000.0], + "high": [42500.0], + "low": [41800.0], + "close": [42200.0], + "volume": [100.0], + } + ) + + # Store in cache + cache.put( + df=df, + exchange="binance", + symbols=["BTCUSDT"], + interval_ms=60000, + data_type="aggTrades", + market_type="futures_um", + date=datetime(2024, 1, 1), + ) + + # Retrieve from cache + result = cache.get( + exchange="binance", + symbols=["BTCUSDT"], + interval_ms=60000, + data_type="aggTrades", + market_type="futures_um", + date=datetime(2024, 1, 1), + ) + + assert result is not None + assert len(result) == 1 + assert result["symbol"][0] == "BTCUSDT" + + def test_factory_creates_correct_backend(self, temp_dir): + """get_storage_backend should create correct backend type.""" + local_backend = get_storage_backend("local", str(temp_dir)) + assert isinstance(local_backend, LocalStorageBackend) + + def test_local_backend_full_path(self, temp_dir): + """LocalStorageBackend.full_path should return absolute path.""" + backend = LocalStorageBackend(str(temp_dir)) + full_path = backend.full_path("test/file.parquet") + + assert str(temp_dir) in full_path + assert "test/file.parquet" in full_path + assert Path(full_path).is_absolute() + + def test_storage_round_trip(self, temp_dir): + """Data should survive write/read cycle through storage.""" + backend = LocalStorageBackend(str(temp_dir)) + + original_df = pl.DataFrame( + { + "a": [1, 2, 3], + "b": ["x", "y", "z"], + "c": [1.1, 2.2, 3.3], + } + ) + + backend.write_parquet(original_df, "test.parquet") + loaded_df = backend.read_parquet("test.parquet") + + assert original_df.equals(loaded_df) diff --git a/tests/storage/test_local.py b/tests/storage/test_local.py index 74b8133..16c74e2 100644 --- a/tests/storage/test_local.py +++ b/tests/storage/test_local.py @@ -75,5 +75,55 @@ def test_makedirs_creates_directory(self, backend, temp_dir): def test_full_path_returns_absolute_path(self, backend, temp_dir): """full_path() should return absolute path for DuckDB.""" full = backend.full_path("some/file.parquet") + assert Path(full).is_absolute(), f"Expected absolute path, got: {full}" assert str(temp_dir) in full assert "some/file.parquet" in full + + def test_full_path_returns_absolute_path_with_relative_base(self): + """full_path should return absolute path even when base_path is relative.""" + # Create backend with relative path + backend = LocalStorageBackend("./test_data") + + # Get full path + result = backend.full_path("some/file.parquet") + + # Should be absolute + assert Path(result).is_absolute(), f"Expected absolute path, got: {result}" + + # Should contain the resolved base path + expected_base = Path("./test_data").resolve() + assert str(expected_base) in result + + def test_full_path_returns_absolute_path_with_absolute_base(self): + """full_path should return absolute path when base_path is absolute.""" + # Create backend with absolute path + base_path = Path("/tmp/test_data").resolve() + backend = LocalStorageBackend(str(base_path)) + + # Get full path + result = backend.full_path("some/file.parquet") + + # Should be absolute + assert Path(result).is_absolute(), f"Expected absolute path, got: {result}" + + # Should contain the base path + assert str(base_path) in result + + def test_resolve_path_rejects_absolute_paths(self, backend): + """_resolve_path() should reject absolute paths.""" + with pytest.raises(ValueError, match="Path must be relative"): + backend._resolve_path("/etc/passwd") + + def test_resolve_path_rejects_directory_traversal(self, backend): + """_resolve_path() should reject directory traversal attempts.""" + with pytest.raises(ValueError, match="Path traversal detected"): + backend._resolve_path("../outside.txt") + + with pytest.raises(ValueError, match="Path traversal detected"): + backend._resolve_path("subdir/../../../root.txt") + + def test_delete_nonexistent_file_does_not_raise(self, backend): + """delete() should not raise when file doesn't exist.""" + # This should not raise an exception + backend.delete("nonexistent.parquet") + assert not backend.exists("nonexistent.parquet") diff --git a/tests/storage/test_s3.py b/tests/storage/test_s3.py new file mode 100644 index 0000000..d72d099 --- /dev/null +++ b/tests/storage/test_s3.py @@ -0,0 +1,119 @@ +# tests/storage/test_s3.py +"""Tests for S3StorageBackend using mocks.""" + +import io +import pytest +from unittest.mock import MagicMock, patch, PropertyMock +import polars as pl +try: + import boto3 + has_boto3 = True +except ImportError: + has_boto3 = False + + +@pytest.mark.skipif(not has_boto3, reason="boto3 not installed") +class TestS3StorageBackend: + """Tests for S3StorageBackend with mocked boto3.""" + + @pytest.fixture + def mock_boto3(self): + """Mock boto3 module.""" + with patch("factorium.storage.s3.boto3") as mock: + # Setup default mock behavior + mock.client.return_value = MagicMock() + yield mock + + @pytest.fixture + def backend(self, mock_boto3): + """Create S3StorageBackend with mocked boto3.""" + from factorium.storage.s3 import S3StorageBackend + + return S3StorageBackend(bucket="test-bucket", prefix="data") + + def test_full_path_with_prefix(self, backend): + """full_path should return correct S3 URI with prefix.""" + assert backend.full_path("cache/file.parquet") == "s3://test-bucket/data/cache/file.parquet" + + def test_full_path_without_prefix(self, mock_boto3): + """full_path should work without prefix.""" + from factorium.storage.s3 import S3StorageBackend + + backend = S3StorageBackend(bucket="test-bucket", prefix="") + assert backend.full_path("file.parquet") == "s3://test-bucket/file.parquet" + + def test_exists_returns_true_for_existing_object(self, backend, mock_boto3): + """exists() should return True when object exists.""" + mock_boto3.client.return_value.head_object.return_value = {} + assert backend.exists("existing.parquet") is True + + def test_exists_returns_false_for_missing_object(self, backend, mock_boto3): + """exists() should return False when object doesn't exist.""" + from botocore.exceptions import ClientError + + mock_boto3.client.return_value.head_object.side_effect = ClientError( + {"Error": {"Code": "404", "Message": "Not Found"}}, "HeadObject" + ) + assert backend.exists("missing.parquet") is False + + def test_delete_calls_delete_object(self, backend, mock_boto3): + """delete() should call S3 delete_object.""" + backend.delete("to_delete.parquet") + mock_boto3.client.return_value.delete_object.assert_called_once_with( + Bucket="test-bucket", Key="data/to_delete.parquet" + ) + + def test_makedirs_is_noop(self, backend): + """makedirs() should be a no-op for S3.""" + # Should not raise + backend.makedirs("some/path") + + def test_write_parquet_uploads_to_s3(self, backend, mock_boto3): + """write_parquet() should upload parquet data to S3.""" + df = pl.DataFrame({"a": [1, 2, 3]}) + backend.write_parquet(df, "test.parquet") + + # Verify put_object was called + mock_boto3.client.return_value.put_object.assert_called_once() + call_kwargs = mock_boto3.client.return_value.put_object.call_args[1] + assert call_kwargs["Bucket"] == "test-bucket" + assert call_kwargs["Key"] == "data/test.parquet" + + def test_read_parquet_uses_duckdb(self, backend, mock_boto3): + """read_parquet() should use DuckDB with correct S3 URI.""" + import sys + + mock_duckdb = MagicMock() + mock_con = MagicMock() + mock_duckdb.connect.return_value = mock_con + mock_con.execute.return_value.pl.return_value = pl.DataFrame({"a": [1]}) + + with patch.dict("sys.modules", {"duckdb": mock_duckdb}): + result = backend.read_parquet("test.parquet") + + mock_duckdb.connect.assert_called_once_with(":memory:") + # Check that at least one execute call contains the S3 URI + execute_calls = [str(call) for call in mock_con.execute.call_args_list] + assert any("s3://test-bucket/data/test.parquet" in call for call in execute_calls) + mock_con.close.assert_called_once() + + def test_glob_returns_matching_files(self, backend, mock_boto3): + """glob() should return files matching pattern.""" + # Mock paginator response + mock_paginator = MagicMock() + mock_boto3.client.return_value.get_paginator.return_value = mock_paginator + mock_paginator.paginate.return_value = [ + { + "Contents": [ + {"Key": "data/cache/file1.parquet"}, + {"Key": "data/cache/file2.parquet"}, + {"Key": "data/other/file3.parquet"}, + ] + } + ] + + matches = backend.glob("cache/*.parquet") + + assert len(matches) == 2 + assert "cache/file1.parquet" in matches + assert "cache/file2.parquet" in matches diff --git a/tests/storage/test_s3_integration.py b/tests/storage/test_s3_integration.py new file mode 100644 index 0000000..8ff3241 --- /dev/null +++ b/tests/storage/test_s3_integration.py @@ -0,0 +1,127 @@ +"""Integration tests for S3StorageBackend with real MinIO. + +These tests require MinIO to be running. They will be skipped if MinIO is not available. + +To run locally: + docker-compose -f docker-compose.minio.yml up -d + uv run pytest tests/storage/test_s3_integration.py -v + docker-compose -f docker-compose.minio.yml down +""" + +import os +import uuid + +import polars as pl +import pytest + +from .conftest import ( + requires_minio, + MINIO_ENDPOINT, + MINIO_ACCESS_KEY, + MINIO_SECRET_KEY, +) + + +@requires_minio +class TestS3StorageBackendIntegration: + """Integration tests for S3StorageBackend with real MinIO.""" + + @pytest.fixture + def s3_backend(self, minio_test_bucket, monkeypatch): + """Create S3StorageBackend connected to MinIO.""" + # Use monkeypatch to set environment variables (auto-restored after test) + monkeypatch.setenv("AWS_ACCESS_KEY_ID", MINIO_ACCESS_KEY) + monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", MINIO_SECRET_KEY) + monkeypatch.setenv("AWS_ENDPOINT_URL", f"http://{MINIO_ENDPOINT}") + monkeypatch.setenv("AWS_REGION", "us-east-1") + + from factorium.storage.s3 import S3StorageBackend + + prefix = f"test-{uuid.uuid4().hex[:8]}" + backend = S3StorageBackend(bucket=minio_test_bucket, prefix=prefix) + + yield backend + + # Cleanup: delete test prefix + # (handled by minio_test_bucket fixture at session end) + + def test_write_and_read_parquet(self, s3_backend): + """Should write and read Parquet files through MinIO.""" + df = pl.DataFrame( + { + "a": [1, 2, 3], + "b": ["x", "y", "z"], + "c": [1.1, 2.2, 3.3], + } + ) + + s3_backend.write_parquet(df, "test/data.parquet") + result = s3_backend.read_parquet("test/data.parquet") + + assert result.equals(df) + + def test_exists_returns_true_for_existing_object(self, s3_backend): + """exists() should return True for existing objects in MinIO.""" + df = pl.DataFrame({"a": [1]}) + s3_backend.write_parquet(df, "exists_test.parquet") + + assert s3_backend.exists("exists_test.parquet") is True + + def test_exists_returns_false_for_missing_object(self, s3_backend): + """exists() should return False for non-existing objects.""" + assert s3_backend.exists("nonexistent.parquet") is False + + def test_glob_finds_matching_files(self, s3_backend): + """glob() should find files matching pattern in MinIO.""" + df = pl.DataFrame({"a": [1]}) + s3_backend.write_parquet(df, "glob_test/file1.parquet") + s3_backend.write_parquet(df, "glob_test/file2.parquet") + s3_backend.write_parquet(df, "other/file3.parquet") + + matches = s3_backend.glob("glob_test/*.parquet") + + assert len(matches) == 2 + assert all("glob_test" in m for m in matches) + + def test_delete_removes_object(self, s3_backend): + """delete() should remove object from MinIO.""" + df = pl.DataFrame({"a": [1]}) + s3_backend.write_parquet(df, "delete_test.parquet") + assert s3_backend.exists("delete_test.parquet") + + s3_backend.delete("delete_test.parquet") + + assert not s3_backend.exists("delete_test.parquet") + + def test_full_path_returns_s3_uri(self, s3_backend, minio_test_bucket): + """full_path() should return proper S3 URI.""" + path = s3_backend.full_path("some/file.parquet") + + assert path.startswith("s3://") + assert minio_test_bucket in path + assert "some/file.parquet" in path + + def test_makedirs_is_noop(self, s3_backend): + """makedirs() should not raise for S3 (virtual directories).""" + # Should not raise + s3_backend.makedirs("some/deep/path") + + def test_large_dataframe_roundtrip(self, s3_backend): + """Should handle larger DataFrames correctly.""" + import numpy as np + + # Create a larger DataFrame (~1MB) + n_rows = 50000 + df = pl.DataFrame( + { + "id": list(range(n_rows)), + "value": np.random.randn(n_rows), + "category": [f"cat_{i % 100}" for i in range(n_rows)], + } + ) + + s3_backend.write_parquet(df, "large_test.parquet") + result = s3_backend.read_parquet("large_test.parquet") + + assert len(result) == n_rows + assert result.schema == df.schema diff --git a/tests/test_data_loader.py b/tests/test_data_loader.py index 00d0617..e7a28a8 100644 --- a/tests/test_data_loader.py +++ b/tests/test_data_loader.py @@ -56,7 +56,7 @@ def sample_trades_df(): @pytest.fixture def loader(): """Create a BinanceDataLoader instance.""" - return BinanceDataLoader(base_path="./test_data") + return BinanceDataLoader(path="./test_data") @pytest.fixture @@ -69,7 +69,7 @@ def temp_data_dir(): @pytest.fixture def loader_with_temp_dir(temp_data_dir): """Create a BinanceDataLoader with temporary directory.""" - return BinanceDataLoader(base_path=str(temp_data_dir)) + return BinanceDataLoader(path=str(temp_data_dir)) def create_mock_df(symbol: str, seed: int = 42) -> pd.DataFrame: @@ -223,7 +223,7 @@ class TestCheckAllFilesExist: def test_all_files_exist(self, temp_data_dir): """Test when all required files exist.""" - loader = BinanceDataLoader(base_path=str(temp_data_dir)) + loader = BinanceDataLoader(path=str(temp_data_dir)) # Create parquet files for 3 days for i in range(3): @@ -243,7 +243,7 @@ def test_all_files_exist(self, temp_data_dir): def test_some_files_missing(self, temp_data_dir): """Test when some files are missing.""" - loader = BinanceDataLoader(base_path=str(temp_data_dir)) + loader = BinanceDataLoader(path=str(temp_data_dir)) # Create files for day 1 and 3, skip day 2 create_parquet_file(temp_data_dir, "futures_um", "aggTrades", "BTCUSDT", datetime(2024, 1, 1)) @@ -262,7 +262,7 @@ def test_some_files_missing(self, temp_data_dir): def test_no_files_exist(self, temp_data_dir): """Test when no files exist.""" - loader = BinanceDataLoader(base_path=str(temp_data_dir)) + loader = BinanceDataLoader(path=str(temp_data_dir)) result = loader._check_all_files_exist( symbol="BTCUSDT", @@ -277,7 +277,7 @@ def test_no_files_exist(self, temp_data_dir): def test_checks_correct_path_structure(self, temp_data_dir): """Test that check uses correct Hive partition path.""" - loader = BinanceDataLoader(base_path=str(temp_data_dir)) + loader = BinanceDataLoader(path=str(temp_data_dir)) # Create file with correct structure expected_path = ( @@ -310,7 +310,7 @@ def test_checks_correct_path_structure(self, temp_data_dir): def test_spot_market_path(self, temp_data_dir): """Test file check for spot market (different path).""" - loader = BinanceDataLoader(base_path=str(temp_data_dir)) + loader = BinanceDataLoader(path=str(temp_data_dir)) # Create file for spot market expected_path = ( @@ -606,18 +606,18 @@ def test_default_base_path(self): def test_custom_base_path(self, temp_data_dir): """Test custom base_path is set correctly.""" - loader = BinanceDataLoader(base_path=str(temp_data_dir)) + loader = BinanceDataLoader(path=str(temp_data_dir)) assert loader.base_path == temp_data_dir def test_downloader_is_created(self, temp_data_dir): """Test that BinanceDataDownloader is created.""" - loader = BinanceDataLoader(base_path=str(temp_data_dir)) + loader = BinanceDataLoader(path=str(temp_data_dir)) assert loader.downloader is not None def test_download_settings_passed_to_downloader(self, temp_data_dir): """Test that download settings are passed to downloader.""" loader = BinanceDataLoader( - base_path=str(temp_data_dir), max_concurrent_downloads=10, retry_attempts=5, retry_delay=2 + path=str(temp_data_dir), max_concurrent_downloads=10, retry_attempts=5, retry_delay=2 ) assert loader.downloader.max_concurrent_downloads == 10 diff --git a/tests/test_evaluation.py b/tests/test_evaluation.py index 3e27b20..b502005 100644 --- a/tests/test_evaluation.py +++ b/tests/test_evaluation.py @@ -2,47 +2,63 @@ import numpy as np import pytest from factorium.factors.core import Factor +from factorium.factors.analyzer import FactorAnalysisResult + def test_factor_evaluation_flow(): # Create dummy price data (upward trend) - dates = pd.date_range('2023-01-01', periods=10, freq='D') - symbols = ['AAPL', 'MSFT', 'GOOG'] - + dates = pd.date_range("2023-01-01", periods=10, freq="D") + symbols = ["AAPL", "MSFT", "GOOG"] + price_data = [] for d in dates: for s in symbols: # Price increases over time p = 100 + (d - dates[0]).days * 2 + np.random.randn() price_data.append([d, d, s, p]) - - prices_df = pd.DataFrame(price_data, columns=['start_time', 'end_time', 'symbol', 'factor']) - prices_factor = Factor(prices_df, name='close') - - # Create dummy signal factor + + prices_df = pd.DataFrame(price_data, columns=["start_time", "end_time", "symbol", "factor"]) + prices_factor = Factor(prices_df, name="close") + + # Create dummy signal factor signal_data = [] for i, d in enumerate(dates): for s in symbols: sig = np.random.randn() signal_data.append([d, d, s, sig]) - - signal_df = pd.DataFrame(signal_data, columns=['start_time', 'end_time', 'symbol', 'factor']) - signal_factor = Factor(signal_df, name='signal') - - # Run eval method with plot + + signal_df = pd.DataFrame(signal_data, columns=["start_time", "end_time", "symbol", "factor"]) + signal_factor = Factor(signal_df, name="signal") + + # Run eval method with output_dir import os - plot_path = "test_eval_plot.png" - results = signal_factor.eval(prices_factor, periods=[1, 2], quantiles=2, save_path=plot_path) - + import tempfile + import shutil + + output_dir = tempfile.mkdtemp() + result = signal_factor.eval(prices_factor, periods=1, quantiles=2, output_dir=output_dir) + # Assertions - assert 'ic_mean' in results - assert 'ic_ir' in results - assert 'layer_returns' in results - assert 'turnover_mean' in results - assert isinstance(results['ic_mean'], pd.Series) - assert 1 in results['layer_returns'] - assert 2 in results['layer_returns'] - - # Check if plot was saved - assert os.path.exists(plot_path) - if os.path.exists(plot_path): - os.remove(plot_path) # Cleanup + assert isinstance(result, FactorAnalysisResult) + # ic_summary is now always dict[int, dict[str, float]] + assert 1 in result.ic_summary + assert "mean_ic" in result.ic_summary[1] + assert "ic_ir" in result.ic_summary[1] + assert isinstance(result.ic_series, pd.DataFrame) + assert isinstance(result.turnover_mean, float) + # quantile_returns is now dict[int, pd.DataFrame] + assert isinstance(result.quantile_returns, dict) + assert 1 in result.quantile_returns + assert len(result.quantile_returns[1].columns) == 2 # quantiles=2 + + # Check if output directory was created and has files + assert os.path.exists(output_dir) + # Should have created a timestamped subdirectory + subdirs = [d for d in os.listdir(output_dir) if os.path.isdir(os.path.join(output_dir, d))] + assert len(subdirs) == 1 + exp_dir = os.path.join(output_dir, subdirs[0]) + assert os.path.exists(os.path.join(exp_dir, "ic_series.csv")) + assert os.path.exists(os.path.join(exp_dir, "plots")) + + # Cleanup + shutil.rmtree(output_dir) diff --git a/tests/test_plotting.py b/tests/test_plotting.py index f262982..02182dc 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -20,7 +20,7 @@ def sample_aggbar_for_plotting(): """ # Generate more time points for better visualization dates = pd.date_range(start="2025-01-01", periods=50, freq="1h") - timestamps = (dates.astype(np.int64) // 10**6).astype(int) + timestamps = (dates.astype("datetime64[ms]").astype(np.int64)).astype(int) symbols = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"] data_list = []