105 |
106 | ### License
107 |
108 | This project is licensed under the GNU Affero General Public License v3.0.
109 |
110 | The complete license text can be accessed in the repository at [LICENSE](https://github.com/Zaczero/CBBI/blob/main/LICENSE).
111 |
--------------------------------------------------------------------------------
/api/cbbiinfo_api.py:
--------------------------------------------------------------------------------
1 | import pandas as pd
2 |
3 | from utils import HTTP
4 |
5 |
6 | def cbbi_fetch(key: str) -> pd.DataFrame:
7 | response = HTTP.get('https://colintalkscrypto.com/cbbi/data/latest.json')
8 | response.raise_for_status()
9 | response_data = response.json()[key]
10 |
11 | df = pd.DataFrame(
12 | response_data.items(),
13 | columns=[
14 | 'Date',
15 | 'Value',
16 | ],
17 | )
18 | df['Date'] = pd.to_datetime(df['Date'], unit='s').dt.tz_localize(None)
19 |
20 | return df
21 |
--------------------------------------------------------------------------------
/api/coinsoto_api.py:
--------------------------------------------------------------------------------
1 | import pandas as pd
2 |
3 | from utils import HTTP
4 |
5 |
6 | def cs_fetch(path: str, data_selector: str, col_name: str) -> pd.DataFrame:
7 | response = HTTP.get(f'https://coinank.com/indicatorapi/{path}')
8 | response.raise_for_status()
9 | data = response.json()['data']
10 |
11 | if 'timeList' not in data and 'line' in data:
12 | data = data['line']
13 |
14 | data_x = data['timeList']
15 | data_y = data[data_selector]
16 | assert len(data_x) == len(data_y), f'{len(data_x)=} != {len(data_y)=}'
17 |
18 | df = pd.DataFrame(
19 | {
20 | 'Date': data_x[: len(data_y)],
21 | col_name: data_y,
22 | }
23 | )
24 |
25 | df['Date'] = pd.to_datetime(df['Date'], unit='ms').dt.tz_localize(None)
26 |
27 | return df
28 |
--------------------------------------------------------------------------------
/api/glassnode_api.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import pandas as pd
4 |
5 | from utils import HTTP
6 |
7 |
8 | def gn_fetch(url_selector: str, col_name: str, **kwargs) -> pd.DataFrame:
9 | api_key = os.getenv('GLASSNODE_API_KEY')
10 |
11 | if not api_key:
12 | raise Exception('GlassNode fallback in unavailable (missing api key)')
13 |
14 | response = HTTP.get(
15 | f'https://api.glassnode.com/v1/metrics/indicators/{url_selector}',
16 | params=kwargs,
17 | headers={'X-Api-Key': api_key},
18 | )
19 | response.raise_for_status()
20 | response_json = response.json()
21 | response_x = [d['t'] for d in response_json]
22 | response_y = [d['v'] for d in response_json]
23 |
24 | df = pd.DataFrame(
25 | {
26 | 'Date': response_x,
27 | col_name: response_y,
28 | }
29 | )
30 | df['Date'] = pd.to_datetime(df['Date'], unit='s').dt.tz_localize(None)
31 |
32 | return df
33 |
--------------------------------------------------------------------------------
/asciinema/thumbnail.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zaczero/CBBI/4b275fa095b550bf5f7e9de15e6d8f0ab6fa14be/asciinema/thumbnail.webp
--------------------------------------------------------------------------------
/asciinema/thumbnail.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zaczero/CBBI/4b275fa095b550bf5f7e9de15e6d8f0ab6fa14be/asciinema/thumbnail.xcf
--------------------------------------------------------------------------------
/fetch_bitcoin_data.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import pandas as pd
3 | from filecache import filecache
4 |
5 | from utils import HTTP, mark_days_since, mark_highs_lows
6 |
7 |
8 | @filecache(7200) # 2 hours
9 | def fetch_bitcoin_data() -> pd.DataFrame:
10 | """
11 | Fetches historical Bitcoin data into a DataFrame.
12 | Very early data is discarded due to high volatility.
13 |
14 | Returns:
15 | DataFrame containing Bitcoin data.
16 | """
17 | print('📈 Requesting historical Bitcoin data…')
18 |
19 | response = HTTP.get(
20 | 'https://api.blockchair.com/bitcoin/blocks',
21 | params={
22 | 'a': 'date,count(),min(id),max(id),sum(generation),sum(generation_usd)',
23 | 's': 'date(desc)',
24 | },
25 | )
26 | response.raise_for_status()
27 | response_json = response.json()
28 |
29 | df = pd.DataFrame(response_json['data'][::-1])
30 | df.rename(
31 | columns={
32 | 'date': 'Date',
33 | 'count()': 'TotalBlocks',
34 | 'min(id)': 'MinBlockID',
35 | 'max(id)': 'MaxBlockID',
36 | 'sum(generation)': 'TotalGeneration',
37 | 'sum(generation_usd)': 'TotalGenerationUSD',
38 | },
39 | inplace=True,
40 | )
41 |
42 | df['Date'] = pd.to_datetime(df['Date'])
43 | df['TotalGeneration'] /= 1e8
44 | df['BlockGeneration'] = df['TotalGeneration'] / df['TotalBlocks']
45 | df['BlockGenerationUSD'] = df['TotalGenerationUSD'] / df['TotalBlocks']
46 |
47 | df = df.merge(fetch_price_data(), on='Date', how='left')
48 | df.loc[df['Price'].isna(), 'Price'] = df['BlockGenerationUSD'] / df['BlockGeneration']
49 | df['PriceLog'] = np.log(df['Price'])
50 | df['PriceLogInterp'] = np.interp(
51 | x=df['PriceLog'],
52 | xp=(df['PriceLog'].min(), df['PriceLog'].max()),
53 | fp=(0, 1),
54 | )
55 |
56 | df = df.loc[df['Date'] >= '2011-06-27']
57 | df.reset_index(drop=True, inplace=True)
58 |
59 | df = fix_current_day_data(df)
60 | df = add_block_halving_data(df)
61 | df = mark_highs_lows(df, 'Price', False, round(365 * 2), 180)
62 |
63 | # move 2021' peak to the first price peak
64 | df.loc[df['Date'] == '2021-11-09', 'PriceHigh'] = 0
65 | df.loc[df['Date'] == '2021-04-14', 'PriceHigh'] = 1
66 |
67 | df = mark_days_since(df, ['PriceHigh', 'PriceLow', 'Halving'])
68 | return df
69 |
70 |
71 | def fetch_price_data() -> pd.DataFrame:
72 | response = HTTP.get(
73 | 'https://api.coinmarketcap.com/data-api/v3/cryptocurrency/detail/chart',
74 | params={
75 | 'id': 1,
76 | 'range': 'ALL',
77 | },
78 | )
79 |
80 | response.raise_for_status()
81 | response_json = response.json()
82 | response_x = [float(k) for k in response_json['data']['points']]
83 | response_y = [value['v'][0] for value in response_json['data']['points'].values()]
84 |
85 | df = pd.DataFrame(
86 | {
87 | 'Date': response_x,
88 | 'Price': response_y,
89 | }
90 | )
91 | df['Date'] = pd.to_datetime(df['Date'], unit='s').dt.tz_localize(None).dt.floor('d')
92 | df.sort_values(by='Date', inplace=True)
93 | df.drop_duplicates('Date', keep='last', inplace=True)
94 |
95 | return df
96 |
97 |
98 | def fix_current_day_data(df: pd.DataFrame) -> pd.DataFrame:
99 | row = df.iloc[-1].copy()
100 |
101 | target_total_blocks = 24 * 6
102 | target_scale = target_total_blocks / row['TotalBlocks']
103 |
104 | for col_name in ['TotalBlocks', 'TotalGeneration', 'TotalGenerationUSD']:
105 | row[col_name] *= target_scale
106 |
107 | df.iloc[-1] = row
108 | return df
109 |
110 |
111 | def add_block_halving_data(df: pd.DataFrame) -> pd.DataFrame:
112 | reward_halving_every = 210000
113 | current_block_halving_id = reward_halving_every
114 | current_block_production = 50
115 | df['Halving'] = 0
116 | df['NextHalvingBlock'] = current_block_halving_id
117 |
118 | while True:
119 | df.loc[
120 | (current_block_halving_id - reward_halving_every) <= df['MaxBlockID'],
121 | 'BlockGeneration',
122 | ] = current_block_production
123 |
124 | block_halving_row = df[
125 | (df['MinBlockID'] <= current_block_halving_id) & (df['MaxBlockID'] >= current_block_halving_id)
126 | ].squeeze()
127 |
128 | if block_halving_row.shape[0] == 0:
129 | break
130 |
131 | current_block_halving_id += reward_halving_every
132 | current_block_production /= 2
133 | df.loc[block_halving_row.name, 'Halving'] = 1
134 | df.loc[df.index > block_halving_row.name, 'NextHalvingBlock'] = current_block_halving_id
135 |
136 | df['DaysToHalving'] = pd.to_timedelta((df['NextHalvingBlock'] - df['MaxBlockID']) / (24 * 6), unit='D')
137 | df['NextHalvingDate'] = df['Date'] + df['DaysToHalving']
138 | return df
139 |
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import time
3 | import traceback
4 | from pathlib import Path
5 |
6 | import fire
7 | import numpy as np
8 | import pandas as pd
9 | import seaborn as sns
10 | from matplotlib import pyplot as plt
11 | from pyfiglet import figlet_format
12 | from sty import bg, ef, fg, rs
13 | from tqdm import tqdm
14 |
15 | from fetch_bitcoin_data import fetch_bitcoin_data
16 | from metrics.base_metric import BaseMetric
17 | from metrics.mvrv_z_score import MVRVMetric
18 | from metrics.pi_cycle import PiCycleMetric
19 | from metrics.puell_multiple import PuellMetric
20 | from metrics.reserve_risk import ReserveRiskMetric
21 | from metrics.rhodl_ratio import RHODLMetric
22 | from metrics.rupl import RUPLMetric
23 | from metrics.trolololo import TrolololoMetric
24 | from metrics.two_year_moving_average import TwoYearMovingAverageMetric
25 | from metrics.woobull_topcap_cvdd import WoobullMetric
26 | from utils import format_percentage, get_color
27 |
28 |
29 | def get_metrics() -> list[BaseMetric]:
30 | """
31 | Returns a list of available metrics to be calculated.
32 | """
33 | return [
34 | PiCycleMetric(),
35 | RUPLMetric(),
36 | RHODLMetric(),
37 | PuellMetric(),
38 | TwoYearMovingAverageMetric(),
39 | TrolololoMetric(),
40 | MVRVMetric(),
41 | ReserveRiskMetric(),
42 | WoobullMetric(),
43 | ]
44 |
45 |
46 | def calculate_confidence_score(df: pd.DataFrame, cols: list[str]) -> pd.Series:
47 | """
48 | Calculate the confidence score for a DataFrame.
49 |
50 | This function takes in a DataFrame and a list of column names
51 | and returns a Series with the mean value of the specified columns for each row.
52 |
53 | Args:
54 | df: A pandas DataFrame.
55 | cols: A list of column names to include in the calculation.
56 |
57 | Returns:
58 | A pandas Series with the mean value for the specified columns for each row in the DataFrame.
59 | """
60 | return df[cols].mean(axis=1)
61 |
62 |
63 | async def run(json_file: str, charts_file: str, output_dir: str | None) -> None:
64 | output_dir_path = Path.cwd() if output_dir is None else Path(output_dir)
65 |
66 | json_file_path = output_dir_path / Path(json_file)
67 | charts_file_path = output_dir_path / Path(charts_file)
68 |
69 | if not output_dir_path.exists():
70 | output_dir_path.mkdir(mode=0o755, parents=True)
71 |
72 | df_bitcoin = fetch_bitcoin_data()
73 | df_bitcoin_org = df_bitcoin.copy()
74 |
75 | current_price = df_bitcoin['Price'].tail(1).values[0]
76 | print('Current Bitcoin price: ' + ef.b + fg.li_green + bg.da_green + f' $ {round(current_price):,} ' + rs.all)
77 |
78 | metrics = get_metrics()
79 | metrics_cols = []
80 | metrics_descriptions = []
81 |
82 | sns.set(
83 | font_scale=0.225,
84 | rc={
85 | 'figure.titlesize': 12, # For suptitle (overridden later)
86 | 'axes.titlesize': 7.5, # 50% larger than original 5
87 | 'axes.labelsize': 6, # 50% larger than original 4
88 | 'xtick.labelsize': 4,
89 | 'ytick.labelsize': 4,
90 | 'lines.linewidth': 0.5,
91 | 'grid.linewidth': 0.3,
92 | 'savefig.dpi': 1000,
93 | 'figure.dpi': 300,
94 | },
95 | )
96 |
97 | axes_per_metric = 2
98 | fig, axes = plt.subplots(len(metrics), axes_per_metric, figsize=(4 * axes_per_metric, 3 * len(metrics)))
99 | axes = axes.reshape(-1, axes_per_metric)
100 |
101 | # Adjust layout
102 | plt.tight_layout(pad=10)
103 | plt.subplots_adjust(top=0.98)
104 |
105 | # Updated title
106 | plt.suptitle("CBBI metric data input → output", fontsize=11.25, weight='bold', y=0.99508)
107 |
108 | for metric, ax_row in zip(metrics, axes, strict=True):
109 | # Swap chart positions so visual flow goes from left to right.
110 | df_bitcoin[metric.name] = (await metric.calculate(df_bitcoin_org.copy(), [ax_row[1], ax_row[0]])).clip(0, 1)
111 | metrics_cols.append(metric.name)
112 | metrics_descriptions.append(metric.description)
113 |
114 | # Add black horizontal lines at y=1 and y=0 to show metric boundaries.
115 | ax_row[1].axhline(y=1, color='black', linewidth=0.5)
116 | ax_row[1].axhline(y=0, color='black', linewidth=0.5)
117 |
118 | # Shade above y=1 and below y=0 with 10% black, to bring focus to the data within range.
119 | y_min, y_max = ax_row[1].get_ylim() # Get the y-axis limits for reference
120 | # Shade above y=1 to the top edge
121 | ax_row[1].fill_betweenx(
122 | y=[1, y_max], # From y=1 to the top
123 | x1=0, x2=1, # Full width in axes fraction (0 to 1)
124 | transform=ax_row[1].get_yaxis_transform(), # Use y-data coordinates, x-axes fraction
125 | color='black', alpha=0.1, edgecolor='none', zorder=0
126 | )
127 | # Shade below y=0 to the bottom edge
128 | ax_row[1].fill_betweenx(
129 | y=[y_min, 0], # From bottom to y=0
130 | x1=0, x2=1, # Full width in axes fraction (0 to 1)
131 | transform=ax_row[1].get_yaxis_transform(), # Use y-data coordinates, x-axes fraction
132 | color='black', alpha=0.1, edgecolor='none', zorder=0
133 | )
134 |
135 | # Add a gray arrow between charts, to make directional flow very clear.
136 | ax_row[0].annotate(
137 | '',
138 | xy=(1.0967, 0.75), xycoords='axes fraction',
139 | xytext=(1.0367, 0.75), textcoords='axes fraction',
140 | arrowprops=dict(arrowstyle='->', color='darkgray', lw=1.5, shrinkA=0, shrinkB=0, mutation_scale=10),
141 | ha='center', va='center'
142 | )
143 |
144 | print('Generating charts…')
145 | plt.savefig(charts_file_path)
146 |
147 | confidence_col = 'Confidence'
148 |
149 | df_result = pd.DataFrame(df_bitcoin[['Date', 'Price', *metrics_cols]])
150 | df_result.set_index('Date', inplace=True)
151 | df_result[confidence_col] = calculate_confidence_score(df_result, metrics_cols)
152 | df_result.to_json(json_file_path, double_precision=4, date_unit='s', indent=2)
153 |
154 | df_result_last = df_result.tail(1)
155 | confidence_details = {
156 | description: df_result_last[name].iloc[0]
157 | for name, description in zip(metrics_cols, metrics_descriptions, strict=True)
158 | }
159 |
160 | print('\n' + ef.b + ':: Confidence we are at the peak ::' + rs.all)
161 | print(
162 | fg.cyan
163 | + ef.bold
164 | + figlet_format(format_percentage(df_result_last[confidence_col].iloc[0], ''), font='univers')
165 | + rs.all,
166 | end='',
167 | )
168 |
169 | for description, value in confidence_details.items():
170 | if not np.isnan(value):
171 | print(fg.white + get_color(value) + f'{format_percentage(value)} ' + rs.all, end='')
172 | print(f' - {description}')
173 |
174 | print()
175 | print('Source code: ' + ef.u + fg.li_blue + 'https://github.com/Zaczero/CBBI' + rs.all)
176 | print('License: ' + ef.b + 'AGPL-3.0' + rs.all)
177 | print()
178 |
179 |
180 | def run_and_retry(
181 | json_file: str = 'latest.json',
182 | charts_file: str = 'charts.svg',
183 | output_dir: str | None = 'output',
184 | max_attempts: int = 10,
185 | sleep_seconds_on_error: int = 10,
186 | ) -> None:
187 | """
188 | Calculates the current CBBI confidence value alongside all the required metrics.
189 | Everything gets pretty printed to the current standard output and a clean copy
190 | is saved to a JSON file specified by the path in the ``json_file`` argument.
191 | A charts image is generated on the path specified by the ``charts_file`` argument
192 | which summarizes all individual metrics' historical data in a visual way.
193 | The execution is attempted multiple times in case an error occurs.
194 |
195 | Args:
196 | json_file: File path where the output is saved in the JSON format.
197 | charts_file: File path where the charts image is saved (formats supported by pyplot.savefig).
198 | output_dir: Directory path where the output is stored.
199 | If set to ``None`` then use the current working directory.
200 | If the directory does not exist, it will be created.
201 | max_attempts: Maximum number of attempts before termination. An attempt is counted when an error occurs.
202 | sleep_seconds_on_error: Duration of the sleep in seconds before attempting again after an error occurs.
203 |
204 | Returns:
205 | None
206 | """
207 | assert max_attempts > 0, 'Value of the max_attempts argument must be positive'
208 | assert sleep_seconds_on_error >= 0, 'Value of the sleep_seconds_on_error argument must be non-negative'
209 |
210 | for _ in range(max_attempts):
211 | try:
212 | asyncio.run(run(json_file, charts_file, output_dir))
213 | exit(0)
214 |
215 | except Exception:
216 | print(fg.black + bg.yellow + ' An error has occurred! ' + rs.all)
217 | traceback.print_exc()
218 |
219 | print(f'\nRetrying in {sleep_seconds_on_error} seconds…', flush=True)
220 | for _ in tqdm(range(sleep_seconds_on_error)):
221 | time.sleep(1)
222 |
223 | print(f'Max attempts limit has been reached ({max_attempts}).')
224 | print('Better luck next time!')
225 | exit(-1)
226 |
227 |
228 | if __name__ == '__main__':
229 | fire.Fire(run_and_retry)
230 |
--------------------------------------------------------------------------------
/metrics/base_metric.py:
--------------------------------------------------------------------------------
1 | import traceback
2 | from abc import ABC, abstractmethod
3 |
4 | import pandas as pd
5 | from matplotlib.axes import Axes
6 | from sty import bg, fg, rs
7 |
8 | from api.cbbiinfo_api import cbbi_fetch
9 | from utils import send_error_notification
10 |
11 |
12 | class BaseMetric(ABC):
13 | @property
14 | @abstractmethod
15 | def name(self) -> str:
16 | pass
17 |
18 | @property
19 | @abstractmethod
20 | def description(self) -> str:
21 | pass
22 |
23 | @abstractmethod
24 | def _calculate(self, df: pd.DataFrame, ax: list[Axes]) -> pd.Series:
25 | pass
26 |
27 | def _fallback(self, df: pd.DataFrame) -> pd.Series:
28 | df = df.merge(cbbi_fetch(self.name), on='Date', how='left')
29 | df['Value'] = df['Value'].ffill()
30 |
31 | return df['Value']
32 |
33 | async def calculate(self, df: pd.DataFrame, ax: list[Axes]) -> pd.Series:
34 | try:
35 | return self._calculate(df, ax)
36 | except Exception as ex:
37 | traceback.print_exc()
38 | await send_error_notification(ex)
39 |
40 | print(fg.black + bg.yellow + f' Requesting fallback values for {self.name} (from CBBI.info) ' + rs.all)
41 | return self._fallback(df)
42 |
--------------------------------------------------------------------------------
/metrics/mvrv_z_score.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import pandas as pd
3 | import seaborn as sns
4 | from matplotlib.axes import Axes
5 | from sklearn.linear_model import LinearRegression
6 |
7 | from api.coinsoto_api import cs_fetch
8 | from metrics.base_metric import BaseMetric
9 | from utils import add_common_markers
10 |
11 |
12 | class MVRVMetric(BaseMetric):
13 | @property
14 | def name(self) -> str:
15 | return 'MVRV'
16 |
17 | @property
18 | def description(self) -> str:
19 | return 'MVRV Z-Score'
20 |
21 | def _calculate(self, df: pd.DataFrame, ax: list[Axes]) -> pd.Series:
22 | bull_days_shift = 6
23 | low_model_adjust = 0.26
24 |
25 | df = df.merge(
26 | cs_fetch(
27 | path='chain/index/charts?type=/charts/mvrv-zscore/',
28 | data_selector='value4',
29 | col_name='MVRV',
30 | ),
31 | on='Date',
32 | how='left',
33 | )
34 | df.loc[df['DaysSinceHalving'] < df['DaysSincePriceLow'], 'MVRV'] = df['MVRV'].shift(bull_days_shift)
35 | df['MVRV'] = df['MVRV'].ffill()
36 | df['MVRV'] = np.log(df['MVRV'] + 1)
37 |
38 | high_rows = df.loc[df['PriceHigh'] == 1]
39 | high_x = high_rows.index.values.reshape(-1, 1)
40 | high_y = high_rows['MVRV'].values.reshape(-1, 1)
41 |
42 | low_rows = df.loc[df['PriceLow'] == 1]
43 | low_x = low_rows.index.values.reshape(-1, 1)
44 | low_y = low_rows['MVRV'].values.reshape(-1, 1)
45 |
46 | x = df.index.values.reshape(-1, 1)
47 |
48 | lin_model = LinearRegression()
49 | lin_model.fit(high_x, high_y)
50 | df['HighModel'] = lin_model.predict(x)
51 |
52 | lin_model.fit(low_x, low_y)
53 | df['LowModel'] = lin_model.predict(x) + low_model_adjust
54 |
55 | df['Index'] = (df['MVRV'] - df['LowModel']) / (df['HighModel'] - df['LowModel'])
56 |
57 | df['IndexNoNa'] = df['Index'].fillna(0)
58 | ax[0].set_title(self.description)
59 | sns.lineplot(data=df, x='Date', y='IndexNoNa', ax=ax[0])
60 | add_common_markers(df, ax[0])
61 |
62 | sns.lineplot(data=df, x='Date', y='MVRV', ax=ax[1])
63 | sns.lineplot(data=df, x='Date', y='HighModel', ax=ax[1])
64 | sns.lineplot(data=df, x='Date', y='LowModel', ax=ax[1])
65 | add_common_markers(df, ax[1], price_line=False)
66 |
67 | return df['Index']
68 |
--------------------------------------------------------------------------------
/metrics/pi_cycle.py:
--------------------------------------------------------------------------------
1 | from itertools import zip_longest
2 |
3 | import numpy as np
4 | import pandas as pd
5 | import seaborn as sns
6 | from matplotlib.axes import Axes
7 |
8 | from metrics.base_metric import BaseMetric
9 | from utils import add_common_markers, mark_highs_lows, split_df_on_index_gap
10 |
11 |
12 | class PiCycleMetric(BaseMetric):
13 | @property
14 | def name(self) -> str:
15 | return 'PiCycle'
16 |
17 | @property
18 | def description(self) -> str:
19 | return 'Pi Cycle Top Indicator'
20 |
21 | def _calculate(self, df: pd.DataFrame, ax: list[Axes]) -> pd.Series:
22 | df['111DMA'] = df['Price'].rolling(111).mean()
23 | df['350DMAx2'] = df['Price'].rolling(350).mean() * 2
24 |
25 | df['111DMALog'] = np.log(df['111DMA'])
26 | df['350DMAx2Log'] = np.log(df['350DMAx2'])
27 | df['PiCycleDiff'] = np.abs(df['111DMALog'] - df['350DMAx2Log'])
28 | df['PiCycleDiffThreshold'] = 0.0
29 |
30 | df['PiCycleIsFluke'] = df['111DMA'] > df['350DMAx2']
31 |
32 | df_flukes = [*split_df_on_index_gap(df[df['PiCycleIsFluke']])]
33 | df_actuals = [*split_df_on_index_gap(df[~df['PiCycleIsFluke']])]
34 |
35 | for df_fluke, df_actual, df_fluke_next in zip_longest(df_flukes, df_actuals[1:], df_flukes[1:], fillvalue=None):
36 | if df_fluke is None:
37 | break
38 |
39 | max_divergence_idx = df_fluke['PiCycleDiff'].argmax()
40 | max_divergence_row = df_fluke.iloc[max_divergence_idx]
41 | df.loc[max_divergence_row.name < df.index, 'PiCycleDiffThreshold'] = max_divergence_row['PiCycleDiff']
42 |
43 | if df_actual is not None:
44 | df_actual_above = df_actual[df_actual['PiCycleDiff'] >= max_divergence_row['PiCycleDiff']]
45 |
46 | if df_actual_above.shape[0] > 0:
47 | df.loc[df_actual_above.index.min() <= df.index, 'PiCycleDiffThreshold'] = 0
48 |
49 | if df_fluke_next is not None:
50 | df.loc[df_fluke_next.index.min() <= df.index, 'PiCycleDiffThreshold'] = 0
51 |
52 | df.loc[df['PiCycleDiff'] < df['PiCycleDiffThreshold'], 'PiCycleDiff'] = df['PiCycleDiffThreshold']
53 | df = mark_highs_lows(df, 'PiCycleDiff', True, round(365 * 2), 365)
54 |
55 | for _, row in df.loc[df['PiCycleDiffHigh'] == 1].iterrows():
56 | df.loc[df.index > row.name, 'PreviousPiCycleDiffHighValue'] = row['PiCycleDiff']
57 |
58 | df['PiCycleIndex'] = 1 - (df['PiCycleDiff'] / df['PreviousPiCycleDiffHighValue'])
59 | df.loc[df['PiCycleIndex'] < 0, 'PiCycleIndex'] = 0
60 |
61 | df['PiCycleIndexNoNa'] = df['PiCycleIndex'].fillna(0)
62 | ax[0].set_title(self.description)
63 | sns.lineplot(data=df, x='Date', y='PiCycleIndexNoNa', ax=ax[0])
64 | add_common_markers(df, ax[0])
65 |
66 | return df['PiCycleIndex']
67 |
--------------------------------------------------------------------------------
/metrics/puell_multiple.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import pandas as pd
3 | import seaborn as sns
4 | from matplotlib.axes import Axes
5 | from sklearn.linear_model import LinearRegression
6 |
7 | from api.coinsoto_api import cs_fetch
8 | from metrics.base_metric import BaseMetric
9 | from utils import add_common_markers
10 |
11 |
12 | class PuellMetric(BaseMetric):
13 | @property
14 | def name(self) -> str:
15 | return 'Puell'
16 |
17 | @property
18 | def description(self) -> str:
19 | return 'Puell Multiple'
20 |
21 | def _calculate(self, df: pd.DataFrame, ax: list[Axes]) -> pd.Series:
22 | df = df.merge(
23 | cs_fetch(
24 | path='getPuellMultiple',
25 | data_selector='puellMultiplList',
26 | col_name='Puell',
27 | ),
28 | on='Date',
29 | how='left',
30 | )
31 | df['Puell'] = df['Puell'].ffill()
32 | df['PuellLog'] = np.log(df['Puell'])
33 |
34 | high_rows = df.loc[df['PriceHigh'] == 1]
35 | high_x = high_rows.index.values.reshape(-1, 1)
36 | high_y = high_rows['PuellLog'].values.reshape(-1, 1)
37 |
38 | # low_rows = df.loc[df['PriceLow'] == 1][1:]
39 | # low_x = low_rows.index.values.reshape(-1, 1)
40 | # low_y = low_rows['PuellLog'].values.reshape(-1, 1)
41 |
42 | x = df.index.values.reshape(-1, 1)
43 |
44 | lin_model = LinearRegression()
45 | lin_model.fit(high_x, high_y)
46 | df['PuellLogHighModel'] = lin_model.predict(x)
47 |
48 | # lin_model.fit(low_x, low_y)
49 | # df['PuellLogLowModel'] = lin_model.predict(x)
50 | df['PuellLogLowModel'] = -1
51 |
52 | df['PuellIndex'] = (df['PuellLog'] - df['PuellLogLowModel']) / (
53 | df['PuellLogHighModel'] - df['PuellLogLowModel']
54 | )
55 |
56 | df['PuellIndexNoNa'] = df['PuellIndex'].fillna(0)
57 | ax[0].set_title(self.description)
58 | sns.lineplot(data=df, x='Date', y='PuellIndexNoNa', ax=ax[0])
59 | add_common_markers(df, ax[0])
60 |
61 | sns.lineplot(data=df, x='Date', y='PuellLog', ax=ax[1])
62 | sns.lineplot(data=df, x='Date', y='PuellLogHighModel', ax=ax[1])
63 | sns.lineplot(data=df, x='Date', y='PuellLogLowModel', ax=ax[1])
64 | add_common_markers(df, ax[1], price_line=False)
65 |
66 | return df['PuellIndex']
67 |
--------------------------------------------------------------------------------
/metrics/reserve_risk.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import pandas as pd
3 | import seaborn as sns
4 | from matplotlib.axes import Axes
5 | from sklearn.linear_model import LinearRegression
6 |
7 | from api.coinsoto_api import cs_fetch
8 | from metrics.base_metric import BaseMetric
9 | from utils import add_common_markers
10 |
11 |
12 | class ReserveRiskMetric(BaseMetric):
13 | @property
14 | def name(self) -> str:
15 | return 'ReserveRisk'
16 |
17 | @property
18 | def description(self) -> str:
19 | return 'Reserve Risk'
20 |
21 | def _calculate(self, df: pd.DataFrame, ax: list[Axes]) -> pd.Series:
22 | days_shift = 1
23 |
24 | df = df.merge(
25 | cs_fetch(
26 | path='chain/index/charts?type=/charts/reserve-risk/',
27 | data_selector='value4',
28 | col_name='Risk',
29 | ),
30 | on='Date',
31 | how='left',
32 | )
33 | df['Risk'] = df['Risk'].shift(days_shift, fill_value=np.nan)
34 | df['Risk'] = df['Risk'].ffill()
35 | df['RiskLog'] = np.log(df['Risk'])
36 |
37 | high_rows = df.loc[df['PriceHigh'] == 1]
38 | high_x = high_rows.index.values.reshape(-1, 1)
39 | high_y = high_rows['RiskLog'].values.reshape(-1, 1)
40 |
41 | low_rows = df.loc[df['PriceLow'] == 1][1:]
42 | low_x = low_rows.index.values.reshape(-1, 1)
43 | low_y = low_rows['RiskLog'].values.reshape(-1, 1)
44 |
45 | x = df.index.values.reshape(-1, 1)
46 |
47 | lin_model = LinearRegression()
48 | lin_model.fit(high_x, high_y)
49 | df['HighModel'] = lin_model.predict(x)
50 | df['HighModel'] = df['HighModel'] - 0.15
51 |
52 | lin_model.fit(low_x, low_y)
53 | df['LowModel'] = lin_model.predict(x)
54 |
55 | df['RiskIndex'] = (df['RiskLog'] - df['LowModel']) / (df['HighModel'] - df['LowModel'])
56 |
57 | df['RiskIndexNoNa'] = df['RiskIndex'].fillna(0)
58 | ax[0].set_title(self.description)
59 | sns.lineplot(data=df, x='Date', y='RiskIndexNoNa', ax=ax[0])
60 | add_common_markers(df, ax[0])
61 |
62 | sns.lineplot(data=df, x='Date', y='RiskLog', ax=ax[1])
63 | sns.lineplot(data=df, x='Date', y='HighModel', ax=ax[1])
64 | sns.lineplot(data=df, x='Date', y='LowModel', ax=ax[1])
65 | add_common_markers(df, ax[1], price_line=False)
66 |
67 | return df['RiskIndex']
68 |
--------------------------------------------------------------------------------
/metrics/rhodl_ratio.py:
--------------------------------------------------------------------------------
1 | import traceback
2 |
3 | import numpy as np
4 | import pandas as pd
5 | import seaborn as sns
6 | from matplotlib.axes import Axes
7 | from sklearn.linear_model import LinearRegression
8 | from sty import bg, fg, rs
9 |
10 | from api.coinsoto_api import cs_fetch
11 | from api.glassnode_api import gn_fetch
12 | from metrics.base_metric import BaseMetric
13 | from utils import add_common_markers
14 |
15 |
16 | class RHODLMetric(BaseMetric):
17 | @property
18 | def name(self) -> str:
19 | return 'RHODL'
20 |
21 | @property
22 | def description(self) -> str:
23 | return 'RHODL Ratio'
24 |
25 | def _calculate(self, df: pd.DataFrame, ax: list[Axes]) -> pd.Series:
26 | try:
27 | remote_df = cs_fetch(
28 | path='chain/index/charts?type=/charts/rhodl-ratio/',
29 | data_selector='value1',
30 | col_name='RHODL',
31 | )
32 | except Exception:
33 | traceback.print_exc()
34 | print(fg.black + bg.yellow + f' Requesting fallback values for {self.name} (from GlassNode) ' + rs.all)
35 |
36 | remote_df = gn_fetch(url_selector='rhodl_ratio', col_name='RHODL', a='BTC')
37 |
38 | df = df.merge(remote_df, on='Date', how='left')
39 | df['RHODL'] = df['RHODL'].ffill()
40 | df['RHODLLog'] = np.log(df['RHODL'])
41 |
42 | high_rows = df.loc[(df['PriceHigh'] == 1) | (df['Date'] == '2024-12-18')]
43 | high_x = high_rows.index.values.reshape(-1, 1)
44 | high_y = high_rows['RHODLLog'].values.reshape(-1, 1)
45 |
46 | low_rows = df.loc[df['PriceLow'] == 1][1:]
47 | low_x = low_rows.index.values.reshape(-1, 1)
48 | low_y = low_rows['RHODLLog'].values.reshape(-1, 1)
49 |
50 | x = df.index.values.reshape(-1, 1)
51 |
52 | lin_model = LinearRegression()
53 | lin_model.fit(high_x, high_y)
54 | df['RHODLLogHighModel'] = lin_model.predict(x)
55 |
56 | lin_model.fit(low_x, low_y)
57 | df['RHODLLogLowModel'] = lin_model.predict(x)
58 |
59 | df['RHODLIndex'] = (df['RHODLLog'] - df['RHODLLogLowModel']) / (
60 | df['RHODLLogHighModel'] - df['RHODLLogLowModel']
61 | )
62 |
63 | df['RHODLIndexNoNa'] = df['RHODLIndex'].fillna(0)
64 | ax[0].set_title(self.description)
65 | sns.lineplot(data=df, x='Date', y='RHODLIndexNoNa', ax=ax[0])
66 | add_common_markers(df, ax[0])
67 |
68 | sns.lineplot(data=df, x='Date', y='RHODLLog', ax=ax[1])
69 | sns.lineplot(data=df, x='Date', y='RHODLLogHighModel', ax=ax[1])
70 | sns.lineplot(data=df, x='Date', y='RHODLLogLowModel', ax=ax[1])
71 | add_common_markers(df, ax[1], price_line=False)
72 |
73 | return df['RHODLIndex']
74 |
--------------------------------------------------------------------------------
/metrics/rupl.py:
--------------------------------------------------------------------------------
1 | import pandas as pd
2 | import seaborn as sns
3 | from matplotlib.axes import Axes
4 | from sklearn.linear_model import LinearRegression
5 |
6 | from api.coinsoto_api import cs_fetch
7 | from metrics.base_metric import BaseMetric
8 | from utils import add_common_markers
9 |
10 |
11 | class RUPLMetric(BaseMetric):
12 | @property
13 | def name(self) -> str:
14 | return 'RUPL'
15 |
16 | @property
17 | def description(self) -> str:
18 | return 'RUPL/NUPL Chart'
19 |
20 | def _calculate(self, df: pd.DataFrame, ax: list[Axes]) -> pd.Series:
21 | df = df.merge(
22 | cs_fetch(
23 | path='chain/index/charts?type=/charts/relative-unrealized-prof/',
24 | data_selector='value1',
25 | col_name='RUPL',
26 | ),
27 | on='Date',
28 | how='left',
29 | )
30 | df['RUPL'] = df['RUPL'].ffill()
31 |
32 | high_rows = df.loc[df['PriceHigh'] == 1]
33 | high_x = high_rows.index.values.reshape(-1, 1)
34 | high_y = high_rows['RUPL'].values.reshape(-1, 1)
35 |
36 | low_rows = df.loc[df['PriceLow'] == 1][1:]
37 | low_x = low_rows.index.values.reshape(-1, 1)
38 | low_y = low_rows['RUPL'].values.reshape(-1, 1)
39 |
40 | x = df.index.values.reshape(-1, 1)
41 |
42 | lin_model = LinearRegression()
43 | lin_model.fit(high_x, high_y)
44 | df['HighModel'] = lin_model.predict(x)
45 |
46 | lin_model.fit(low_x, low_y)
47 | df['LowModel'] = lin_model.predict(x)
48 |
49 | df['RUPLIndex'] = (df['RUPL'] - df['LowModel']) / (df['HighModel'] - df['LowModel'])
50 |
51 | ax[0].set_title(self.description)
52 | sns.lineplot(data=df, x='Date', y='RUPLIndex', ax=ax[0])
53 | add_common_markers(df, ax[0])
54 |
55 | sns.lineplot(data=df, x='Date', y='RUPL', ax=ax[1])
56 | sns.lineplot(data=df, x='Date', y='HighModel', ax=ax[1])
57 | sns.lineplot(data=df, x='Date', y='LowModel', ax=ax[1])
58 | add_common_markers(df, ax[1], price_line=False)
59 |
60 | return df['RUPLIndex']
61 |
--------------------------------------------------------------------------------
/metrics/trolololo.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import pandas as pd
3 | import seaborn as sns
4 | from matplotlib.axes import Axes
5 | from sklearn.linear_model import LinearRegression
6 |
7 | from metrics.base_metric import BaseMetric
8 | from utils import add_common_markers
9 |
10 |
11 | class TrolololoMetric(BaseMetric):
12 | @property
13 | def name(self) -> str:
14 | return 'Trolololo'
15 |
16 | @property
17 | def description(self) -> str:
18 | return 'Bitcoin Trolololo Trend Line'
19 |
20 | def _calculate(self, df: pd.DataFrame, ax: list[Axes]) -> pd.Series:
21 | begin_date = pd.to_datetime('2012-01-01')
22 |
23 | df['TroloDaysSinceBegin'] = (df['Date'] - begin_date).dt.days
24 |
25 | # Maximum Bubble Territory
26 | df['TroloTopPrice'] = np.power(10, 2.900 * np.log(df['TroloDaysSinceBegin'] + 1400) - 19.463)
27 | df['TroloTopPriceLog'] = np.log(df['TroloTopPrice'])
28 |
29 | # Basically a Fire Sale
30 | df['TroloBottomPrice'] = np.power(10, 2.788 * np.log(df['TroloDaysSinceBegin'] + 1200) - 19.463)
31 | df['TroloBottomPriceLog'] = np.log(df['TroloBottomPrice'])
32 |
33 | df['TroloDifference'] = df['TroloTopPriceLog'] - df['TroloBottomPriceLog']
34 | df['TroloOvershootActual'] = df['PriceLog'] - df['TroloTopPriceLog']
35 | df['TroloUndershootActual'] = df['PriceLog'] - df['TroloBottomPriceLog']
36 |
37 | high_rows = df.loc[(df['PriceHigh'] == 1) & (df['Date'] >= begin_date)]
38 | high_x = high_rows.index.values.reshape(-1, 1)
39 | high_y = high_rows['TroloOvershootActual'].values.reshape(-1, 1)
40 | high_y[0] *= 0.6 # the first value seems too high
41 |
42 | low_rows = df.loc[(df['PriceLow'] == 1) & (df['Date'] >= begin_date)]
43 | low_x = low_rows.index.values.reshape(-1, 1)
44 | low_y = low_rows['TroloUndershootActual'].values.reshape(-1, 1)
45 |
46 | x = df.index.values.reshape(-1, 1)
47 |
48 | lin_model = LinearRegression()
49 | lin_model.fit(high_x, high_y)
50 | df['TroloOvershootModel'] = lin_model.predict(x)
51 |
52 | lin_model.fit(low_x, low_y)
53 | df['TroloUndershootModel'] = lin_model.predict(x)
54 |
55 | df['TroloHighModel'] = df['TroloTopPriceLog'] + df['TroloOvershootModel']
56 | df['TroloLowModel'] = df['TroloBottomPriceLog'] + df['TroloUndershootModel']
57 |
58 | df['TroloIndex'] = (df['PriceLog'] - df['TroloLowModel']) / (df['TroloHighModel'] - df['TroloLowModel'])
59 |
60 | ax[0].set_title(self.description)
61 | sns.lineplot(data=df, x='Date', y='TroloIndex', ax=ax[0])
62 | add_common_markers(df, ax[0])
63 |
64 | sns.lineplot(data=df, x='Date', y='PriceLog', ax=ax[1])
65 | sns.lineplot(data=df, x='Date', y='TroloHighModel', ax=ax[1])
66 | sns.lineplot(data=df, x='Date', y='TroloLowModel', ax=ax[1])
67 | add_common_markers(df, ax[1], price_line=False)
68 |
69 | return df['TroloIndex']
70 |
--------------------------------------------------------------------------------
/metrics/two_year_moving_average.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import pandas as pd
3 | import seaborn as sns
4 | from matplotlib.axes import Axes
5 | from sklearn.linear_model import LinearRegression
6 |
7 | from api.coinsoto_api import cs_fetch
8 | from metrics.base_metric import BaseMetric
9 | from utils import add_common_markers
10 |
11 |
12 | class TwoYearMovingAverageMetric(BaseMetric):
13 | @property
14 | def name(self) -> str:
15 | return '2YMA'
16 |
17 | @property
18 | def description(self) -> str:
19 | return '2 Year Moving Average'
20 |
21 | def _calculate(self, df: pd.DataFrame, ax: list[Axes]) -> pd.Series:
22 | df = df.merge(
23 | cs_fetch(
24 | path='getBtcMultiplier',
25 | data_selector='mA730List',
26 | col_name='2YMA',
27 | ),
28 | on='Date',
29 | how='left',
30 | )
31 | df['2YMA'] = df['2YMA'].ffill()
32 | df['2YMALog'] = np.log(df['2YMA'])
33 | df['2YMALogDiff'] = df['PriceLog'] - df['2YMALog']
34 |
35 | high_rows = df.loc[df['PriceHigh'] == 1]
36 | high_x = high_rows.index.values.reshape(-1, 1)
37 | high_y = high_rows['2YMALogDiff'].values.reshape(-1, 1)
38 |
39 | low_rows = df.loc[df['PriceLow'] == 1]
40 | low_x = low_rows.index.values.reshape(-1, 1)
41 | low_y = low_rows['2YMALogDiff'].values.reshape(-1, 1)
42 |
43 | x = df.index.values.reshape(-1, 1)
44 |
45 | lin_model = LinearRegression()
46 | lin_model.fit(high_x, high_y)
47 | df['2YMALogOvershootModel'] = lin_model.predict(x)
48 |
49 | lin_model.fit(low_x, low_y)
50 | df['2YMALogUndershootModel'] = lin_model.predict(x)
51 |
52 | df['2YMAHighModel'] = df['2YMALogOvershootModel'] + df['2YMALog']
53 | df['2YMALowModel'] = df['2YMALogUndershootModel'] + df['2YMALog']
54 |
55 | df['2YMAIndex'] = (df['PriceLog'] - df['2YMALowModel']) / (df['2YMAHighModel'] - df['2YMALowModel'])
56 |
57 | df['2YMAIndexNoNa'] = df['2YMAIndex'].fillna(0)
58 | ax[0].set_title(self.description)
59 | sns.lineplot(data=df, x='Date', y='2YMAIndexNoNa', ax=ax[0])
60 | add_common_markers(df, ax[0])
61 |
62 | sns.lineplot(data=df, x='Date', y='PriceLog', ax=ax[1])
63 | sns.lineplot(data=df, x='Date', y='2YMAHighModel', ax=ax[1])
64 | sns.lineplot(data=df, x='Date', y='2YMALowModel', ax=ax[1])
65 | add_common_markers(df, ax[1], price_line=False)
66 |
67 | return df['2YMAIndex']
68 |
--------------------------------------------------------------------------------
/metrics/woobull_topcap_cvdd.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import pandas as pd
3 | import seaborn as sns
4 | from matplotlib.axes import Axes
5 | from sklearn.linear_model import LinearRegression
6 |
7 | from metrics.base_metric import BaseMetric
8 | from utils import HTTP, add_common_markers
9 |
10 |
11 | def _fetch_df() -> pd.DataFrame:
12 | response = HTTP.get('https://woocharts.com/bitcoin-price-models/data/chart.json')
13 | response.raise_for_status()
14 | data = response.json()
15 |
16 | df_top = pd.DataFrame(
17 | {
18 | 'Date': data['top_']['x'],
19 | 'Top': data['top_']['y'],
20 | }
21 | )
22 | df_top['Date'] = pd.to_datetime(df_top['Date'], unit='ms').dt.tz_localize(None)
23 |
24 | df_cvdd = pd.DataFrame(
25 | {
26 | 'Date': data['cvdd']['x'],
27 | 'CVDD': data['cvdd']['y'],
28 | }
29 | )
30 | df_cvdd['Date'] = pd.to_datetime(df_cvdd['Date'], unit='ms').dt.tz_localize(None)
31 |
32 | df = df_top.merge(df_cvdd, on='Date')
33 |
34 | return df
35 |
36 |
37 | class WoobullMetric(BaseMetric):
38 | @property
39 | def name(self) -> str:
40 | return 'Woobull'
41 |
42 | @property
43 | def description(self) -> str:
44 | return 'Woobull Top Cap vs CVDD'
45 |
46 | def _calculate(self, df: pd.DataFrame, ax: list[Axes]) -> pd.Series:
47 | df = df.merge(_fetch_df(), on='Date', how='left')
48 | df['Top'] = df['Top'].ffill()
49 | df['TopLog'] = np.log(df['Top'])
50 | df['CVDD'] = df['CVDD'].ffill()
51 | df['CVDDLog'] = np.log(df['CVDD'])
52 |
53 | df['Woobull'] = (df['PriceLog'] - df['CVDDLog']) / (df['TopLog'] - df['CVDDLog'])
54 |
55 | high_rows = df.loc[df['PriceHigh'] == 1]
56 | high_x = high_rows.index.values.reshape(-1, 1)
57 | high_y = high_rows['Woobull'].values.reshape(-1, 1)
58 |
59 | low_rows = df.loc[df['PriceLow'] == 1][1:]
60 | low_x = low_rows.index.values.reshape(-1, 1)
61 | low_y = low_rows['Woobull'].values.reshape(-1, 1)
62 |
63 | x = df.index.values.reshape(-1, 1)
64 |
65 | lin_model = LinearRegression()
66 | lin_model.fit(high_x, high_y)
67 | df['HighModel'] = lin_model.predict(x)
68 | df['HighModel'] = df['HighModel'] - 0.025
69 |
70 | lin_model.fit(low_x, low_y)
71 | df['LowModel'] = lin_model.predict(x)
72 |
73 | df['WoobullIndex'] = (df['Woobull'] - df['LowModel']) / (df['HighModel'] - df['LowModel'])
74 |
75 | ax[0].set_title(self.description)
76 | sns.lineplot(data=df, x='Date', y='WoobullIndex', ax=ax[0])
77 | add_common_markers(df, ax[0])
78 |
79 | sns.lineplot(data=df, x='Date', y='Woobull', ax=ax[1])
80 | sns.lineplot(data=df, x='Date', y='HighModel', ax=ax[1])
81 | sns.lineplot(data=df, x='Date', y='LowModel', ax=ax[1])
82 | add_common_markers(df, ax[1], price_line=False)
83 |
84 | return df['WoobullIndex']
85 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | dependencies = [
3 | "filecache",
4 | "fire",
5 | "httpx[brotli,zstd]",
6 | "matplotlib",
7 | "numpy",
8 | "pandas",
9 | "pyfiglet",
10 | "python-telegram-bot",
11 | "scikit-learn",
12 | "seaborn",
13 | "sty",
14 | "tqdm",
15 | ]
16 | name = "cbbi"
17 | requires-python = "~=3.13"
18 | version = "0.0.0"
19 |
20 | [tool.uv]
21 | package = false
22 | python-downloads = "never"
23 | python-preference = "only-system"
24 |
25 | [tool.setuptools]
26 | packages = ["."]
27 |
28 | [tool.ruff]
29 | # Exclude a variety of commonly ignored directories.
30 | exclude = [
31 | ".bzr",
32 | ".direnv",
33 | ".eggs",
34 | ".git",
35 | ".git-rewrite",
36 | ".hg",
37 | ".mypy_cache",
38 | ".nox",
39 | ".pants.d",
40 | ".pytype",
41 | ".ruff_cache",
42 | ".svn",
43 | ".tox",
44 | ".venv",
45 | "__pypackages__",
46 | "_build",
47 | "buck-out",
48 | "build",
49 | "dist",
50 | "node_modules",
51 | "venv",
52 | ]
53 |
54 | indent-width = 4
55 | line-length = 120
56 | target-version = "py313"
57 |
58 | [tool.ruff.lint]
59 | ignore = [
60 | "S101", # assert
61 | ]
62 | # see https://docs.astral.sh/ruff/rules/ for rules documentation
63 | select = [
64 | "A", # flake8-builtins
65 | "ARG", # flake8-unused-argumentsf
66 | "ASYNC", # flake8-async
67 | "B", # flake8-bugbear
68 | # "COM", # flake8-commas
69 | "C4", # flake8-comprehensions
70 | "E4", # pycodestyle
71 | "E7",
72 | "E9",
73 | "F", # pyflakes
74 | # "FBT", # flake8-boolean-trap
75 | "FLY", # flynt
76 | # "FURB", # refurb (preview)
77 | "G", # flake8-logging-format
78 | "I", # isort
79 | "INT", # flake8-gettext
80 | # "LOG", # flake8-logging (preview)
81 | "N", # pep8-naming
82 | "NPY", # numpy
83 | "PERF", # perflint
84 | "PGH", # pygrep-hooks
85 | "PIE", # flake8-pie
86 | "Q", # flake8-quotes
87 | "UP", # pyupgrade
88 | # "PL", # pylint
89 | "PT", # flake8-pytest-style
90 | "PTH", # flake8-use-pathlib
91 | "PYI", # flake8-pyi
92 | "RSE", # flake8-raise
93 | "RUF", # ruff
94 | "S", # flake8-bandit
95 | "SIM", # flake8-simplify
96 | "SLF", # flake8-self
97 | "SLOT", # flake8-slots
98 | "T10", # flake8-debugger
99 | # "T20", # flake8-print
100 | # "TRY", # tryceratops
101 | "YTT", # flake8-2020
102 | ]
103 |
104 | fixable = ["ALL"]
105 | unfixable = []
106 |
107 | [tool.ruff.format]
108 | quote-style = "single"
109 | indent-style = "space"
110 | skip-magic-trailing-comma = false
111 | line-ending = "lf"
112 |
113 | [tool.ruff.lint.flake8-builtins]
114 | builtins-ignorelist = ["id", "open", "type"]
115 |
116 | [tool.ruff.lint.flake8-quotes]
117 | docstring-quotes = "double"
118 | inline-quotes = "single"
119 | multiline-quotes = "double"
120 |
121 | [tool.ruff.lint.pylint]
122 | max-args = 10
123 |
--------------------------------------------------------------------------------
/shell.nix:
--------------------------------------------------------------------------------
1 | {}:
2 |
3 | let
4 | # Update with `nixpkgs-update` command
5 | pkgs = import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/2bfc080955153be0be56724be6fa5477b4eefabb.tar.gz") { };
6 |
7 | pythonLibs = with pkgs; [
8 | stdenv.cc.cc.lib
9 | zlib.out
10 | ];
11 | python' = with pkgs; (symlinkJoin {
12 | name = "python";
13 | paths = [ python313 ];
14 | buildInputs = [ makeWrapper ];
15 | postBuild = ''
16 | wrapProgram "$out/bin/python3.13" --prefix LD_LIBRARY_PATH : "${lib.makeLibraryPath pythonLibs}"
17 | '';
18 | });
19 |
20 | packages' = with pkgs; [
21 | python'
22 | uv
23 | ruff
24 |
25 | (writeShellScriptBin "nixpkgs-update" ''
26 | set -e
27 | hash=$(
28 | curl --silent --location \
29 | https://prometheus.nixos.org/api/v1/query \
30 | -d "query=channel_revision{channel=\"nixpkgs-unstable\"}" | \
31 | grep --only-matching --extended-regexp "[0-9a-f]{40}")
32 | sed -i -E "s|/nixpkgs/archive/[0-9a-f]{40}\.tar\.gz|/nixpkgs/archive/$hash.tar.gz|" shell.nix
33 | echo "Nixpkgs updated to $hash"
34 | '')
35 | (writeShellScriptBin "docker-build-push" ''
36 | set -e
37 | if command -v podman &> /dev/null; then docker() { podman "$@"; } fi
38 | docker push $(docker load < $(nix-build --no-out-link) | sed -En 's/Loaded image: (\S+)/\1/p')
39 | '')
40 | ];
41 |
42 | shell' = ''
43 | export SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt
44 | export PYTHONNOUSERSITE=1
45 | export PYTHONPATH=""
46 | export TZ=UTC
47 |
48 | current_python=$(readlink -e .venv/bin/python || echo "")
49 | current_python=''${current_python%/bin/*}
50 | [ "$current_python" != "${python'}" ] && rm -rf .venv/
51 |
52 | echo "Installing Python dependencies"
53 | export UV_PYTHON="${python'}/bin/python"
54 | uv sync --frozen
55 |
56 | echo "Activating Python virtual environment"
57 | source .venv/bin/activate
58 |
59 | if [ -f .env ]; then
60 | echo "Loading .env file"
61 | set -o allexport
62 | source .env set
63 | set +o allexport
64 | else
65 | echo "Skipped loading .env file (not found)"
66 | fi
67 | '';
68 | in
69 | pkgs.mkShell {
70 | buildInputs = packages';
71 | shellHook = shell';
72 | }
73 |
--------------------------------------------------------------------------------
/utils.py:
--------------------------------------------------------------------------------
1 | import os
2 | import traceback
3 | from datetime import datetime
4 | from math import ceil
5 |
6 | import numpy as np
7 | import pandas as pd
8 | import seaborn as sns
9 | import telegram
10 | from httpx import Client
11 | from matplotlib.axes import Axes
12 | from sty import bg
13 |
14 | HTTP = Client(
15 | headers={'User-Agent': 'Mozilla/5.0 (Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0'},
16 | timeout=30,
17 | follow_redirects=True,
18 | )
19 |
20 |
21 | def mark_highs_lows(
22 | df: pd.DataFrame,
23 | col: str,
24 | begin_with_high: bool,
25 | window_size: float,
26 | ignore_last_rows: int,
27 | ) -> pd.DataFrame:
28 | """
29 | Marks highs and lows (peaks) of the column values inside the given DataFrame.
30 | Marked points are indicated by the value '1' inside their corresponding, newly added, '``col``High' and '``col``Low' columns.
31 |
32 | Args:
33 | df: DataFrame from which the column values are selected and to which marked points columns are added.
34 | col: Column name of which values are selected inside the given DataFrame.
35 | begin_with_high: Indicates whether the first peak is high or low.
36 | window_size: Window size for the algorithm to consider.
37 | Too low value will mark too many peaks, whereas, too high value will mark too little peaks.
38 | ignore_last_rows: Amount of trailing DataFrame rows for which highs and lows should not be marked.
39 |
40 | Returns:
41 | Modified input DataFrame with columns, indicating the marked points, added.
42 | """
43 | col_high = col + 'High'
44 | col_low = col + 'Low'
45 |
46 | assert col in df.columns, f'The column name "{col}" (col) could not be found inside the given DataFrame (df)'
47 | assert col_high not in df.columns, 'The DataFrame (df) already contains the "High" column - bug prone'
48 | assert col_low not in df.columns, 'The DataFrame (df) already contains the "Low" column - bug prone'
49 | assert window_size > 0, 'Value of the window_size argument must be at least 1'
50 |
51 | df[col_high] = 0
52 | df[col_low] = 0
53 |
54 | searching_high = begin_with_high
55 | current_index = df.index[0]
56 |
57 | while True:
58 | window = df.loc[current_index : current_index + window_size, col]
59 |
60 | if sum(~np.isnan(window)) == 0 and window.shape[0] > 1:
61 | current_index += window.shape[0]
62 | continue
63 |
64 | if window.shape[0] <= 1:
65 | break
66 |
67 | window_index = window.idxmax() if searching_high else window.idxmin()
68 |
69 | if window_index == current_index:
70 | df.loc[window_index, col_high if searching_high else col_low] = 1
71 | searching_high = not searching_high
72 | window_index = window_index + 1
73 |
74 | current_index = window_index
75 |
76 | df.loc[df.shape[0] - ignore_last_rows :, (col_high, col_low)] = 0
77 |
78 | # stabilize the algorithm until a next major update
79 | df.loc[df['Date'] >= '2023-07-01', (col_high, col_low)] = 0
80 | return df
81 |
82 |
83 | def mark_days_since(df: pd.DataFrame, cols: list[str]) -> pd.DataFrame:
84 | """
85 | This function takes a DataFrame and a list of column names
86 | and calculates the number of days since the last value of 1 for each column in the list.
87 |
88 | The resulting DataFrame will have new columns for each input column, with the column name prefixed by 'DaysSince'.
89 | The value in these new columns will be the number of days since the last value of 1 in the corresponding input column.
90 |
91 | Args:
92 | df: The input DataFrame.
93 | cols: The list of columns in the DataFrame to calculate the days since the last value of 1.
94 |
95 | Returns:
96 | The modified DataFrame with the new columns added.
97 | """
98 | for col in cols:
99 | indexes = df.loc[df[col] == 1].index
100 | df[f'DaysSince{col}'] = df.index.to_series().apply(
101 | lambda v: min([v - index if index <= v else np.nan for index in indexes]) # noqa: B023
102 | )
103 |
104 | return df
105 |
106 |
107 | def add_common_markers(df: pd.DataFrame, ax: Axes, price_line: bool = True) -> None:
108 | """
109 | This function adds common markers to a plot.
110 |
111 | Args:
112 | df: The DataFrame containing the data to be plotted.
113 | ax: The Axes object to be plotted on.
114 | price_line: If True, a line plot of the 'PriceLogInterp' column will be added to the Axes. Default is True.
115 |
116 | Returns:
117 | None
118 | """
119 | if price_line:
120 | sns.lineplot(data=df, x='Date', y='PriceLogInterp', alpha=0.4, color='orange', ax=ax)
121 |
122 | for _, row in df[df['Halving'] == 1].iterrows():
123 | days_since_epoch = (row['Date'] - datetime(1970, 1, 1)).days
124 | ax.axvline(x=days_since_epoch, color='navy', linestyle=':')
125 |
126 | for _, row in df[df['PriceHigh'] == 1].iterrows():
127 | days_since_epoch = (row['Date'] - datetime(1970, 1, 1)).days
128 | ax.axvline(x=days_since_epoch, color='green', linestyle=':')
129 |
130 | for _, row in df[df['PriceLow'] == 1].iterrows():
131 | days_since_epoch = (row['Date'] - datetime(1970, 1, 1)).days
132 | ax.axvline(x=days_since_epoch, color='red', linestyle=':')
133 |
134 |
135 | def split_df_on_index_gap(df: pd.DataFrame, min_gap: int = 1):
136 | """
137 | Split a Pandas DataFrame on gaps in the index values.
138 |
139 | Args:
140 | df: The DataFrame to split.
141 | min_gap: The minimum gap size in the index values to split on.
142 |
143 | Returns:
144 | A list of DataFrames split on the specified gaps in the index values.
145 | """
146 | begin_idx = None
147 | end_idx = None
148 |
149 | for i, _ in df.iterrows():
150 | if begin_idx is None:
151 | begin_idx = i
152 | end_idx = i
153 | elif (i - end_idx) <= min_gap:
154 | end_idx = i
155 | else:
156 | yield df.loc[begin_idx:end_idx]
157 | begin_idx = i
158 | end_idx = i
159 |
160 | if begin_idx is not None:
161 | yield df.loc[begin_idx:end_idx]
162 |
163 |
164 | def format_percentage(val: float, suffix: str = ' %') -> str:
165 | """
166 | Formats a percentage value (0.0 - 1.0) in the standardized way.
167 | Returned value has a constant width and a trailing '%' sign.
168 |
169 | Args:
170 | val: Percentage value to be formatted.
171 | suffix: String to be appended to the result.
172 |
173 | Returns:
174 | Formatted percentage value with a constant width and trailing '%' sign.
175 |
176 | Examples:
177 | >>> print(format_percentage(0.359))
178 | str(' 36 %')
179 |
180 | >>> print(format_percentage(1.1))
181 | str('110 %')
182 | """
183 |
184 | return f'{ceil(val * 100): >3d}{suffix}'
185 |
186 |
187 | def get_color(val: float) -> str:
188 | """
189 | Maps a percentage value (0.0 - 1.0) to its corresponding color.
190 | The color is used to indicate whether the value is low (0.0) or high (1.0).
191 | Returned value is a valid sty-package color string.
192 |
193 | Args:
194 | val: Percentage value to be mapped into a color.
195 |
196 | Returns:
197 | Valid sty-package color string.
198 | """
199 |
200 | config = [
201 | bg.da_red,
202 | 0.3,
203 | bg.da_yellow,
204 | 0.65,
205 | bg.da_green,
206 | 0.85,
207 | bg.da_cyan,
208 | 0.97,
209 | bg.da_magenta,
210 | ]
211 |
212 | bin_index = np.digitize([round(val, 2)], config[1::2])[0]
213 | return config[::2][bin_index]
214 |
215 |
216 | async def send_error_notification(exception: Exception) -> bool:
217 | """
218 | This function sends a notification to a Telegram chat with details of the provided exception.
219 |
220 | Args:
221 | exception: The exception to be reported.
222 |
223 | Returns:
224 | A boolean indicating whether the notification was sent successfully.
225 | """
226 | telegram_token = os.getenv('TELEGRAM_TOKEN')
227 | telegram_chat_id = os.getenv('TELEGRAM_CHAT_ID')
228 | if not telegram_token or not telegram_chat_id:
229 | return False
230 |
231 | async with telegram.Bot(telegram_token) as bot:
232 | await bot.send_message(
233 | telegram_chat_id,
234 | f'🚨 An error has occurred: {exception!s}\n'
235 | f'\n'
236 | f'🔍️ Stack trace\n'
237 | f'{"".join(traceback.format_exception(exception))}
',
238 | parse_mode='HTML',
239 | )
240 | return True
241 |
--------------------------------------------------------------------------------
/uv.lock:
--------------------------------------------------------------------------------
1 | version = 1
2 | revision = 1
3 | requires-python = ">=3.13, <4"
4 |
5 | [[package]]
6 | name = "anyio"
7 | version = "4.9.0"
8 | source = { registry = "https://pypi.org/simple" }
9 | dependencies = [
10 | { name = "idna" },
11 | { name = "sniffio" },
12 | ]
13 | sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 }
14 | wheels = [
15 | { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 },
16 | ]
17 |
18 | [[package]]
19 | name = "brotli"
20 | version = "1.1.0"
21 | source = { registry = "https://pypi.org/simple" }
22 | sdist = { url = "https://files.pythonhosted.org/packages/2f/c2/f9e977608bdf958650638c3f1e28f85a1b075f075ebbe77db8555463787b/Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724", size = 7372270 }
23 | wheels = [
24 | { url = "https://files.pythonhosted.org/packages/0a/9f/fb37bb8ffc52a8da37b1c03c459a8cd55df7a57bdccd8831d500e994a0ca/Brotli-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5", size = 815681 },
25 | { url = "https://files.pythonhosted.org/packages/06/b3/dbd332a988586fefb0aa49c779f59f47cae76855c2d00f450364bb574cac/Brotli-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8", size = 422475 },
26 | { url = "https://files.pythonhosted.org/packages/bb/80/6aaddc2f63dbcf2d93c2d204e49c11a9ec93a8c7c63261e2b4bd35198283/Brotli-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f", size = 2906173 },
27 | { url = "https://files.pythonhosted.org/packages/ea/1d/e6ca79c96ff5b641df6097d299347507d39a9604bde8915e76bf026d6c77/Brotli-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648", size = 2943803 },
28 | { url = "https://files.pythonhosted.org/packages/ac/a3/d98d2472e0130b7dd3acdbb7f390d478123dbf62b7d32bda5c830a96116d/Brotli-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0", size = 2918946 },
29 | { url = "https://files.pythonhosted.org/packages/c4/a5/c69e6d272aee3e1423ed005d8915a7eaa0384c7de503da987f2d224d0721/Brotli-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089", size = 2845707 },
30 | { url = "https://files.pythonhosted.org/packages/58/9f/4149d38b52725afa39067350696c09526de0125ebfbaab5acc5af28b42ea/Brotli-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368", size = 2936231 },
31 | { url = "https://files.pythonhosted.org/packages/5a/5a/145de884285611838a16bebfdb060c231c52b8f84dfbe52b852a15780386/Brotli-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c", size = 2848157 },
32 | { url = "https://files.pythonhosted.org/packages/50/ae/408b6bfb8525dadebd3b3dd5b19d631da4f7d46420321db44cd99dcf2f2c/Brotli-1.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284", size = 3035122 },
33 | { url = "https://files.pythonhosted.org/packages/af/85/a94e5cfaa0ca449d8f91c3d6f78313ebf919a0dbd55a100c711c6e9655bc/Brotli-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7", size = 2930206 },
34 | { url = "https://files.pythonhosted.org/packages/c2/f0/a61d9262cd01351df22e57ad7c34f66794709acab13f34be2675f45bf89d/Brotli-1.1.0-cp313-cp313-win32.whl", hash = "sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0", size = 333804 },
35 | { url = "https://files.pythonhosted.org/packages/7e/c1/ec214e9c94000d1c1974ec67ced1c970c148aa6b8d8373066123fc3dbf06/Brotli-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b", size = 358517 },
36 | ]
37 |
38 | [[package]]
39 | name = "brotlicffi"
40 | version = "1.1.0.0"
41 | source = { registry = "https://pypi.org/simple" }
42 | dependencies = [
43 | { name = "cffi" },
44 | ]
45 | sdist = { url = "https://files.pythonhosted.org/packages/95/9d/70caa61192f570fcf0352766331b735afa931b4c6bc9a348a0925cc13288/brotlicffi-1.1.0.0.tar.gz", hash = "sha256:b77827a689905143f87915310b93b273ab17888fd43ef350d4832c4a71083c13", size = 465192 }
46 | wheels = [
47 | { url = "https://files.pythonhosted.org/packages/a2/11/7b96009d3dcc2c931e828ce1e157f03824a69fb728d06bfd7b2fc6f93718/brotlicffi-1.1.0.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9b7ae6bd1a3f0df532b6d67ff674099a96d22bc0948955cb338488c31bfb8851", size = 453786 },
48 | { url = "https://files.pythonhosted.org/packages/d6/e6/a8f46f4a4ee7856fbd6ac0c6fb0dc65ed181ba46cd77875b8d9bbe494d9e/brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19ffc919fa4fc6ace69286e0a23b3789b4219058313cf9b45625016bf7ff996b", size = 2911165 },
49 | { url = "https://files.pythonhosted.org/packages/be/20/201559dff14e83ba345a5ec03335607e47467b6633c210607e693aefac40/brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9feb210d932ffe7798ee62e6145d3a757eb6233aa9a4e7db78dd3690d7755814", size = 2927895 },
50 | { url = "https://files.pythonhosted.org/packages/cd/15/695b1409264143be3c933f708a3f81d53c4a1e1ebbc06f46331decbf6563/brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84763dbdef5dd5c24b75597a77e1b30c66604725707565188ba54bab4f114820", size = 2851834 },
51 | { url = "https://files.pythonhosted.org/packages/b4/40/b961a702463b6005baf952794c2e9e0099bde657d0d7e007f923883b907f/brotlicffi-1.1.0.0-cp37-abi3-win32.whl", hash = "sha256:1b12b50e07c3911e1efa3a8971543e7648100713d4e0971b13631cce22c587eb", size = 341731 },
52 | { url = "https://files.pythonhosted.org/packages/1c/fa/5408a03c041114ceab628ce21766a4ea882aa6f6f0a800e04ee3a30ec6b9/brotlicffi-1.1.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:994a4f0681bb6c6c3b0925530a1926b7a189d878e6e5e38fae8efa47c5d9c613", size = 366783 },
53 | ]
54 |
55 | [[package]]
56 | name = "cbbi"
57 | version = "0.0.0"
58 | source = { virtual = "." }
59 | dependencies = [
60 | { name = "filecache" },
61 | { name = "fire" },
62 | { name = "httpx", extra = ["brotli", "zstd"] },
63 | { name = "matplotlib" },
64 | { name = "numpy" },
65 | { name = "pandas" },
66 | { name = "pyfiglet" },
67 | { name = "python-telegram-bot" },
68 | { name = "scikit-learn" },
69 | { name = "seaborn" },
70 | { name = "sty" },
71 | { name = "tqdm" },
72 | ]
73 |
74 | [package.metadata]
75 | requires-dist = [
76 | { name = "filecache" },
77 | { name = "fire" },
78 | { name = "httpx", extras = ["brotli", "zstd"] },
79 | { name = "matplotlib" },
80 | { name = "numpy" },
81 | { name = "pandas" },
82 | { name = "pyfiglet" },
83 | { name = "python-telegram-bot" },
84 | { name = "scikit-learn" },
85 | { name = "seaborn" },
86 | { name = "sty" },
87 | { name = "tqdm" },
88 | ]
89 |
90 | [[package]]
91 | name = "certifi"
92 | version = "2025.1.31"
93 | source = { registry = "https://pypi.org/simple" }
94 | sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 }
95 | wheels = [
96 | { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 },
97 | ]
98 |
99 | [[package]]
100 | name = "cffi"
101 | version = "1.17.1"
102 | source = { registry = "https://pypi.org/simple" }
103 | dependencies = [
104 | { name = "pycparser" },
105 | ]
106 | sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 }
107 | wheels = [
108 | { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 },
109 | { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 },
110 | { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 },
111 | { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 },
112 | { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 },
113 | { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 },
114 | { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 },
115 | { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 },
116 | { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 },
117 | { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 },
118 | { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 },
119 | ]
120 |
121 | [[package]]
122 | name = "colorama"
123 | version = "0.4.6"
124 | source = { registry = "https://pypi.org/simple" }
125 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
126 | wheels = [
127 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
128 | ]
129 |
130 | [[package]]
131 | name = "contourpy"
132 | version = "1.3.1"
133 | source = { registry = "https://pypi.org/simple" }
134 | dependencies = [
135 | { name = "numpy" },
136 | ]
137 | sdist = { url = "https://files.pythonhosted.org/packages/25/c2/fc7193cc5383637ff390a712e88e4ded0452c9fbcf84abe3de5ea3df1866/contourpy-1.3.1.tar.gz", hash = "sha256:dfd97abd83335045a913e3bcc4a09c0ceadbe66580cf573fe961f4a825efa699", size = 13465753 }
138 | wheels = [
139 | { url = "https://files.pythonhosted.org/packages/9a/e7/de62050dce687c5e96f946a93546910bc67e483fe05324439e329ff36105/contourpy-1.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a761d9ccfc5e2ecd1bf05534eda382aa14c3e4f9205ba5b1684ecfe400716ef2", size = 271548 },
140 | { url = "https://files.pythonhosted.org/packages/78/4d/c2a09ae014ae984c6bdd29c11e74d3121b25eaa117eca0bb76340efd7e1c/contourpy-1.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:523a8ee12edfa36f6d2a49407f705a6ef4c5098de4f498619787e272de93f2d5", size = 255576 },
141 | { url = "https://files.pythonhosted.org/packages/ab/8a/915380ee96a5638bda80cd061ccb8e666bfdccea38d5741cb69e6dbd61fc/contourpy-1.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece6df05e2c41bd46776fbc712e0996f7c94e0d0543af1656956d150c4ca7c81", size = 306635 },
142 | { url = "https://files.pythonhosted.org/packages/29/5c/c83ce09375428298acd4e6582aeb68b1e0d1447f877fa993d9bf6cd3b0a0/contourpy-1.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:573abb30e0e05bf31ed067d2f82500ecfdaec15627a59d63ea2d95714790f5c2", size = 345925 },
143 | { url = "https://files.pythonhosted.org/packages/29/63/5b52f4a15e80c66c8078a641a3bfacd6e07106835682454647aca1afc852/contourpy-1.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9fa36448e6a3a1a9a2ba23c02012c43ed88905ec80163f2ffe2421c7192a5d7", size = 318000 },
144 | { url = "https://files.pythonhosted.org/packages/9a/e2/30ca086c692691129849198659bf0556d72a757fe2769eb9620a27169296/contourpy-1.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ea9924d28fc5586bf0b42d15f590b10c224117e74409dd7a0be3b62b74a501c", size = 322689 },
145 | { url = "https://files.pythonhosted.org/packages/6b/77/f37812ef700f1f185d348394debf33f22d531e714cf6a35d13d68a7003c7/contourpy-1.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b75aa69cb4d6f137b36f7eb2ace9280cfb60c55dc5f61c731fdf6f037f958a3", size = 1268413 },
146 | { url = "https://files.pythonhosted.org/packages/3f/6d/ce84e79cdd128542ebeb268f84abb4b093af78e7f8ec504676673d2675bc/contourpy-1.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:041b640d4ec01922083645a94bb3b2e777e6b626788f4095cf21abbe266413c1", size = 1326530 },
147 | { url = "https://files.pythonhosted.org/packages/72/22/8282f4eae20c73c89bee7a82a19c4e27af9b57bb602ecaa00713d5bdb54d/contourpy-1.3.1-cp313-cp313-win32.whl", hash = "sha256:36987a15e8ace5f58d4d5da9dca82d498c2bbb28dff6e5d04fbfcc35a9cb3a82", size = 175315 },
148 | { url = "https://files.pythonhosted.org/packages/e3/d5/28bca491f65312b438fbf076589dcde7f6f966b196d900777f5811b9c4e2/contourpy-1.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:a7895f46d47671fa7ceec40f31fae721da51ad34bdca0bee83e38870b1f47ffd", size = 220987 },
149 | { url = "https://files.pythonhosted.org/packages/2f/24/a4b285d6adaaf9746e4700932f579f1a7b6f9681109f694cfa233ae75c4e/contourpy-1.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9ddeb796389dadcd884c7eb07bd14ef12408aaae358f0e2ae24114d797eede30", size = 285001 },
150 | { url = "https://files.pythonhosted.org/packages/48/1d/fb49a401b5ca4f06ccf467cd6c4f1fd65767e63c21322b29b04ec40b40b9/contourpy-1.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:19c1555a6801c2f084c7ddc1c6e11f02eb6a6016ca1318dd5452ba3f613a1751", size = 268553 },
151 | { url = "https://files.pythonhosted.org/packages/79/1e/4aef9470d13fd029087388fae750dccb49a50c012a6c8d1d634295caa644/contourpy-1.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:841ad858cff65c2c04bf93875e384ccb82b654574a6d7f30453a04f04af71342", size = 310386 },
152 | { url = "https://files.pythonhosted.org/packages/b0/34/910dc706ed70153b60392b5305c708c9810d425bde12499c9184a1100888/contourpy-1.3.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4318af1c925fb9a4fb190559ef3eec206845f63e80fb603d47f2d6d67683901c", size = 349806 },
153 | { url = "https://files.pythonhosted.org/packages/31/3c/faee6a40d66d7f2a87f7102236bf4780c57990dd7f98e5ff29881b1b1344/contourpy-1.3.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:14c102b0eab282427b662cb590f2e9340a9d91a1c297f48729431f2dcd16e14f", size = 321108 },
154 | { url = "https://files.pythonhosted.org/packages/17/69/390dc9b20dd4bb20585651d7316cc3054b7d4a7b4f8b710b2b698e08968d/contourpy-1.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05e806338bfeaa006acbdeba0ad681a10be63b26e1b17317bfac3c5d98f36cda", size = 327291 },
155 | { url = "https://files.pythonhosted.org/packages/ef/74/7030b67c4e941fe1e5424a3d988080e83568030ce0355f7c9fc556455b01/contourpy-1.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4d76d5993a34ef3df5181ba3c92fabb93f1eaa5729504fb03423fcd9f3177242", size = 1263752 },
156 | { url = "https://files.pythonhosted.org/packages/f0/ed/92d86f183a8615f13f6b9cbfc5d4298a509d6ce433432e21da838b4b63f4/contourpy-1.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:89785bb2a1980c1bd87f0cb1517a71cde374776a5f150936b82580ae6ead44a1", size = 1318403 },
157 | { url = "https://files.pythonhosted.org/packages/b3/0e/c8e4950c77dcfc897c71d61e56690a0a9df39543d2164040301b5df8e67b/contourpy-1.3.1-cp313-cp313t-win32.whl", hash = "sha256:8eb96e79b9f3dcadbad2a3891672f81cdcab7f95b27f28f1c67d75f045b6b4f1", size = 185117 },
158 | { url = "https://files.pythonhosted.org/packages/c1/31/1ae946f11dfbd229222e6d6ad8e7bd1891d3d48bde5fbf7a0beb9491f8e3/contourpy-1.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:287ccc248c9e0d0566934e7d606201abd74761b5703d804ff3df8935f523d546", size = 236668 },
159 | ]
160 |
161 | [[package]]
162 | name = "cycler"
163 | version = "0.12.1"
164 | source = { registry = "https://pypi.org/simple" }
165 | sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615 }
166 | wheels = [
167 | { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321 },
168 | ]
169 |
170 | [[package]]
171 | name = "filecache"
172 | version = "0.81"
173 | source = { registry = "https://pypi.org/simple" }
174 | sdist = { url = "https://files.pythonhosted.org/packages/b3/f5/647f13b1cae32f8d3b84866f6bac688b7923c5d7643b994e5e89865c9a2a/filecache-0.81.tar.gz", hash = "sha256:be071ad64937b51f38b03ecd82b9b68c08d0f570cdddb30aa8f90150fe54b30a", size = 6423 }
175 | wheels = [
176 | { url = "https://files.pythonhosted.org/packages/eb/79/f96a2addff21798ea11aa51ae15052514e9ac0ab4ab9470ddd1a0da6fd3e/filecache-0.81-py3-none-any.whl", hash = "sha256:91ce1a42b532d0e9ad75364c13159bafc3015973d4a5a0dbf37e4b4feb194055", size = 4449 },
177 | ]
178 |
179 | [[package]]
180 | name = "fire"
181 | version = "0.7.0"
182 | source = { registry = "https://pypi.org/simple" }
183 | dependencies = [
184 | { name = "termcolor" },
185 | ]
186 | sdist = { url = "https://files.pythonhosted.org/packages/6b/b6/82c7e601d6d3c3278c40b7bd35e17e82aa227f050aa9f66cb7b7fce29471/fire-0.7.0.tar.gz", hash = "sha256:961550f07936eaf65ad1dc8360f2b2bf8408fad46abbfa4d2a3794f8d2a95cdf", size = 87189 }
187 |
188 | [[package]]
189 | name = "fonttools"
190 | version = "4.57.0"
191 | source = { registry = "https://pypi.org/simple" }
192 | sdist = { url = "https://files.pythonhosted.org/packages/03/2d/a9a0b6e3a0cf6bd502e64fc16d894269011930cabfc89aee20d1635b1441/fonttools-4.57.0.tar.gz", hash = "sha256:727ece10e065be2f9dd239d15dd5d60a66e17eac11aea47d447f9f03fdbc42de", size = 3492448 }
193 | wheels = [
194 | { url = "https://files.pythonhosted.org/packages/e9/2f/11439f3af51e4bb75ac9598c29f8601aa501902dcedf034bdc41f47dd799/fonttools-4.57.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:408ce299696012d503b714778d89aa476f032414ae57e57b42e4b92363e0b8ef", size = 2739175 },
195 | { url = "https://files.pythonhosted.org/packages/25/52/677b55a4c0972dc3820c8dba20a29c358197a78229daa2ea219fdb19e5d5/fonttools-4.57.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bbceffc80aa02d9e8b99f2a7491ed8c4a783b2fc4020119dc405ca14fb5c758c", size = 2276583 },
196 | { url = "https://files.pythonhosted.org/packages/64/79/184555f8fa77b827b9460a4acdbbc0b5952bb6915332b84c615c3a236826/fonttools-4.57.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f022601f3ee9e1f6658ed6d184ce27fa5216cee5b82d279e0f0bde5deebece72", size = 4766437 },
197 | { url = "https://files.pythonhosted.org/packages/f8/ad/c25116352f456c0d1287545a7aa24e98987b6d99c5b0456c4bd14321f20f/fonttools-4.57.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dea5893b58d4637ffa925536462ba626f8a1b9ffbe2f5c272cdf2c6ebadb817", size = 4838431 },
198 | { url = "https://files.pythonhosted.org/packages/53/ae/398b2a833897297797a44f519c9af911c2136eb7aa27d3f1352c6d1129fa/fonttools-4.57.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dff02c5c8423a657c550b48231d0a48d7e2b2e131088e55983cfe74ccc2c7cc9", size = 4951011 },
199 | { url = "https://files.pythonhosted.org/packages/b7/5d/7cb31c4bc9ffb9a2bbe8b08f8f53bad94aeb158efad75da645b40b62cb73/fonttools-4.57.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:767604f244dc17c68d3e2dbf98e038d11a18abc078f2d0f84b6c24571d9c0b13", size = 5205679 },
200 | { url = "https://files.pythonhosted.org/packages/4c/e4/6934513ec2c4d3d69ca1bc3bd34d5c69dafcbf68c15388dd3bb062daf345/fonttools-4.57.0-cp313-cp313-win32.whl", hash = "sha256:8e2e12d0d862f43d51e5afb8b9751c77e6bec7d2dc00aad80641364e9df5b199", size = 2144833 },
201 | { url = "https://files.pythonhosted.org/packages/c4/0d/2177b7fdd23d017bcfb702fd41e47d4573766b9114da2fddbac20dcc4957/fonttools-4.57.0-cp313-cp313-win_amd64.whl", hash = "sha256:f1d6bc9c23356908db712d282acb3eebd4ae5ec6d8b696aa40342b1d84f8e9e3", size = 2190799 },
202 | { url = "https://files.pythonhosted.org/packages/90/27/45f8957c3132917f91aaa56b700bcfc2396be1253f685bd5c68529b6f610/fonttools-4.57.0-py3-none-any.whl", hash = "sha256:3122c604a675513c68bd24c6a8f9091f1c2376d18e8f5fe5a101746c81b3e98f", size = 1093605 },
203 | ]
204 |
205 | [[package]]
206 | name = "h11"
207 | version = "0.14.0"
208 | source = { registry = "https://pypi.org/simple" }
209 | sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 }
210 | wheels = [
211 | { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 },
212 | ]
213 |
214 | [[package]]
215 | name = "httpcore"
216 | version = "1.0.7"
217 | source = { registry = "https://pypi.org/simple" }
218 | dependencies = [
219 | { name = "certifi" },
220 | { name = "h11" },
221 | ]
222 | sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 }
223 | wheels = [
224 | { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 },
225 | ]
226 |
227 | [[package]]
228 | name = "httpx"
229 | version = "0.28.1"
230 | source = { registry = "https://pypi.org/simple" }
231 | dependencies = [
232 | { name = "anyio" },
233 | { name = "certifi" },
234 | { name = "httpcore" },
235 | { name = "idna" },
236 | ]
237 | sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 }
238 | wheels = [
239 | { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 },
240 | ]
241 |
242 | [package.optional-dependencies]
243 | brotli = [
244 | { name = "brotli", marker = "platform_python_implementation == 'CPython'" },
245 | { name = "brotlicffi", marker = "platform_python_implementation != 'CPython'" },
246 | ]
247 | zstd = [
248 | { name = "zstandard" },
249 | ]
250 |
251 | [[package]]
252 | name = "idna"
253 | version = "3.10"
254 | source = { registry = "https://pypi.org/simple" }
255 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
256 | wheels = [
257 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
258 | ]
259 |
260 | [[package]]
261 | name = "joblib"
262 | version = "1.4.2"
263 | source = { registry = "https://pypi.org/simple" }
264 | sdist = { url = "https://files.pythonhosted.org/packages/64/33/60135848598c076ce4b231e1b1895170f45fbcaeaa2c9d5e38b04db70c35/joblib-1.4.2.tar.gz", hash = "sha256:2382c5816b2636fbd20a09e0f4e9dad4736765fdfb7dca582943b9c1366b3f0e", size = 2116621 }
265 | wheels = [
266 | { url = "https://files.pythonhosted.org/packages/91/29/df4b9b42f2be0b623cbd5e2140cafcaa2bef0759a00b7b70104dcfe2fb51/joblib-1.4.2-py3-none-any.whl", hash = "sha256:06d478d5674cbc267e7496a410ee875abd68e4340feff4490bcb7afb88060ae6", size = 301817 },
267 | ]
268 |
269 | [[package]]
270 | name = "kiwisolver"
271 | version = "1.4.8"
272 | source = { registry = "https://pypi.org/simple" }
273 | sdist = { url = "https://files.pythonhosted.org/packages/82/59/7c91426a8ac292e1cdd53a63b6d9439abd573c875c3f92c146767dd33faf/kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e", size = 97538 }
274 | wheels = [
275 | { url = "https://files.pythonhosted.org/packages/79/b3/e62464a652f4f8cd9006e13d07abad844a47df1e6537f73ddfbf1bc997ec/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1c8ceb754339793c24aee1c9fb2485b5b1f5bb1c2c214ff13368431e51fc9a09", size = 124156 },
276 | { url = "https://files.pythonhosted.org/packages/8d/2d/f13d06998b546a2ad4f48607a146e045bbe48030774de29f90bdc573df15/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a62808ac74b5e55a04a408cda6156f986cefbcf0ada13572696b507cc92fa1", size = 66555 },
277 | { url = "https://files.pythonhosted.org/packages/59/e3/b8bd14b0a54998a9fd1e8da591c60998dc003618cb19a3f94cb233ec1511/kiwisolver-1.4.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:68269e60ee4929893aad82666821aaacbd455284124817af45c11e50a4b42e3c", size = 65071 },
278 | { url = "https://files.pythonhosted.org/packages/f0/1c/6c86f6d85ffe4d0ce04228d976f00674f1df5dc893bf2dd4f1928748f187/kiwisolver-1.4.8-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34d142fba9c464bc3bbfeff15c96eab0e7310343d6aefb62a79d51421fcc5f1b", size = 1378053 },
279 | { url = "https://files.pythonhosted.org/packages/4e/b9/1c6e9f6dcb103ac5cf87cb695845f5fa71379021500153566d8a8a9fc291/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc373e0eef45b59197de815b1b28ef89ae3955e7722cc9710fb91cd77b7f47", size = 1472278 },
280 | { url = "https://files.pythonhosted.org/packages/ee/81/aca1eb176de671f8bda479b11acdc42c132b61a2ac861c883907dde6debb/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77e6f57a20b9bd4e1e2cedda4d0b986ebd0216236f0106e55c28aea3d3d69b16", size = 1478139 },
281 | { url = "https://files.pythonhosted.org/packages/49/f4/e081522473671c97b2687d380e9e4c26f748a86363ce5af48b4a28e48d06/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08e77738ed7538f036cd1170cbed942ef749137b1311fa2bbe2a7fda2f6bf3cc", size = 1413517 },
282 | { url = "https://files.pythonhosted.org/packages/8f/e9/6a7d025d8da8c4931522922cd706105aa32b3291d1add8c5427cdcd66e63/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5ce1e481a74b44dd5e92ff03ea0cb371ae7a0268318e202be06c8f04f4f1246", size = 1474952 },
283 | { url = "https://files.pythonhosted.org/packages/82/13/13fa685ae167bee5d94b415991c4fc7bb0a1b6ebea6e753a87044b209678/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fc2ace710ba7c1dfd1a3b42530b62b9ceed115f19a1656adefce7b1782a37794", size = 2269132 },
284 | { url = "https://files.pythonhosted.org/packages/ef/92/bb7c9395489b99a6cb41d502d3686bac692586db2045adc19e45ee64ed23/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3452046c37c7692bd52b0e752b87954ef86ee2224e624ef7ce6cb21e8c41cc1b", size = 2425997 },
285 | { url = "https://files.pythonhosted.org/packages/ed/12/87f0e9271e2b63d35d0d8524954145837dd1a6c15b62a2d8c1ebe0f182b4/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e9a60b50fe8b2ec6f448fe8d81b07e40141bfced7f896309df271a0b92f80f3", size = 2376060 },
286 | { url = "https://files.pythonhosted.org/packages/02/6e/c8af39288edbce8bf0fa35dee427b082758a4b71e9c91ef18fa667782138/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:918139571133f366e8362fa4a297aeba86c7816b7ecf0bc79168080e2bd79957", size = 2520471 },
287 | { url = "https://files.pythonhosted.org/packages/13/78/df381bc7b26e535c91469f77f16adcd073beb3e2dd25042efd064af82323/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e063ef9f89885a1d68dd8b2e18f5ead48653176d10a0e324e3b0030e3a69adeb", size = 2338793 },
288 | { url = "https://files.pythonhosted.org/packages/d0/dc/c1abe38c37c071d0fc71c9a474fd0b9ede05d42f5a458d584619cfd2371a/kiwisolver-1.4.8-cp313-cp313-win_amd64.whl", hash = "sha256:a17b7c4f5b2c51bb68ed379defd608a03954a1845dfed7cc0117f1cc8a9b7fd2", size = 71855 },
289 | { url = "https://files.pythonhosted.org/packages/a0/b6/21529d595b126ac298fdd90b705d87d4c5693de60023e0efcb4f387ed99e/kiwisolver-1.4.8-cp313-cp313-win_arm64.whl", hash = "sha256:3cd3bc628b25f74aedc6d374d5babf0166a92ff1317f46267f12d2ed54bc1d30", size = 65430 },
290 | { url = "https://files.pythonhosted.org/packages/34/bd/b89380b7298e3af9b39f49334e3e2a4af0e04819789f04b43d560516c0c8/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:370fd2df41660ed4e26b8c9d6bbcad668fbe2560462cba151a721d49e5b6628c", size = 126294 },
291 | { url = "https://files.pythonhosted.org/packages/83/41/5857dc72e5e4148eaac5aa76e0703e594e4465f8ab7ec0fc60e3a9bb8fea/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:84a2f830d42707de1d191b9490ac186bf7997a9495d4e9072210a1296345f7dc", size = 67736 },
292 | { url = "https://files.pythonhosted.org/packages/e1/d1/be059b8db56ac270489fb0b3297fd1e53d195ba76e9bbb30e5401fa6b759/kiwisolver-1.4.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7a3ad337add5148cf51ce0b55642dc551c0b9d6248458a757f98796ca7348712", size = 66194 },
293 | { url = "https://files.pythonhosted.org/packages/e1/83/4b73975f149819eb7dcf9299ed467eba068ecb16439a98990dcb12e63fdd/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7506488470f41169b86d8c9aeff587293f530a23a23a49d6bc64dab66bedc71e", size = 1465942 },
294 | { url = "https://files.pythonhosted.org/packages/c7/2c/30a5cdde5102958e602c07466bce058b9d7cb48734aa7a4327261ac8e002/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f0121b07b356a22fb0414cec4666bbe36fd6d0d759db3d37228f496ed67c880", size = 1595341 },
295 | { url = "https://files.pythonhosted.org/packages/ff/9b/1e71db1c000385aa069704f5990574b8244cce854ecd83119c19e83c9586/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6d6bd87df62c27d4185de7c511c6248040afae67028a8a22012b010bc7ad062", size = 1598455 },
296 | { url = "https://files.pythonhosted.org/packages/85/92/c8fec52ddf06231b31cbb779af77e99b8253cd96bd135250b9498144c78b/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:291331973c64bb9cce50bbe871fb2e675c4331dab4f31abe89f175ad7679a4d7", size = 1522138 },
297 | { url = "https://files.pythonhosted.org/packages/0b/51/9eb7e2cd07a15d8bdd976f6190c0164f92ce1904e5c0c79198c4972926b7/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:893f5525bb92d3d735878ec00f781b2de998333659507d29ea4466208df37bed", size = 1582857 },
298 | { url = "https://files.pythonhosted.org/packages/0f/95/c5a00387a5405e68ba32cc64af65ce881a39b98d73cc394b24143bebc5b8/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b47a465040146981dc9db8647981b8cb96366fbc8d452b031e4f8fdffec3f26d", size = 2293129 },
299 | { url = "https://files.pythonhosted.org/packages/44/83/eeb7af7d706b8347548313fa3a3a15931f404533cc54fe01f39e830dd231/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:99cea8b9dd34ff80c521aef46a1dddb0dcc0283cf18bde6d756f1e6f31772165", size = 2421538 },
300 | { url = "https://files.pythonhosted.org/packages/05/f9/27e94c1b3eb29e6933b6986ffc5fa1177d2cd1f0c8efc5f02c91c9ac61de/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:151dffc4865e5fe6dafce5480fab84f950d14566c480c08a53c663a0020504b6", size = 2390661 },
301 | { url = "https://files.pythonhosted.org/packages/d9/d4/3c9735faa36ac591a4afcc2980d2691000506050b7a7e80bcfe44048daa7/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:577facaa411c10421314598b50413aa1ebcf5126f704f1e5d72d7e4e9f020d90", size = 2546710 },
302 | { url = "https://files.pythonhosted.org/packages/4c/fa/be89a49c640930180657482a74970cdcf6f7072c8d2471e1babe17a222dc/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:be4816dc51c8a471749d664161b434912eee82f2ea66bd7628bd14583a833e85", size = 2349213 },
303 | ]
304 |
305 | [[package]]
306 | name = "matplotlib"
307 | version = "3.10.1"
308 | source = { registry = "https://pypi.org/simple" }
309 | dependencies = [
310 | { name = "contourpy" },
311 | { name = "cycler" },
312 | { name = "fonttools" },
313 | { name = "kiwisolver" },
314 | { name = "numpy" },
315 | { name = "packaging" },
316 | { name = "pillow" },
317 | { name = "pyparsing" },
318 | { name = "python-dateutil" },
319 | ]
320 | sdist = { url = "https://files.pythonhosted.org/packages/2f/08/b89867ecea2e305f408fbb417139a8dd941ecf7b23a2e02157c36da546f0/matplotlib-3.10.1.tar.gz", hash = "sha256:e8d2d0e3881b129268585bf4765ad3ee73a4591d77b9a18c214ac7e3a79fb2ba", size = 36743335 }
321 | wheels = [
322 | { url = "https://files.pythonhosted.org/packages/60/73/6770ff5e5523d00f3bc584acb6031e29ee5c8adc2336b16cd1d003675fe0/matplotlib-3.10.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c42eee41e1b60fd83ee3292ed83a97a5f2a8239b10c26715d8a6172226988d7b", size = 8176112 },
323 | { url = "https://files.pythonhosted.org/packages/08/97/b0ca5da0ed54a3f6599c3ab568bdda65269bc27c21a2c97868c1625e4554/matplotlib-3.10.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4f0647b17b667ae745c13721602b540f7aadb2a32c5b96e924cd4fea5dcb90f1", size = 8046931 },
324 | { url = "https://files.pythonhosted.org/packages/df/9a/1acbdc3b165d4ce2dcd2b1a6d4ffb46a7220ceee960c922c3d50d8514067/matplotlib-3.10.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa3854b5f9473564ef40a41bc922be978fab217776e9ae1545c9b3a5cf2092a3", size = 8453422 },
325 | { url = "https://files.pythonhosted.org/packages/51/d0/2bc4368abf766203e548dc7ab57cf7e9c621f1a3c72b516cc7715347b179/matplotlib-3.10.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e496c01441be4c7d5f96d4e40f7fca06e20dcb40e44c8daa2e740e1757ad9e6", size = 8596819 },
326 | { url = "https://files.pythonhosted.org/packages/ab/1b/8b350f8a1746c37ab69dda7d7528d1fc696efb06db6ade9727b7887be16d/matplotlib-3.10.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5d45d3f5245be5b469843450617dcad9af75ca50568acf59997bed9311131a0b", size = 9402782 },
327 | { url = "https://files.pythonhosted.org/packages/89/06/f570373d24d93503988ba8d04f213a372fa1ce48381c5eb15da985728498/matplotlib-3.10.1-cp313-cp313-win_amd64.whl", hash = "sha256:8e8e25b1209161d20dfe93037c8a7f7ca796ec9aa326e6e4588d8c4a5dd1e473", size = 8063812 },
328 | { url = "https://files.pythonhosted.org/packages/fc/e0/8c811a925b5a7ad75135f0e5af46408b78af88bbb02a1df775100ef9bfef/matplotlib-3.10.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:19b06241ad89c3ae9469e07d77efa87041eac65d78df4fcf9cac318028009b01", size = 8214021 },
329 | { url = "https://files.pythonhosted.org/packages/4a/34/319ec2139f68ba26da9d00fce2ff9f27679fb799a6c8e7358539801fd629/matplotlib-3.10.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01e63101ebb3014e6e9f80d9cf9ee361a8599ddca2c3e166c563628b39305dbb", size = 8090782 },
330 | { url = "https://files.pythonhosted.org/packages/77/ea/9812124ab9a99df5b2eec1110e9b2edc0b8f77039abf4c56e0a376e84a29/matplotlib-3.10.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f06bad951eea6422ac4e8bdebcf3a70c59ea0a03338c5d2b109f57b64eb3972", size = 8478901 },
331 | { url = "https://files.pythonhosted.org/packages/c9/db/b05bf463689134789b06dea85828f8ebe506fa1e37593f723b65b86c9582/matplotlib-3.10.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3dfb036f34873b46978f55e240cff7a239f6c4409eac62d8145bad3fc6ba5a3", size = 8613864 },
332 | { url = "https://files.pythonhosted.org/packages/c2/04/41ccec4409f3023a7576df3b5c025f1a8c8b81fbfe922ecfd837ac36e081/matplotlib-3.10.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dc6ab14a7ab3b4d813b88ba957fc05c79493a037f54e246162033591e770de6f", size = 9409487 },
333 | { url = "https://files.pythonhosted.org/packages/ac/c2/0d5aae823bdcc42cc99327ecdd4d28585e15ccd5218c453b7bcd827f3421/matplotlib-3.10.1-cp313-cp313t-win_amd64.whl", hash = "sha256:bc411ebd5889a78dabbc457b3fa153203e22248bfa6eedc6797be5df0164dbf9", size = 8134832 },
334 | ]
335 |
336 | [[package]]
337 | name = "numpy"
338 | version = "2.2.4"
339 | source = { registry = "https://pypi.org/simple" }
340 | sdist = { url = "https://files.pythonhosted.org/packages/e1/78/31103410a57bc2c2b93a3597340a8119588571f6a4539067546cb9a0bfac/numpy-2.2.4.tar.gz", hash = "sha256:9ba03692a45d3eef66559efe1d1096c4b9b75c0986b5dff5530c378fb8331d4f", size = 20270701 }
341 | wheels = [
342 | { url = "https://files.pythonhosted.org/packages/2a/d0/bd5ad792e78017f5decfb2ecc947422a3669a34f775679a76317af671ffc/numpy-2.2.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cf4e5c6a278d620dee9ddeb487dc6a860f9b199eadeecc567f777daace1e9e7", size = 20933623 },
343 | { url = "https://files.pythonhosted.org/packages/c3/bc/2b3545766337b95409868f8e62053135bdc7fa2ce630aba983a2aa60b559/numpy-2.2.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1974afec0b479e50438fc3648974268f972e2d908ddb6d7fb634598cdb8260a0", size = 14148681 },
344 | { url = "https://files.pythonhosted.org/packages/6a/70/67b24d68a56551d43a6ec9fe8c5f91b526d4c1a46a6387b956bf2d64744e/numpy-2.2.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:79bd5f0a02aa16808fcbc79a9a376a147cc1045f7dfe44c6e7d53fa8b8a79392", size = 5148759 },
345 | { url = "https://files.pythonhosted.org/packages/1c/8b/e2fc8a75fcb7be12d90b31477c9356c0cbb44abce7ffb36be39a0017afad/numpy-2.2.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:3387dd7232804b341165cedcb90694565a6015433ee076c6754775e85d86f1fc", size = 6683092 },
346 | { url = "https://files.pythonhosted.org/packages/13/73/41b7b27f169ecf368b52533edb72e56a133f9e86256e809e169362553b49/numpy-2.2.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f527d8fdb0286fd2fd97a2a96c6be17ba4232da346931d967a0630050dfd298", size = 14081422 },
347 | { url = "https://files.pythonhosted.org/packages/4b/04/e208ff3ae3ddfbafc05910f89546382f15a3f10186b1f56bd99f159689c2/numpy-2.2.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bce43e386c16898b91e162e5baaad90c4b06f9dcbe36282490032cec98dc8ae7", size = 16132202 },
348 | { url = "https://files.pythonhosted.org/packages/fe/bc/2218160574d862d5e55f803d88ddcad88beff94791f9c5f86d67bd8fbf1c/numpy-2.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31504f970f563d99f71a3512d0c01a645b692b12a63630d6aafa0939e52361e6", size = 15573131 },
349 | { url = "https://files.pythonhosted.org/packages/a5/78/97c775bc4f05abc8a8426436b7cb1be806a02a2994b195945600855e3a25/numpy-2.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:81413336ef121a6ba746892fad881a83351ee3e1e4011f52e97fba79233611fd", size = 17894270 },
350 | { url = "https://files.pythonhosted.org/packages/b9/eb/38c06217a5f6de27dcb41524ca95a44e395e6a1decdc0c99fec0832ce6ae/numpy-2.2.4-cp313-cp313-win32.whl", hash = "sha256:f486038e44caa08dbd97275a9a35a283a8f1d2f0ee60ac260a1790e76660833c", size = 6308141 },
351 | { url = "https://files.pythonhosted.org/packages/52/17/d0dd10ab6d125c6d11ffb6dfa3423c3571befab8358d4f85cd4471964fcd/numpy-2.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:207a2b8441cc8b6a2a78c9ddc64d00d20c303d79fba08c577752f080c4007ee3", size = 12636885 },
352 | { url = "https://files.pythonhosted.org/packages/fa/e2/793288ede17a0fdc921172916efb40f3cbc2aa97e76c5c84aba6dc7e8747/numpy-2.2.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8120575cb4882318c791f839a4fd66161a6fa46f3f0a5e613071aae35b5dd8f8", size = 20961829 },
353 | { url = "https://files.pythonhosted.org/packages/3a/75/bb4573f6c462afd1ea5cbedcc362fe3e9bdbcc57aefd37c681be1155fbaa/numpy-2.2.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a761ba0fa886a7bb33c6c8f6f20213735cb19642c580a931c625ee377ee8bd39", size = 14161419 },
354 | { url = "https://files.pythonhosted.org/packages/03/68/07b4cd01090ca46c7a336958b413cdbe75002286295f2addea767b7f16c9/numpy-2.2.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:ac0280f1ba4a4bfff363a99a6aceed4f8e123f8a9b234c89140f5e894e452ecd", size = 5196414 },
355 | { url = "https://files.pythonhosted.org/packages/a5/fd/d4a29478d622fedff5c4b4b4cedfc37a00691079623c0575978d2446db9e/numpy-2.2.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:879cf3a9a2b53a4672a168c21375166171bc3932b7e21f622201811c43cdd3b0", size = 6709379 },
356 | { url = "https://files.pythonhosted.org/packages/41/78/96dddb75bb9be730b87c72f30ffdd62611aba234e4e460576a068c98eff6/numpy-2.2.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f05d4198c1bacc9124018109c5fba2f3201dbe7ab6e92ff100494f236209c960", size = 14051725 },
357 | { url = "https://files.pythonhosted.org/packages/00/06/5306b8199bffac2a29d9119c11f457f6c7d41115a335b78d3f86fad4dbe8/numpy-2.2.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2f085ce2e813a50dfd0e01fbfc0c12bbe5d2063d99f8b29da30e544fb6483b8", size = 16101638 },
358 | { url = "https://files.pythonhosted.org/packages/fa/03/74c5b631ee1ded596945c12027649e6344614144369fd3ec1aaced782882/numpy-2.2.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:92bda934a791c01d6d9d8e038363c50918ef7c40601552a58ac84c9613a665bc", size = 15571717 },
359 | { url = "https://files.pythonhosted.org/packages/cb/dc/4fc7c0283abe0981e3b89f9b332a134e237dd476b0c018e1e21083310c31/numpy-2.2.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ee4d528022f4c5ff67332469e10efe06a267e32f4067dc76bb7e2cddf3cd25ff", size = 17879998 },
360 | { url = "https://files.pythonhosted.org/packages/e5/2b/878576190c5cfa29ed896b518cc516aecc7c98a919e20706c12480465f43/numpy-2.2.4-cp313-cp313t-win32.whl", hash = "sha256:05c076d531e9998e7e694c36e8b349969c56eadd2cdcd07242958489d79a7286", size = 6366896 },
361 | { url = "https://files.pythonhosted.org/packages/3e/05/eb7eec66b95cf697f08c754ef26c3549d03ebd682819f794cb039574a0a6/numpy-2.2.4-cp313-cp313t-win_amd64.whl", hash = "sha256:188dcbca89834cc2e14eb2f106c96d6d46f200fe0200310fc29089657379c58d", size = 12739119 },
362 | ]
363 |
364 | [[package]]
365 | name = "packaging"
366 | version = "24.2"
367 | source = { registry = "https://pypi.org/simple" }
368 | sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 }
369 | wheels = [
370 | { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
371 | ]
372 |
373 | [[package]]
374 | name = "pandas"
375 | version = "2.2.3"
376 | source = { registry = "https://pypi.org/simple" }
377 | dependencies = [
378 | { name = "numpy" },
379 | { name = "python-dateutil" },
380 | { name = "pytz" },
381 | { name = "tzdata" },
382 | ]
383 | sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213 }
384 | wheels = [
385 | { url = "https://files.pythonhosted.org/packages/64/22/3b8f4e0ed70644e85cfdcd57454686b9057c6c38d2f74fe4b8bc2527214a/pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015", size = 12477643 },
386 | { url = "https://files.pythonhosted.org/packages/e4/93/b3f5d1838500e22c8d793625da672f3eec046b1a99257666c94446969282/pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28", size = 11281573 },
387 | { url = "https://files.pythonhosted.org/packages/f5/94/6c79b07f0e5aab1dcfa35a75f4817f5c4f677931d4234afcd75f0e6a66ca/pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0", size = 15196085 },
388 | { url = "https://files.pythonhosted.org/packages/e8/31/aa8da88ca0eadbabd0a639788a6da13bb2ff6edbbb9f29aa786450a30a91/pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24", size = 12711809 },
389 | { url = "https://files.pythonhosted.org/packages/ee/7c/c6dbdb0cb2a4344cacfb8de1c5808ca885b2e4dcfde8008266608f9372af/pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659", size = 16356316 },
390 | { url = "https://files.pythonhosted.org/packages/57/b7/8b757e7d92023b832869fa8881a992696a0bfe2e26f72c9ae9f255988d42/pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb", size = 14022055 },
391 | { url = "https://files.pythonhosted.org/packages/3b/bc/4b18e2b8c002572c5a441a64826252ce5da2aa738855747247a971988043/pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d", size = 11481175 },
392 | { url = "https://files.pythonhosted.org/packages/76/a3/a5d88146815e972d40d19247b2c162e88213ef51c7c25993942c39dbf41d/pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468", size = 12615650 },
393 | { url = "https://files.pythonhosted.org/packages/9c/8c/f0fd18f6140ddafc0c24122c8a964e48294acc579d47def376fef12bcb4a/pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18", size = 11290177 },
394 | { url = "https://files.pythonhosted.org/packages/ed/f9/e995754eab9c0f14c6777401f7eece0943840b7a9fc932221c19d1abee9f/pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2", size = 14651526 },
395 | { url = "https://files.pythonhosted.org/packages/25/b0/98d6ae2e1abac4f35230aa756005e8654649d305df9a28b16b9ae4353bff/pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4", size = 11871013 },
396 | { url = "https://files.pythonhosted.org/packages/cc/57/0f72a10f9db6a4628744c8e8f0df4e6e21de01212c7c981d31e50ffc8328/pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d", size = 15711620 },
397 | { url = "https://files.pythonhosted.org/packages/ab/5f/b38085618b950b79d2d9164a711c52b10aefc0ae6833b96f626b7021b2ed/pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", size = 13098436 },
398 | ]
399 |
400 | [[package]]
401 | name = "pillow"
402 | version = "11.1.0"
403 | source = { registry = "https://pypi.org/simple" }
404 | sdist = { url = "https://files.pythonhosted.org/packages/f3/af/c097e544e7bd278333db77933e535098c259609c4eb3b85381109602fb5b/pillow-11.1.0.tar.gz", hash = "sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20", size = 46742715 }
405 | wheels = [
406 | { url = "https://files.pythonhosted.org/packages/b3/31/9ca79cafdce364fd5c980cd3416c20ce1bebd235b470d262f9d24d810184/pillow-11.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae98e14432d458fc3de11a77ccb3ae65ddce70f730e7c76140653048c71bfcbc", size = 3226640 },
407 | { url = "https://files.pythonhosted.org/packages/ac/0f/ff07ad45a1f172a497aa393b13a9d81a32e1477ef0e869d030e3c1532521/pillow-11.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cc1331b6d5a6e144aeb5e626f4375f5b7ae9934ba620c0ac6b3e43d5e683a0f0", size = 3101437 },
408 | { url = "https://files.pythonhosted.org/packages/08/2f/9906fca87a68d29ec4530be1f893149e0cb64a86d1f9f70a7cfcdfe8ae44/pillow-11.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:758e9d4ef15d3560214cddbc97b8ef3ef86ce04d62ddac17ad39ba87e89bd3b1", size = 4326605 },
409 | { url = "https://files.pythonhosted.org/packages/b0/0f/f3547ee15b145bc5c8b336401b2d4c9d9da67da9dcb572d7c0d4103d2c69/pillow-11.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b523466b1a31d0dcef7c5be1f20b942919b62fd6e9a9be199d035509cbefc0ec", size = 4411173 },
410 | { url = "https://files.pythonhosted.org/packages/b1/df/bf8176aa5db515c5de584c5e00df9bab0713548fd780c82a86cba2c2fedb/pillow-11.1.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:9044b5e4f7083f209c4e35aa5dd54b1dd5b112b108648f5c902ad586d4f945c5", size = 4369145 },
411 | { url = "https://files.pythonhosted.org/packages/de/7c/7433122d1cfadc740f577cb55526fdc39129a648ac65ce64db2eb7209277/pillow-11.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:3764d53e09cdedd91bee65c2527815d315c6b90d7b8b79759cc48d7bf5d4f114", size = 4496340 },
412 | { url = "https://files.pythonhosted.org/packages/25/46/dd94b93ca6bd555588835f2504bd90c00d5438fe131cf01cfa0c5131a19d/pillow-11.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31eba6bbdd27dde97b0174ddf0297d7a9c3a507a8a1480e1e60ef914fe23d352", size = 4296906 },
413 | { url = "https://files.pythonhosted.org/packages/a8/28/2f9d32014dfc7753e586db9add35b8a41b7a3b46540e965cb6d6bc607bd2/pillow-11.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b5d658fbd9f0d6eea113aea286b21d3cd4d3fd978157cbf2447a6035916506d3", size = 4431759 },
414 | { url = "https://files.pythonhosted.org/packages/33/48/19c2cbe7403870fbe8b7737d19eb013f46299cdfe4501573367f6396c775/pillow-11.1.0-cp313-cp313-win32.whl", hash = "sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9", size = 2291657 },
415 | { url = "https://files.pythonhosted.org/packages/3b/ad/285c556747d34c399f332ba7c1a595ba245796ef3e22eae190f5364bb62b/pillow-11.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:593c5fd6be85da83656b93ffcccc2312d2d149d251e98588b14fbc288fd8909c", size = 2626304 },
416 | { url = "https://files.pythonhosted.org/packages/e5/7b/ef35a71163bf36db06e9c8729608f78dedf032fc8313d19bd4be5c2588f3/pillow-11.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:11633d58b6ee5733bde153a8dafd25e505ea3d32e261accd388827ee987baf65", size = 2375117 },
417 | { url = "https://files.pythonhosted.org/packages/79/30/77f54228401e84d6791354888549b45824ab0ffde659bafa67956303a09f/pillow-11.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:70ca5ef3b3b1c4a0812b5c63c57c23b63e53bc38e758b37a951e5bc466449861", size = 3230060 },
418 | { url = "https://files.pythonhosted.org/packages/ce/b1/56723b74b07dd64c1010fee011951ea9c35a43d8020acd03111f14298225/pillow-11.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8000376f139d4d38d6851eb149b321a52bb8893a88dae8ee7d95840431977081", size = 3106192 },
419 | { url = "https://files.pythonhosted.org/packages/e1/cd/7bf7180e08f80a4dcc6b4c3a0aa9e0b0ae57168562726a05dc8aa8fa66b0/pillow-11.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee85f0696a17dd28fbcfceb59f9510aa71934b483d1f5601d1030c3c8304f3c", size = 4446805 },
420 | { url = "https://files.pythonhosted.org/packages/97/42/87c856ea30c8ed97e8efbe672b58c8304dee0573f8c7cab62ae9e31db6ae/pillow-11.1.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:dd0e081319328928531df7a0e63621caf67652c8464303fd102141b785ef9547", size = 4530623 },
421 | { url = "https://files.pythonhosted.org/packages/ff/41/026879e90c84a88e33fb00cc6bd915ac2743c67e87a18f80270dfe3c2041/pillow-11.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e63e4e5081de46517099dc30abe418122f54531a6ae2ebc8680bcd7096860eab", size = 4465191 },
422 | { url = "https://files.pythonhosted.org/packages/e5/fb/a7960e838bc5df57a2ce23183bfd2290d97c33028b96bde332a9057834d3/pillow-11.1.0-cp313-cp313t-win32.whl", hash = "sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9", size = 2295494 },
423 | { url = "https://files.pythonhosted.org/packages/d7/6c/6ec83ee2f6f0fda8d4cf89045c6be4b0373ebfc363ba8538f8c999f63fcd/pillow-11.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe", size = 2631595 },
424 | { url = "https://files.pythonhosted.org/packages/cf/6c/41c21c6c8af92b9fea313aa47c75de49e2f9a467964ee33eb0135d47eb64/pillow-11.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756", size = 2377651 },
425 | ]
426 |
427 | [[package]]
428 | name = "pycparser"
429 | version = "2.22"
430 | source = { registry = "https://pypi.org/simple" }
431 | sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 }
432 | wheels = [
433 | { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 },
434 | ]
435 |
436 | [[package]]
437 | name = "pyfiglet"
438 | version = "1.0.2"
439 | source = { registry = "https://pypi.org/simple" }
440 | sdist = { url = "https://files.pythonhosted.org/packages/a0/f2/2649b2acace54f861eccd4ab163bfd914236fc93ddb1df02dad2a2552b14/pyfiglet-1.0.2.tar.gz", hash = "sha256:758788018ab8faaddc0984e1ea05ff330d3c64be663c513cc1f105f6a3066dab", size = 832345 }
441 | wheels = [
442 | { url = "https://files.pythonhosted.org/packages/1a/03/bef6fff907e212d67a0003f8ea4819307bba91b2856074a0763dd483ccc4/pyfiglet-1.0.2-py3-none-any.whl", hash = "sha256:889b351d79c99e50a3f619c8f8e6ffdb27fd8c939fc43ecbd7559bd57d5f93ea", size = 1085824 },
443 | ]
444 |
445 | [[package]]
446 | name = "pyparsing"
447 | version = "3.2.3"
448 | source = { registry = "https://pypi.org/simple" }
449 | sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608 }
450 | wheels = [
451 | { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120 },
452 | ]
453 |
454 | [[package]]
455 | name = "python-dateutil"
456 | version = "2.9.0.post0"
457 | source = { registry = "https://pypi.org/simple" }
458 | dependencies = [
459 | { name = "six" },
460 | ]
461 | sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 }
462 | wheels = [
463 | { 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 },
464 | ]
465 |
466 | [[package]]
467 | name = "python-telegram-bot"
468 | version = "22.0"
469 | source = { registry = "https://pypi.org/simple" }
470 | dependencies = [
471 | { name = "httpx" },
472 | ]
473 | sdist = { url = "https://files.pythonhosted.org/packages/61/8c/0bd0d5c6de549ee0ebc2ddf4d49618eec1ece6d25084f3b4ef72bba6590c/python_telegram_bot-22.0.tar.gz", hash = "sha256:acf86f28d86d81cab736177d2988e5bcb27f2248137efd62e02c46e9ba1fe44c", size = 440017 }
474 | wheels = [
475 | { url = "https://files.pythonhosted.org/packages/15/9f/b8c116f606074c19ec2600a7edc222f158c307ca949de568d67fe2b9d364/python_telegram_bot-22.0-py3-none-any.whl", hash = "sha256:23237f778655e634f08cfebbada96ed3692c2bdd3c20c122e90a6d606d6a4516", size = 673473 },
476 | ]
477 |
478 | [[package]]
479 | name = "pytz"
480 | version = "2025.2"
481 | source = { registry = "https://pypi.org/simple" }
482 | sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884 }
483 | wheels = [
484 | { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225 },
485 | ]
486 |
487 | [[package]]
488 | name = "scikit-learn"
489 | version = "1.6.1"
490 | source = { registry = "https://pypi.org/simple" }
491 | dependencies = [
492 | { name = "joblib" },
493 | { name = "numpy" },
494 | { name = "scipy" },
495 | { name = "threadpoolctl" },
496 | ]
497 | sdist = { url = "https://files.pythonhosted.org/packages/9e/a5/4ae3b3a0755f7b35a280ac90b28817d1f380318973cff14075ab41ef50d9/scikit_learn-1.6.1.tar.gz", hash = "sha256:b4fc2525eca2c69a59260f583c56a7557c6ccdf8deafdba6e060f94c1c59738e", size = 7068312 }
498 | wheels = [
499 | { url = "https://files.pythonhosted.org/packages/2e/59/8eb1872ca87009bdcdb7f3cdc679ad557b992c12f4b61f9250659e592c63/scikit_learn-1.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ffa1e9e25b3d93990e74a4be2c2fc61ee5af85811562f1288d5d055880c4322", size = 12010001 },
500 | { url = "https://files.pythonhosted.org/packages/9d/05/f2fc4effc5b32e525408524c982c468c29d22f828834f0625c5ef3d601be/scikit_learn-1.6.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:dc5cf3d68c5a20ad6d571584c0750ec641cc46aeef1c1507be51300e6003a7e1", size = 11096360 },
501 | { url = "https://files.pythonhosted.org/packages/c8/e4/4195d52cf4f113573fb8ebc44ed5a81bd511a92c0228889125fac2f4c3d1/scikit_learn-1.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c06beb2e839ecc641366000ca84f3cf6fa9faa1777e29cf0c04be6e4d096a348", size = 12209004 },
502 | { url = "https://files.pythonhosted.org/packages/94/be/47e16cdd1e7fcf97d95b3cb08bde1abb13e627861af427a3651fcb80b517/scikit_learn-1.6.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8ca8cb270fee8f1f76fa9bfd5c3507d60c6438bbee5687f81042e2bb98e5a97", size = 13171776 },
503 | { url = "https://files.pythonhosted.org/packages/34/b0/ca92b90859070a1487827dbc672f998da95ce83edce1270fc23f96f1f61a/scikit_learn-1.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:7a1c43c8ec9fde528d664d947dc4c0789be4077a3647f232869f41d9bf50e0fb", size = 11071865 },
504 | { url = "https://files.pythonhosted.org/packages/12/ae/993b0fb24a356e71e9a894e42b8a9eec528d4c70217353a1cd7a48bc25d4/scikit_learn-1.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a17c1dea1d56dcda2fac315712f3651a1fea86565b64b48fa1bc090249cbf236", size = 11955804 },
505 | { url = "https://files.pythonhosted.org/packages/d6/54/32fa2ee591af44507eac86406fa6bba968d1eb22831494470d0a2e4a1eb1/scikit_learn-1.6.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6a7aa5f9908f0f28f4edaa6963c0a6183f1911e63a69aa03782f0d924c830a35", size = 11100530 },
506 | { url = "https://files.pythonhosted.org/packages/3f/58/55856da1adec655bdce77b502e94a267bf40a8c0b89f8622837f89503b5a/scikit_learn-1.6.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0650e730afb87402baa88afbf31c07b84c98272622aaba002559b614600ca691", size = 12433852 },
507 | { url = "https://files.pythonhosted.org/packages/ff/4f/c83853af13901a574f8f13b645467285a48940f185b690936bb700a50863/scikit_learn-1.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:3f59fe08dc03ea158605170eb52b22a105f238a5d512c4470ddeca71feae8e5f", size = 11337256 },
508 | ]
509 |
510 | [[package]]
511 | name = "scipy"
512 | version = "1.15.2"
513 | source = { registry = "https://pypi.org/simple" }
514 | dependencies = [
515 | { name = "numpy" },
516 | ]
517 | sdist = { url = "https://files.pythonhosted.org/packages/b7/b9/31ba9cd990e626574baf93fbc1ac61cf9ed54faafd04c479117517661637/scipy-1.15.2.tar.gz", hash = "sha256:cd58a314d92838f7e6f755c8a2167ead4f27e1fd5c1251fd54289569ef3495ec", size = 59417316 }
518 | wheels = [
519 | { url = "https://files.pythonhosted.org/packages/53/40/09319f6e0f276ea2754196185f95cd191cb852288440ce035d5c3a931ea2/scipy-1.15.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01edfac9f0798ad6b46d9c4c9ca0e0ad23dbf0b1eb70e96adb9fa7f525eff0bf", size = 38717587 },
520 | { url = "https://files.pythonhosted.org/packages/fe/c3/2854f40ecd19585d65afaef601e5e1f8dbf6758b2f95b5ea93d38655a2c6/scipy-1.15.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:08b57a9336b8e79b305a143c3655cc5bdbe6d5ece3378578888d2afbb51c4e37", size = 30100266 },
521 | { url = "https://files.pythonhosted.org/packages/dd/b1/f9fe6e3c828cb5930b5fe74cb479de5f3d66d682fa8adb77249acaf545b8/scipy-1.15.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:54c462098484e7466362a9f1672d20888f724911a74c22ae35b61f9c5919183d", size = 22373768 },
522 | { url = "https://files.pythonhosted.org/packages/15/9d/a60db8c795700414c3f681908a2b911e031e024d93214f2d23c6dae174ab/scipy-1.15.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:cf72ff559a53a6a6d77bd8eefd12a17995ffa44ad86c77a5df96f533d4e6c6bb", size = 25154719 },
523 | { url = "https://files.pythonhosted.org/packages/37/3b/9bda92a85cd93f19f9ed90ade84aa1e51657e29988317fabdd44544f1dd4/scipy-1.15.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9de9d1416b3d9e7df9923ab23cd2fe714244af10b763975bea9e4f2e81cebd27", size = 35163195 },
524 | { url = "https://files.pythonhosted.org/packages/03/5a/fc34bf1aa14dc7c0e701691fa8685f3faec80e57d816615e3625f28feb43/scipy-1.15.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb530e4794fc8ea76a4a21ccb67dea33e5e0e60f07fc38a49e821e1eae3b71a0", size = 37255404 },
525 | { url = "https://files.pythonhosted.org/packages/4a/71/472eac45440cee134c8a180dbe4c01b3ec247e0338b7c759e6cd71f199a7/scipy-1.15.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5ea7ed46d437fc52350b028b1d44e002646e28f3e8ddc714011aaf87330f2f32", size = 36860011 },
526 | { url = "https://files.pythonhosted.org/packages/01/b3/21f890f4f42daf20e4d3aaa18182dddb9192771cd47445aaae2e318f6738/scipy-1.15.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:11e7ad32cf184b74380f43d3c0a706f49358b904fa7d5345f16ddf993609184d", size = 39657406 },
527 | { url = "https://files.pythonhosted.org/packages/0d/76/77cf2ac1f2a9cc00c073d49e1e16244e389dd88e2490c91d84e1e3e4d126/scipy-1.15.2-cp313-cp313-win_amd64.whl", hash = "sha256:a5080a79dfb9b78b768cebf3c9dcbc7b665c5875793569f48bf0e2b1d7f68f6f", size = 40961243 },
528 | { url = "https://files.pythonhosted.org/packages/4c/4b/a57f8ddcf48e129e6054fa9899a2a86d1fc6b07a0e15c7eebff7ca94533f/scipy-1.15.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:447ce30cee6a9d5d1379087c9e474628dab3db4a67484be1b7dc3196bfb2fac9", size = 38870286 },
529 | { url = "https://files.pythonhosted.org/packages/0c/43/c304d69a56c91ad5f188c0714f6a97b9c1fed93128c691148621274a3a68/scipy-1.15.2-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:c90ebe8aaa4397eaefa8455a8182b164a6cc1d59ad53f79943f266d99f68687f", size = 30141634 },
530 | { url = "https://files.pythonhosted.org/packages/44/1a/6c21b45d2548eb73be9b9bff421aaaa7e85e22c1f9b3bc44b23485dfce0a/scipy-1.15.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:def751dd08243934c884a3221156d63e15234a3155cf25978b0a668409d45eb6", size = 22415179 },
531 | { url = "https://files.pythonhosted.org/packages/74/4b/aefac4bba80ef815b64f55da06f62f92be5d03b467f2ce3668071799429a/scipy-1.15.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:302093e7dfb120e55515936cb55618ee0b895f8bcaf18ff81eca086c17bd80af", size = 25126412 },
532 | { url = "https://files.pythonhosted.org/packages/b1/53/1cbb148e6e8f1660aacd9f0a9dfa2b05e9ff1cb54b4386fe868477972ac2/scipy-1.15.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cd5b77413e1855351cdde594eca99c1f4a588c2d63711388b6a1f1c01f62274", size = 34952867 },
533 | { url = "https://files.pythonhosted.org/packages/2c/23/e0eb7f31a9c13cf2dca083828b97992dd22f8184c6ce4fec5deec0c81fcf/scipy-1.15.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d0194c37037707b2afa7a2f2a924cf7bac3dc292d51b6a925e5fcb89bc5c776", size = 36890009 },
534 | { url = "https://files.pythonhosted.org/packages/03/f3/e699e19cabe96bbac5189c04aaa970718f0105cff03d458dc5e2b6bd1e8c/scipy-1.15.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:bae43364d600fdc3ac327db99659dcb79e6e7ecd279a75fe1266669d9a652828", size = 36545159 },
535 | { url = "https://files.pythonhosted.org/packages/af/f5/ab3838e56fe5cc22383d6fcf2336e48c8fe33e944b9037fbf6cbdf5a11f8/scipy-1.15.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f031846580d9acccd0044efd1a90e6f4df3a6e12b4b6bd694a7bc03a89892b28", size = 39136566 },
536 | { url = "https://files.pythonhosted.org/packages/0a/c8/b3f566db71461cabd4b2d5b39bcc24a7e1c119535c8361f81426be39bb47/scipy-1.15.2-cp313-cp313t-win_amd64.whl", hash = "sha256:fe8a9eb875d430d81755472c5ba75e84acc980e4a8f6204d402849234d3017db", size = 40477705 },
537 | ]
538 |
539 | [[package]]
540 | name = "seaborn"
541 | version = "0.13.2"
542 | source = { registry = "https://pypi.org/simple" }
543 | dependencies = [
544 | { name = "matplotlib" },
545 | { name = "numpy" },
546 | { name = "pandas" },
547 | ]
548 | sdist = { url = "https://files.pythonhosted.org/packages/86/59/a451d7420a77ab0b98f7affa3a1d78a313d2f7281a57afb1a34bae8ab412/seaborn-0.13.2.tar.gz", hash = "sha256:93e60a40988f4d65e9f4885df477e2fdaff6b73a9ded434c1ab356dd57eefff7", size = 1457696 }
549 | wheels = [
550 | { url = "https://files.pythonhosted.org/packages/83/11/00d3c3dfc25ad54e731d91449895a79e4bf2384dc3ac01809010ba88f6d5/seaborn-0.13.2-py3-none-any.whl", hash = "sha256:636f8336facf092165e27924f223d3c62ca560b1f2bb5dff7ab7fad265361987", size = 294914 },
551 | ]
552 |
553 | [[package]]
554 | name = "six"
555 | version = "1.17.0"
556 | source = { registry = "https://pypi.org/simple" }
557 | sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 }
558 | wheels = [
559 | { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 },
560 | ]
561 |
562 | [[package]]
563 | name = "sniffio"
564 | version = "1.3.1"
565 | source = { registry = "https://pypi.org/simple" }
566 | sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
567 | wheels = [
568 | { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
569 | ]
570 |
571 | [[package]]
572 | name = "sty"
573 | version = "1.0.6"
574 | source = { registry = "https://pypi.org/simple" }
575 | sdist = { url = "https://files.pythonhosted.org/packages/76/6a/aad1817e60f07e5ebc111affee15d4dff9d324981005310566c40f08786c/sty-1.0.6.tar.gz", hash = "sha256:d43ecb71b7bad0b56d622cb219d0be303c16fcb4143b84d1465ded22e29baa00", size = 12217 }
576 | wheels = [
577 | { url = "https://files.pythonhosted.org/packages/ec/89/22b3b7f25f67d04690e3b565ca89062a6e7afb1e7124342d5b5b22e8f014/sty-1.0.6-py3-none-any.whl", hash = "sha256:2b1eba187b3961644f797f97177f939c109c916d3d3a2cb6784454d1f1ce4983", size = 12553 },
578 | ]
579 |
580 | [[package]]
581 | name = "termcolor"
582 | version = "3.0.1"
583 | source = { registry = "https://pypi.org/simple" }
584 | sdist = { url = "https://files.pythonhosted.org/packages/f8/b6/8e2aaa8aeb570b5cc955cd913b083d96c5447bbe27eaf330dfd7cc8e3329/termcolor-3.0.1.tar.gz", hash = "sha256:a6abd5c6e1284cea2934443ba806e70e5ec8fd2449021be55c280f8a3731b611", size = 12935 }
585 | wheels = [
586 | { url = "https://files.pythonhosted.org/packages/a6/7e/a574ccd49ad07e8b117407bac361f1e096b01f1b620365daf60ff702c936/termcolor-3.0.1-py3-none-any.whl", hash = "sha256:da1ed4ec8a5dc5b2e17476d859febdb3cccb612be1c36e64511a6f2485c10c69", size = 7157 },
587 | ]
588 |
589 | [[package]]
590 | name = "threadpoolctl"
591 | version = "3.6.0"
592 | source = { registry = "https://pypi.org/simple" }
593 | sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274 }
594 | wheels = [
595 | { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638 },
596 | ]
597 |
598 | [[package]]
599 | name = "tqdm"
600 | version = "4.67.1"
601 | source = { registry = "https://pypi.org/simple" }
602 | dependencies = [
603 | { name = "colorama", marker = "sys_platform == 'win32'" },
604 | ]
605 | sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 }
606 | wheels = [
607 | { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 },
608 | ]
609 |
610 | [[package]]
611 | name = "tzdata"
612 | version = "2025.2"
613 | source = { registry = "https://pypi.org/simple" }
614 | sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380 }
615 | wheels = [
616 | { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 },
617 | ]
618 |
619 | [[package]]
620 | name = "zstandard"
621 | version = "0.23.0"
622 | source = { registry = "https://pypi.org/simple" }
623 | dependencies = [
624 | { name = "cffi", marker = "platform_python_implementation == 'PyPy'" },
625 | ]
626 | sdist = { url = "https://files.pythonhosted.org/packages/ed/f6/2ac0287b442160a89d726b17a9184a4c615bb5237db763791a7fd16d9df1/zstandard-0.23.0.tar.gz", hash = "sha256:b2d8c62d08e7255f68f7a740bae85b3c9b8e5466baa9cbf7f57f1cde0ac6bc09", size = 681701 }
627 | wheels = [
628 | { url = "https://files.pythonhosted.org/packages/80/f1/8386f3f7c10261fe85fbc2c012fdb3d4db793b921c9abcc995d8da1b7a80/zstandard-0.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:576856e8594e6649aee06ddbfc738fec6a834f7c85bf7cadd1c53d4a58186ef9", size = 788975 },
629 | { url = "https://files.pythonhosted.org/packages/16/e8/cbf01077550b3e5dc86089035ff8f6fbbb312bc0983757c2d1117ebba242/zstandard-0.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38302b78a850ff82656beaddeb0bb989a0322a8bbb1bf1ab10c17506681d772a", size = 633448 },
630 | { url = "https://files.pythonhosted.org/packages/06/27/4a1b4c267c29a464a161aeb2589aff212b4db653a1d96bffe3598f3f0d22/zstandard-0.23.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2240ddc86b74966c34554c49d00eaafa8200a18d3a5b6ffbf7da63b11d74ee2", size = 4945269 },
631 | { url = "https://files.pythonhosted.org/packages/7c/64/d99261cc57afd9ae65b707e38045ed8269fbdae73544fd2e4a4d50d0ed83/zstandard-0.23.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ef230a8fd217a2015bc91b74f6b3b7d6522ba48be29ad4ea0ca3a3775bf7dd5", size = 5306228 },
632 | { url = "https://files.pythonhosted.org/packages/7a/cf/27b74c6f22541f0263016a0fd6369b1b7818941de639215c84e4e94b2a1c/zstandard-0.23.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:774d45b1fac1461f48698a9d4b5fa19a69d47ece02fa469825b442263f04021f", size = 5336891 },
633 | { url = "https://files.pythonhosted.org/packages/fa/18/89ac62eac46b69948bf35fcd90d37103f38722968e2981f752d69081ec4d/zstandard-0.23.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f77fa49079891a4aab203d0b1744acc85577ed16d767b52fc089d83faf8d8ed", size = 5436310 },
634 | { url = "https://files.pythonhosted.org/packages/a8/a8/5ca5328ee568a873f5118d5b5f70d1f36c6387716efe2e369010289a5738/zstandard-0.23.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac184f87ff521f4840e6ea0b10c0ec90c6b1dcd0bad2f1e4a9a1b4fa177982ea", size = 4859912 },
635 | { url = "https://files.pythonhosted.org/packages/ea/ca/3781059c95fd0868658b1cf0440edd832b942f84ae60685d0cfdb808bca1/zstandard-0.23.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c363b53e257246a954ebc7c488304b5592b9c53fbe74d03bc1c64dda153fb847", size = 4936946 },
636 | { url = "https://files.pythonhosted.org/packages/ce/11/41a58986f809532742c2b832c53b74ba0e0a5dae7e8ab4642bf5876f35de/zstandard-0.23.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e7792606d606c8df5277c32ccb58f29b9b8603bf83b48639b7aedf6df4fe8171", size = 5466994 },
637 | { url = "https://files.pythonhosted.org/packages/83/e3/97d84fe95edd38d7053af05159465d298c8b20cebe9ccb3d26783faa9094/zstandard-0.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a0817825b900fcd43ac5d05b8b3079937073d2b1ff9cf89427590718b70dd840", size = 4848681 },
638 | { url = "https://files.pythonhosted.org/packages/6e/99/cb1e63e931de15c88af26085e3f2d9af9ce53ccafac73b6e48418fd5a6e6/zstandard-0.23.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9da6bc32faac9a293ddfdcb9108d4b20416219461e4ec64dfea8383cac186690", size = 4694239 },
639 | { url = "https://files.pythonhosted.org/packages/ab/50/b1e703016eebbc6501fc92f34db7b1c68e54e567ef39e6e59cf5fb6f2ec0/zstandard-0.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fd7699e8fd9969f455ef2926221e0233f81a2542921471382e77a9e2f2b57f4b", size = 5200149 },
640 | { url = "https://files.pythonhosted.org/packages/aa/e0/932388630aaba70197c78bdb10cce2c91fae01a7e553b76ce85471aec690/zstandard-0.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d477ed829077cd945b01fc3115edd132c47e6540ddcd96ca169facff28173057", size = 5655392 },
641 | { url = "https://files.pythonhosted.org/packages/02/90/2633473864f67a15526324b007a9f96c96f56d5f32ef2a56cc12f9548723/zstandard-0.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ce8b52c5987b3e34d5674b0ab529a4602b632ebab0a93b07bfb4dfc8f8a33", size = 5191299 },
642 | { url = "https://files.pythonhosted.org/packages/b0/4c/315ca5c32da7e2dc3455f3b2caee5c8c2246074a61aac6ec3378a97b7136/zstandard-0.23.0-cp313-cp313-win32.whl", hash = "sha256:a9b07268d0c3ca5c170a385a0ab9fb7fdd9f5fd866be004c4ea39e44edce47dd", size = 430862 },
643 | { url = "https://files.pythonhosted.org/packages/a2/bf/c6aaba098e2d04781e8f4f7c0ba3c7aa73d00e4c436bcc0cf059a66691d1/zstandard-0.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:f3513916e8c645d0610815c257cbfd3242adfd5c4cfa78be514e5a3ebb42a41b", size = 495578 },
644 | ]
645 |
--------------------------------------------------------------------------------