From 3171e9b4b07ce3e87d138a251ae4d064de84b19c Mon Sep 17 00:00:00 2001 From: ilya_kim Date: Sat, 27 Sep 2025 23:44:54 +0300 Subject: [PATCH 01/12] appdate all --- .flake8 | 3 +- .idea/PythonProject9.iml | 2 +- .idea/misc.xml | 2 +- README.md | 19 + data/sample_data.py | 72 ++ env.example | 3 + logs/transaction_analyzer.log | 1073 ++++++++++++++++++++++ poetry.lock | 1416 ++++++++++++++++++++++++----- pyproject.toml | 49 +- requirements-dev.txt | 9 - requirements.txt | 17 +- run_app.py | 54 ++ src/__init__.py | 0 src/api_client.py | 214 +++++ src/config.py | 42 + src/logs/transaction_analyzer.log | 60 ++ src/main.py | 192 +++- src/reports.py | 308 +++++-- src/services.py | 267 ++++-- src/utils.py | 337 +++++-- src/views.py | 276 ++++-- test_app.py | 181 ++++ tests/__init__.py | 0 tests/conftest.py | 38 + tests/test_api_client.py | 47 + tests/test_services.py | 3 +- tests/test_utils.py | 87 ++ tests/test_views.py | 127 ++- 28 files changed, 4300 insertions(+), 598 deletions(-) create mode 100644 README.md create mode 100644 data/sample_data.py create mode 100644 env.example create mode 100644 logs/transaction_analyzer.log delete mode 100644 requirements-dev.txt create mode 100644 run_app.py create mode 100644 src/__init__.py create mode 100644 src/api_client.py create mode 100644 src/config.py create mode 100644 src/logs/transaction_analyzer.log create mode 100644 test_app.py create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/test_api_client.py create mode 100644 tests/test_utils.py diff --git a/.flake8 b/.flake8 index 68847e2..3bcf684 100644 --- a/.flake8 +++ b/.flake8 @@ -1,3 +1,4 @@ [flake8] max-line-length = 119 -exclude = .git, __pycache__, venv \ No newline at end of file +exclude = .git,__pycache__,venv,.venv +ignore = E203,W503 \ No newline at end of file diff --git a/.idea/PythonProject9.iml b/.idea/PythonProject9.iml index 0b63526..ee944cf 100644 --- a/.idea/PythonProject9.iml +++ b/.idea/PythonProject9.iml @@ -4,7 +4,7 @@ - + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index c110604..48c6ff1 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -22,5 +22,5 @@ - + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..7466ee3 --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# Transaction Analyzer + +Приложение для анализа банковских транзакций с поддержкой веб-интерфейса, отчетов и аналитики. + +## Функциональность + +- 📊 Анализ транзакций из Excel-файлов +- 🌐 Генерация JSON данных для веб-страниц +- 📈 Отчеты и аналитика +- 💰 Расчет кешбэка и инвесткопилки +- 🔍 Поиск и фильтрация транзакций +- 💹 Интеграция с API курсов валют и акций + +## Установка + +1. Клонируйте репозиторий: +```bash +git clone +cd transaction-analyzer \ No newline at end of file diff --git a/data/sample_data.py b/data/sample_data.py new file mode 100644 index 0000000..ba9459c --- /dev/null +++ b/data/sample_data.py @@ -0,0 +1,72 @@ +""" +Скрипт для генерации тестовых данных +""" + +import pandas as pd +import numpy as np +from datetime import datetime, timedelta +import random + + +def generate_sample_data(num_records: int = 1000) -> pd.DataFrame: + """Генерация тестовых данных транзакций""" + + # Категории транзакций + categories = [ + 'Супермаркеты', 'Фастфуд', 'Транспорт', 'Развлечения', + 'Одежда', 'Электроника', 'Здоровье', 'Образование', + 'Переводы', 'Наличные', 'Зарплата', 'Инвестиции' + ] + + # Статусы операций + statuses = ['OK', 'FAILED', 'PENDING'] + + # Номера карт + cards = ['1234567812345814', '1234567812347512', '1234567812340923'] + + # Генерация дат + start_date = datetime(2023, 1, 1) + end_date = datetime(2023, 12, 31) + date_range = (end_date - start_date).days + + data = [] + for i in range(num_records): + # Случайная дата в 2023 году + random_days = random.randint(0, date_range) + operation_date = start_date + timedelta(days=random_days) + payment_date = operation_date + timedelta(days=random.randint(0, 30)) + + # Случайная сумма (большинство - расходы, некоторые - доходы) + is_income = random.random() < 0.2 # 20% доходы + amount = round(random.uniform(100, 50000), 2) + if is_income: + amount = -amount + + transaction = { + 'Дата операции': operation_date.strftime('%d.%m.%Y'), + 'Дата платежа': payment_date.strftime('%d.%m.%Y'), + 'Номер карты': random.choice(cards), + 'Статус': random.choices(statuses, weights=[0.85, 0.1, 0.05])[0], + 'Сумма операции': amount, + 'Валюта операции': 'RUB', + 'Сумма платежа': amount, + 'Валюта платежа': 'RUB', + 'Кешбэк': round(abs(amount) * 0.01, 2) if amount > 0 and random.random() < 0.7 else 0, + 'Категория': random.choice(categories), + 'MCC': random.randint(1000, 9999), + 'Описание': f'Транзакция #{i + 1}', + 'Бонусы (включая кешбэк)': round(abs(amount) * 0.005, 2) if amount > 0 else 0, + 'Округление на «Инвесткопилку»': random.randint(0, 50), + 'Сумма операции с округлением': round(amount / 50) * 50 if amount > 0 else amount + } + + data.append(transaction) + + return pd.DataFrame(data) + + +if __name__ == "__main__": + # Генерация тестовых данных + df = generate_sample_data(1000) + df.to_excel('data/operations.xlsx', index=False) + print("Тестовые данные сохранены в data/operations.xlsx") diff --git a/env.example b/env.example new file mode 100644 index 0000000..11f8414 --- /dev/null +++ b/env.example @@ -0,0 +1,3 @@ +ALPHA_VANTAGE_API_KEY=your_alpha_vantage_key_here +EXCHANGERATE_API_KEY=your_exchangerate_api_key_here +CURRENCY_API_KEY=your_currency_api_key_here diff --git a/logs/transaction_analyzer.log b/logs/transaction_analyzer.log new file mode 100644 index 0000000..c67d25c --- /dev/null +++ b/logs/transaction_analyzer.log @@ -0,0 +1,1073 @@ +2025-09-27 23:14:32,365 - __main__ - INFO - ... +2025-09-27 23:14:32,366 - src.utils - INFO - data/operations.xlsx +2025-09-27 23:14:32,622 - src.utils - INFO - 1000 +2025-09-27 23:14:32,623 - __main__ - INFO - +2025-09-27 23:14:32,623 - src.utils - INFO - data/operations.xlsx +2025-09-27 23:14:32,747 - src.utils - INFO - 1000 +2025-09-27 23:14:32,748 - src.utils - INFO - 0 2025-09-01 - 2025-09-27 +2025-09-27 23:14:34,849 - src.utils - INFO - data/operations.xlsx +2025-09-27 23:14:34,978 - src.utils - INFO - 1000 +2025-09-27 23:14:34,979 - src.utils - INFO - 0 2025-09-01 - 2025-09-27 +2025-09-27 23:14:34,980 - src.api_client - INFO - Using cached currency rates +2025-09-27 23:14:34,981 - src.api_client - INFO - Using cached stock prices +2025-09-27 23:23:25,440 - __main__ - INFO - ... +2025-09-27 23:23:25,440 - src.utils - INFO - data/operations.xlsx +2025-09-27 23:23:25,684 - src.utils - INFO - 1000 +2025-09-27 23:23:25,685 - __main__ - INFO - +2025-09-27 23:23:25,685 - src.utils - INFO - data/operations.xlsx +2025-09-27 23:23:25,812 - src.utils - INFO - 1000 +2025-09-27 23:23:25,813 - src.utils - INFO - 0 2025-09-01 - 2025-09-27 +2025-09-27 23:23:28,055 - src.api_client - INFO - Using fallback stock prices +2025-09-27 23:23:28,057 - src.utils - INFO - data/operations.xlsx +2025-09-27 23:23:28,182 - src.utils - INFO - 1000 +2025-09-27 23:23:28,183 - src.utils - INFO - 0 2025-09-01 - 2025-09-27 +2025-09-27 23:23:28,184 - src.api_client - INFO - Using cached currency rates +2025-09-27 23:23:29,450 - src.api_client - INFO - Using fallback stock prices +2025-09-27 23:30:07,421 - __main__ - INFO - ... +2025-09-27 23:30:07,422 - src.utils - INFO - data/operations.xlsx +2025-09-27 23:30:07,670 - src.utils - INFO - 1000 +2025-09-27 23:30:07,671 - __main__ - INFO - +2025-09-27 23:30:07,671 - __main__ - ERROR - : name 'ReportGenerator' is not defined +2025-09-27 23:38:33,025 - __main__ - INFO - ... +2025-09-27 23:38:33,026 - src.utils - INFO - data/operations.xlsx +2025-09-27 23:38:33,269 - src.utils - INFO - 1000 +2025-09-27 23:38:33,270 - __main__ - INFO - +2025-09-27 23:38:33,493 - src.utils - INFO - data/operations.xlsx +2025-09-27 23:38:33,620 - src.utils - INFO - 1000 +2025-09-27 23:38:33,622 - src.utils - INFO - 0 2025-09-01 - 2025-09-27 +2025-09-27 23:38:37,571 - src.api_client - INFO - Using fallback stock prices +2025-09-27 23:38:37,573 - src.utils - INFO - data/operations.xlsx +2025-09-27 23:38:37,700 - src.utils - INFO - 1000 +2025-09-27 23:38:37,701 - src.utils - INFO - 0 2025-09-01 - 2025-09-27 +2025-09-27 23:38:37,703 - src.api_client - INFO - Using cached currency rates +2025-09-27 23:38:41,146 - src.api_client - INFO - Using fallback stock prices +2025-09-27 23:39:37,249 - __main__ - INFO - ... +2025-09-27 23:39:37,249 - src.utils - INFO - data/operations.xlsx +2025-09-27 23:39:37,518 - src.utils - INFO - 1000 +2025-09-27 23:39:37,518 - __main__ - INFO - +2025-09-27 23:39:37,741 - src.utils - INFO - data/operations.xlsx +2025-09-27 23:39:37,869 - src.utils - INFO - 1000 +2025-09-27 23:39:37,871 - src.utils - INFO - 0 2025-09-01 - 2025-09-27 +2025-09-27 23:39:42,148 - src.api_client - INFO - Using fallback stock prices +2025-09-27 23:39:42,150 - src.utils - INFO - data/operations.xlsx +2025-09-27 23:39:42,279 - src.utils - INFO - 1000 +2025-09-27 23:39:42,280 - src.utils - INFO - 0 2025-09-01 - 2025-09-27 +2025-09-27 23:39:42,282 - src.api_client - INFO - Using cached currency rates +2025-09-27 23:39:46,372 - src.api_client - INFO - Using fallback stock prices +2025-09-27 23:39:57,888 - __main__ - INFO - ... +2025-09-27 23:39:57,889 - src.utils - INFO - data/operations.xlsx +2025-09-27 23:39:58,139 - src.utils - INFO - 1000 +2025-09-27 23:39:58,140 - __main__ - INFO - +2025-09-27 23:39:58,145 - src.reports - INFO - reports/spending_by_category_20250927_233958.json +2025-09-27 23:39:58,148 - src.reports - INFO - reports/spending_by_weekday_20250927_233958.json +2025-09-27 23:39:58,150 - src.reports - INFO - reports/spending_by_workday_20250927_233958.json +2025-09-27 23:39:58,151 - src.reports - INFO - reports/monthly_summary_20250927_233958.json +2025-09-27 23:40:18,779 - __main__ - INFO - ... +2025-09-27 23:40:18,779 - src.utils - INFO - data/operations.xlsx +2025-09-27 23:40:19,028 - src.utils - INFO - 1000 +2025-09-27 23:40:19,028 - __main__ - INFO - +2025-09-27 23:40:19,029 - src.services - INFO - profitable_cashback_categories +2025-09-27 23:40:19,031 - src.services - WARNING - 9/2025 +2025-09-27 23:40:19,031 - src.services - INFO - profitable_cashback_categories +2025-09-27 23:40:19,038 - src.services - INFO - investment_bank +2025-09-27 23:40:19,038 - src.services - WARNING - +2025-09-27 23:40:19,038 - src.services - WARNING - +2025-09-27 23:40:19,038 - src.services - WARNING - +2025-09-27 23:40:19,038 - src.services - WARNING - +2025-09-27 23:40:19,038 - src.services - WARNING - +2025-09-27 23:40:19,038 - src.services - WARNING - +2025-09-27 23:40:19,038 - src.services - WARNING - +2025-09-27 23:40:19,038 - src.services - WARNING - +2025-09-27 23:40:19,039 - src.services - WARNING - +2025-09-27 23:40:19,039 - src.services - WARNING - +2025-09-27 23:40:19,039 - src.services - WARNING - +2025-09-27 23:40:19,039 - src.services - WARNING - +2025-09-27 23:40:19,039 - src.services - WARNING - +2025-09-27 23:40:19,039 - src.services - WARNING - +2025-09-27 23:40:19,039 - src.services - WARNING - +2025-09-27 23:40:19,039 - src.services - WARNING - +2025-09-27 23:40:19,039 - src.services - WARNING - +2025-09-27 23:40:19,039 - src.services - WARNING - +2025-09-27 23:40:19,039 - src.services - WARNING - +2025-09-27 23:40:19,039 - src.services - WARNING - +2025-09-27 23:40:19,039 - src.services - WARNING - +2025-09-27 23:40:19,040 - src.services - WARNING - +2025-09-27 23:40:19,040 - src.services - WARNING - +2025-09-27 23:40:19,040 - src.services - WARNING - +2025-09-27 23:40:19,040 - src.services - WARNING - +2025-09-27 23:40:19,040 - src.services - WARNING - +2025-09-27 23:40:19,040 - src.services - WARNING - +2025-09-27 23:40:19,040 - src.services - WARNING - +2025-09-27 23:40:19,040 - src.services - WARNING - +2025-09-27 23:40:19,040 - src.services - WARNING - +2025-09-27 23:40:19,040 - src.services - WARNING - +2025-09-27 23:40:19,040 - src.services - WARNING - +2025-09-27 23:40:19,040 - src.services - WARNING - +2025-09-27 23:40:19,040 - src.services - WARNING - +2025-09-27 23:40:19,040 - src.services - WARNING - +2025-09-27 23:40:19,041 - src.services - WARNING - +2025-09-27 23:40:19,041 - src.services - WARNING - +2025-09-27 23:40:19,041 - src.services - WARNING - +2025-09-27 23:40:19,041 - src.services - WARNING - +2025-09-27 23:40:19,041 - src.services - WARNING - +2025-09-27 23:40:19,041 - src.services - WARNING - +2025-09-27 23:40:19,041 - src.services - WARNING - +2025-09-27 23:40:19,041 - src.services - WARNING - +2025-09-27 23:40:19,041 - src.services - WARNING - +2025-09-27 23:40:19,041 - src.services - WARNING - +2025-09-27 23:40:19,041 - src.services - WARNING - +2025-09-27 23:40:19,041 - src.services - WARNING - +2025-09-27 23:40:19,041 - src.services - WARNING - +2025-09-27 23:40:19,041 - src.services - WARNING - +2025-09-27 23:40:19,041 - src.services - WARNING - +2025-09-27 23:40:19,042 - src.services - WARNING - +2025-09-27 23:40:19,042 - src.services - WARNING - +2025-09-27 23:40:19,042 - src.services - WARNING - +2025-09-27 23:40:19,042 - src.services - WARNING - +2025-09-27 23:40:19,042 - src.services - WARNING - +2025-09-27 23:40:19,042 - src.services - WARNING - +2025-09-27 23:40:19,042 - src.services - WARNING - +2025-09-27 23:40:19,042 - src.services - WARNING - +2025-09-27 23:40:19,042 - src.services - WARNING - +2025-09-27 23:40:19,042 - src.services - WARNING - +2025-09-27 23:40:19,042 - src.services - WARNING - +2025-09-27 23:40:19,042 - src.services - WARNING - +2025-09-27 23:40:19,042 - src.services - WARNING - +2025-09-27 23:40:19,042 - src.services - WARNING - +2025-09-27 23:40:19,042 - src.services - WARNING - +2025-09-27 23:40:19,043 - src.services - WARNING - +2025-09-27 23:40:19,043 - src.services - WARNING - +2025-09-27 23:40:19,043 - src.services - WARNING - +2025-09-27 23:40:19,043 - src.services - WARNING - +2025-09-27 23:40:19,043 - src.services - WARNING - +2025-09-27 23:40:19,043 - src.services - WARNING - +2025-09-27 23:40:19,043 - src.services - WARNING - +2025-09-27 23:40:19,043 - src.services - WARNING - +2025-09-27 23:40:19,043 - src.services - WARNING - +2025-09-27 23:40:19,043 - src.services - WARNING - +2025-09-27 23:40:19,043 - src.services - WARNING - +2025-09-27 23:40:19,043 - src.services - WARNING - +2025-09-27 23:40:19,043 - src.services - WARNING - +2025-09-27 23:40:19,043 - src.services - WARNING - +2025-09-27 23:40:19,043 - src.services - WARNING - +2025-09-27 23:40:19,044 - src.services - WARNING - +2025-09-27 23:40:19,044 - src.services - WARNING - +2025-09-27 23:40:19,044 - src.services - WARNING - +2025-09-27 23:40:19,044 - src.services - WARNING - +2025-09-27 23:40:19,044 - src.services - WARNING - +2025-09-27 23:40:19,044 - src.services - WARNING - +2025-09-27 23:40:19,044 - src.services - WARNING - +2025-09-27 23:40:19,044 - src.services - WARNING - +2025-09-27 23:40:19,044 - src.services - WARNING - +2025-09-27 23:40:19,044 - src.services - WARNING - +2025-09-27 23:40:19,044 - src.services - WARNING - +2025-09-27 23:40:19,044 - src.services - WARNING - +2025-09-27 23:40:19,044 - src.services - WARNING - +2025-09-27 23:40:19,044 - src.services - WARNING - +2025-09-27 23:40:19,044 - src.services - WARNING - +2025-09-27 23:40:19,044 - src.services - WARNING - +2025-09-27 23:40:19,045 - src.services - WARNING - +2025-09-27 23:40:19,045 - src.services - WARNING - +2025-09-27 23:40:19,045 - src.services - WARNING - +2025-09-27 23:40:19,045 - src.services - WARNING - +2025-09-27 23:40:19,045 - src.services - WARNING - +2025-09-27 23:40:19,045 - src.services - WARNING - +2025-09-27 23:40:19,045 - src.services - WARNING - +2025-09-27 23:40:19,045 - src.services - WARNING - +2025-09-27 23:40:19,045 - src.services - WARNING - +2025-09-27 23:40:19,045 - src.services - WARNING - +2025-09-27 23:40:19,045 - src.services - WARNING - +2025-09-27 23:40:19,046 - src.services - WARNING - +2025-09-27 23:40:19,046 - src.services - WARNING - +2025-09-27 23:40:19,046 - src.services - WARNING - +2025-09-27 23:40:19,046 - src.services - WARNING - +2025-09-27 23:40:19,046 - src.services - WARNING - +2025-09-27 23:40:19,046 - src.services - WARNING - +2025-09-27 23:40:19,046 - src.services - WARNING - +2025-09-27 23:40:19,046 - src.services - WARNING - +2025-09-27 23:40:19,046 - src.services - WARNING - +2025-09-27 23:40:19,046 - src.services - WARNING - +2025-09-27 23:40:19,046 - src.services - WARNING - +2025-09-27 23:40:19,046 - src.services - WARNING - +2025-09-27 23:40:19,046 - src.services - WARNING - +2025-09-27 23:40:19,046 - src.services - WARNING - +2025-09-27 23:40:19,046 - src.services - WARNING - +2025-09-27 23:40:19,047 - src.services - WARNING - +2025-09-27 23:40:19,047 - src.services - WARNING - +2025-09-27 23:40:19,047 - src.services - WARNING - +2025-09-27 23:40:19,047 - src.services - WARNING - +2025-09-27 23:40:19,047 - src.services - WARNING - +2025-09-27 23:40:19,047 - src.services - WARNING - +2025-09-27 23:40:19,047 - src.services - WARNING - +2025-09-27 23:40:19,047 - src.services - WARNING - +2025-09-27 23:40:19,047 - src.services - WARNING - +2025-09-27 23:40:19,047 - src.services - WARNING - +2025-09-27 23:40:19,047 - src.services - WARNING - +2025-09-27 23:40:19,047 - src.services - WARNING - +2025-09-27 23:40:19,047 - src.services - WARNING - +2025-09-27 23:40:19,047 - src.services - WARNING - +2025-09-27 23:40:19,047 - src.services - WARNING - +2025-09-27 23:40:19,048 - src.services - WARNING - +2025-09-27 23:40:19,048 - src.services - WARNING - +2025-09-27 23:40:19,048 - src.services - WARNING - +2025-09-27 23:40:19,048 - src.services - WARNING - +2025-09-27 23:40:19,048 - src.services - WARNING - +2025-09-27 23:40:19,048 - src.services - WARNING - +2025-09-27 23:40:19,048 - src.services - WARNING - +2025-09-27 23:40:19,048 - src.services - WARNING - +2025-09-27 23:40:19,048 - src.services - WARNING - +2025-09-27 23:40:19,048 - src.services - WARNING - +2025-09-27 23:40:19,048 - src.services - WARNING - +2025-09-27 23:40:19,048 - src.services - WARNING - +2025-09-27 23:40:19,048 - src.services - WARNING - +2025-09-27 23:40:19,048 - src.services - WARNING - +2025-09-27 23:40:19,049 - src.services - WARNING - +2025-09-27 23:40:19,049 - src.services - WARNING - +2025-09-27 23:40:19,049 - src.services - WARNING - +2025-09-27 23:40:19,049 - src.services - WARNING - +2025-09-27 23:40:19,049 - src.services - WARNING - +2025-09-27 23:40:19,049 - src.services - WARNING - +2025-09-27 23:40:19,049 - src.services - WARNING - +2025-09-27 23:40:19,049 - src.services - WARNING - +2025-09-27 23:40:19,049 - src.services - WARNING - +2025-09-27 23:40:19,049 - src.services - WARNING - +2025-09-27 23:40:19,049 - src.services - WARNING - +2025-09-27 23:40:19,049 - src.services - WARNING - +2025-09-27 23:40:19,049 - src.services - WARNING - +2025-09-27 23:40:19,049 - src.services - WARNING - +2025-09-27 23:40:19,049 - src.services - WARNING - +2025-09-27 23:40:19,050 - src.services - WARNING - +2025-09-27 23:40:19,050 - src.services - WARNING - +2025-09-27 23:40:19,050 - src.services - WARNING - +2025-09-27 23:40:19,050 - src.services - WARNING - +2025-09-27 23:40:19,050 - src.services - WARNING - +2025-09-27 23:40:19,050 - src.services - WARNING - +2025-09-27 23:40:19,050 - src.services - WARNING - +2025-09-27 23:40:19,050 - src.services - WARNING - +2025-09-27 23:40:19,050 - src.services - WARNING - +2025-09-27 23:40:19,050 - src.services - WARNING - +2025-09-27 23:40:19,050 - src.services - WARNING - +2025-09-27 23:40:19,050 - src.services - WARNING - +2025-09-27 23:40:19,050 - src.services - WARNING - +2025-09-27 23:40:19,051 - src.services - WARNING - +2025-09-27 23:40:19,051 - src.services - WARNING - +2025-09-27 23:40:19,051 - src.services - WARNING - +2025-09-27 23:40:19,051 - src.services - WARNING - +2025-09-27 23:40:19,051 - src.services - WARNING - +2025-09-27 23:40:19,051 - src.services - WARNING - +2025-09-27 23:40:19,051 - src.services - WARNING - +2025-09-27 23:40:19,051 - src.services - WARNING - +2025-09-27 23:40:19,051 - src.services - WARNING - +2025-09-27 23:40:19,051 - src.services - WARNING - +2025-09-27 23:40:19,051 - src.services - WARNING - +2025-09-27 23:40:19,051 - src.services - WARNING - +2025-09-27 23:40:19,051 - src.services - WARNING - +2025-09-27 23:40:19,051 - src.services - WARNING - +2025-09-27 23:40:19,052 - src.services - WARNING - +2025-09-27 23:40:19,052 - src.services - WARNING - +2025-09-27 23:40:19,052 - src.services - WARNING - +2025-09-27 23:40:19,052 - src.services - WARNING - +2025-09-27 23:40:19,052 - src.services - WARNING - +2025-09-27 23:40:19,052 - src.services - WARNING - +2025-09-27 23:40:19,052 - src.services - WARNING - +2025-09-27 23:40:19,052 - src.services - WARNING - +2025-09-27 23:40:19,052 - src.services - WARNING - +2025-09-27 23:40:19,052 - src.services - WARNING - +2025-09-27 23:40:19,052 - src.services - WARNING - +2025-09-27 23:40:19,052 - src.services - WARNING - +2025-09-27 23:40:19,052 - src.services - WARNING - +2025-09-27 23:40:19,052 - src.services - WARNING - +2025-09-27 23:40:19,052 - src.services - WARNING - +2025-09-27 23:40:19,053 - src.services - WARNING - +2025-09-27 23:40:19,053 - src.services - WARNING - +2025-09-27 23:40:19,053 - src.services - WARNING - +2025-09-27 23:40:19,053 - src.services - WARNING - +2025-09-27 23:40:19,053 - src.services - WARNING - +2025-09-27 23:40:19,053 - src.services - WARNING - +2025-09-27 23:40:19,053 - src.services - WARNING - +2025-09-27 23:40:19,053 - src.services - WARNING - +2025-09-27 23:40:19,053 - src.services - WARNING - +2025-09-27 23:40:19,053 - src.services - WARNING - +2025-09-27 23:40:19,053 - src.services - WARNING - +2025-09-27 23:40:19,053 - src.services - WARNING - +2025-09-27 23:40:19,053 - src.services - WARNING - +2025-09-27 23:40:19,053 - src.services - WARNING - +2025-09-27 23:40:19,053 - src.services - WARNING - +2025-09-27 23:40:19,053 - src.services - WARNING - +2025-09-27 23:40:19,054 - src.services - WARNING - +2025-09-27 23:40:19,054 - src.services - WARNING - +2025-09-27 23:40:19,054 - src.services - WARNING - +2025-09-27 23:40:19,054 - src.services - WARNING - +2025-09-27 23:40:19,054 - src.services - WARNING - +2025-09-27 23:40:19,054 - src.services - WARNING - +2025-09-27 23:40:19,054 - src.services - WARNING - +2025-09-27 23:40:19,054 - src.services - WARNING - +2025-09-27 23:40:19,054 - src.services - WARNING - +2025-09-27 23:40:19,054 - src.services - WARNING - +2025-09-27 23:40:19,054 - src.services - WARNING - +2025-09-27 23:40:19,054 - src.services - WARNING - +2025-09-27 23:40:19,054 - src.services - WARNING - +2025-09-27 23:40:19,054 - src.services - WARNING - +2025-09-27 23:40:19,054 - src.services - WARNING - +2025-09-27 23:40:19,055 - src.services - WARNING - +2025-09-27 23:40:19,055 - src.services - WARNING - +2025-09-27 23:40:19,055 - src.services - WARNING - +2025-09-27 23:40:19,055 - src.services - WARNING - +2025-09-27 23:40:19,055 - src.services - WARNING - +2025-09-27 23:40:19,055 - src.services - WARNING - +2025-09-27 23:40:19,055 - src.services - WARNING - +2025-09-27 23:40:19,055 - src.services - WARNING - +2025-09-27 23:40:19,055 - src.services - WARNING - +2025-09-27 23:40:19,055 - src.services - WARNING - +2025-09-27 23:40:19,055 - src.services - WARNING - +2025-09-27 23:40:19,055 - src.services - WARNING - +2025-09-27 23:40:19,055 - src.services - WARNING - +2025-09-27 23:40:19,055 - src.services - WARNING - +2025-09-27 23:40:19,055 - src.services - WARNING - +2025-09-27 23:40:19,056 - src.services - WARNING - +2025-09-27 23:40:19,056 - src.services - WARNING - +2025-09-27 23:40:19,056 - src.services - WARNING - +2025-09-27 23:40:19,056 - src.services - WARNING - +2025-09-27 23:40:19,056 - src.services - WARNING - +2025-09-27 23:40:19,056 - src.services - WARNING - +2025-09-27 23:40:19,056 - src.services - WARNING - +2025-09-27 23:40:19,056 - src.services - WARNING - +2025-09-27 23:40:19,056 - src.services - WARNING - +2025-09-27 23:40:19,056 - src.services - WARNING - +2025-09-27 23:40:19,056 - src.services - WARNING - +2025-09-27 23:40:19,056 - src.services - WARNING - +2025-09-27 23:40:19,056 - src.services - WARNING - +2025-09-27 23:40:19,057 - src.services - WARNING - +2025-09-27 23:40:19,057 - src.services - WARNING - +2025-09-27 23:40:19,057 - src.services - WARNING - +2025-09-27 23:40:19,057 - src.services - WARNING - +2025-09-27 23:40:19,057 - src.services - WARNING - +2025-09-27 23:40:19,057 - src.services - WARNING - +2025-09-27 23:40:19,057 - src.services - WARNING - +2025-09-27 23:40:19,057 - src.services - WARNING - +2025-09-27 23:40:19,057 - src.services - WARNING - +2025-09-27 23:40:19,057 - src.services - WARNING - +2025-09-27 23:40:19,057 - src.services - WARNING - +2025-09-27 23:40:19,057 - src.services - WARNING - +2025-09-27 23:40:19,057 - src.services - WARNING - +2025-09-27 23:40:19,057 - src.services - WARNING - +2025-09-27 23:40:19,057 - src.services - WARNING - +2025-09-27 23:40:19,058 - src.services - WARNING - +2025-09-27 23:40:19,058 - src.services - WARNING - +2025-09-27 23:40:19,058 - src.services - WARNING - +2025-09-27 23:40:19,058 - src.services - WARNING - +2025-09-27 23:40:19,058 - src.services - WARNING - +2025-09-27 23:40:19,058 - src.services - WARNING - +2025-09-27 23:40:19,058 - src.services - WARNING - +2025-09-27 23:40:19,058 - src.services - WARNING - +2025-09-27 23:40:19,058 - src.services - WARNING - +2025-09-27 23:40:19,058 - src.services - WARNING - +2025-09-27 23:40:19,058 - src.services - WARNING - +2025-09-27 23:40:19,058 - src.services - WARNING - +2025-09-27 23:40:19,058 - src.services - WARNING - +2025-09-27 23:40:19,058 - src.services - WARNING - +2025-09-27 23:40:19,059 - src.services - WARNING - +2025-09-27 23:40:19,059 - src.services - WARNING - +2025-09-27 23:40:19,059 - src.services - WARNING - +2025-09-27 23:40:19,059 - src.services - WARNING - +2025-09-27 23:40:19,059 - src.services - WARNING - +2025-09-27 23:40:19,059 - src.services - WARNING - +2025-09-27 23:40:19,059 - src.services - WARNING - +2025-09-27 23:40:19,059 - src.services - WARNING - +2025-09-27 23:40:19,059 - src.services - WARNING - +2025-09-27 23:40:19,059 - src.services - WARNING - +2025-09-27 23:40:19,059 - src.services - WARNING - +2025-09-27 23:40:19,059 - src.services - WARNING - +2025-09-27 23:40:19,059 - src.services - WARNING - +2025-09-27 23:40:19,059 - src.services - WARNING - +2025-09-27 23:40:19,059 - src.services - WARNING - +2025-09-27 23:40:19,060 - src.services - WARNING - +2025-09-27 23:40:19,060 - src.services - WARNING - +2025-09-27 23:40:19,060 - src.services - WARNING - +2025-09-27 23:40:19,060 - src.services - WARNING - +2025-09-27 23:40:19,060 - src.services - WARNING - +2025-09-27 23:40:19,060 - src.services - WARNING - +2025-09-27 23:40:19,060 - src.services - WARNING - +2025-09-27 23:40:19,060 - src.services - WARNING - +2025-09-27 23:40:19,060 - src.services - WARNING - +2025-09-27 23:40:19,060 - src.services - WARNING - +2025-09-27 23:40:19,060 - src.services - WARNING - +2025-09-27 23:40:19,060 - src.services - WARNING - +2025-09-27 23:40:19,060 - src.services - WARNING - +2025-09-27 23:40:19,060 - src.services - WARNING - +2025-09-27 23:40:19,061 - src.services - WARNING - +2025-09-27 23:40:19,061 - src.services - WARNING - +2025-09-27 23:40:19,061 - src.services - WARNING - +2025-09-27 23:40:19,061 - src.services - WARNING - +2025-09-27 23:40:19,061 - src.services - WARNING - +2025-09-27 23:40:19,061 - src.services - WARNING - +2025-09-27 23:40:19,061 - src.services - WARNING - +2025-09-27 23:40:19,061 - src.services - WARNING - +2025-09-27 23:40:19,061 - src.services - WARNING - +2025-09-27 23:40:19,061 - src.services - WARNING - +2025-09-27 23:40:19,061 - src.services - WARNING - +2025-09-27 23:40:19,061 - src.services - WARNING - +2025-09-27 23:40:19,061 - src.services - WARNING - +2025-09-27 23:40:19,061 - src.services - WARNING - +2025-09-27 23:40:19,061 - src.services - WARNING - +2025-09-27 23:40:19,062 - src.services - WARNING - +2025-09-27 23:40:19,062 - src.services - WARNING - +2025-09-27 23:40:19,062 - src.services - WARNING - +2025-09-27 23:40:19,062 - src.services - WARNING - +2025-09-27 23:40:19,062 - src.services - WARNING - +2025-09-27 23:40:19,062 - src.services - WARNING - +2025-09-27 23:40:19,062 - src.services - WARNING - +2025-09-27 23:40:19,062 - src.services - WARNING - +2025-09-27 23:40:19,062 - src.services - WARNING - +2025-09-27 23:40:19,062 - src.services - WARNING - +2025-09-27 23:40:19,062 - src.services - WARNING - +2025-09-27 23:40:19,062 - src.services - WARNING - +2025-09-27 23:40:19,062 - src.services - WARNING - +2025-09-27 23:40:19,062 - src.services - WARNING - +2025-09-27 23:40:19,062 - src.services - WARNING - +2025-09-27 23:40:19,063 - src.services - WARNING - +2025-09-27 23:40:19,063 - src.services - WARNING - +2025-09-27 23:40:19,063 - src.services - WARNING - +2025-09-27 23:40:19,063 - src.services - WARNING - +2025-09-27 23:40:19,063 - src.services - WARNING - +2025-09-27 23:40:19,063 - src.services - WARNING - +2025-09-27 23:40:19,063 - src.services - WARNING - +2025-09-27 23:40:19,063 - src.services - WARNING - +2025-09-27 23:40:19,063 - src.services - WARNING - +2025-09-27 23:40:19,063 - src.services - WARNING - +2025-09-27 23:40:19,063 - src.services - WARNING - +2025-09-27 23:40:19,063 - src.services - WARNING - +2025-09-27 23:40:19,063 - src.services - WARNING - +2025-09-27 23:40:19,063 - src.services - WARNING - +2025-09-27 23:40:19,063 - src.services - WARNING - +2025-09-27 23:40:19,064 - src.services - WARNING - +2025-09-27 23:40:19,064 - src.services - WARNING - +2025-09-27 23:40:19,064 - src.services - WARNING - +2025-09-27 23:40:19,064 - src.services - WARNING - +2025-09-27 23:40:19,064 - src.services - WARNING - +2025-09-27 23:40:19,064 - src.services - WARNING - +2025-09-27 23:40:19,064 - src.services - WARNING - +2025-09-27 23:40:19,064 - src.services - WARNING - +2025-09-27 23:40:19,064 - src.services - WARNING - +2025-09-27 23:40:19,064 - src.services - WARNING - +2025-09-27 23:40:19,064 - src.services - WARNING - +2025-09-27 23:40:19,064 - src.services - WARNING - +2025-09-27 23:40:19,064 - src.services - WARNING - +2025-09-27 23:40:19,064 - src.services - WARNING - +2025-09-27 23:40:19,064 - src.services - WARNING - +2025-09-27 23:40:19,065 - src.services - WARNING - +2025-09-27 23:40:19,065 - src.services - WARNING - +2025-09-27 23:40:19,065 - src.services - WARNING - +2025-09-27 23:40:19,065 - src.services - WARNING - +2025-09-27 23:40:19,065 - src.services - WARNING - +2025-09-27 23:40:19,065 - src.services - WARNING - +2025-09-27 23:40:19,065 - src.services - WARNING - +2025-09-27 23:40:19,065 - src.services - WARNING - +2025-09-27 23:40:19,065 - src.services - WARNING - +2025-09-27 23:40:19,065 - src.services - WARNING - +2025-09-27 23:40:19,065 - src.services - WARNING - +2025-09-27 23:40:19,065 - src.services - WARNING - +2025-09-27 23:40:19,065 - src.services - WARNING - +2025-09-27 23:40:19,065 - src.services - WARNING - +2025-09-27 23:40:19,065 - src.services - WARNING - +2025-09-27 23:40:19,066 - src.services - WARNING - +2025-09-27 23:40:19,066 - src.services - WARNING - +2025-09-27 23:40:19,066 - src.services - WARNING - +2025-09-27 23:40:19,066 - src.services - WARNING - +2025-09-27 23:40:19,066 - src.services - WARNING - +2025-09-27 23:40:19,066 - src.services - WARNING - +2025-09-27 23:40:19,066 - src.services - WARNING - +2025-09-27 23:40:19,066 - src.services - WARNING - +2025-09-27 23:40:19,066 - src.services - WARNING - +2025-09-27 23:40:19,066 - src.services - WARNING - +2025-09-27 23:40:19,066 - src.services - WARNING - +2025-09-27 23:40:19,066 - src.services - WARNING - +2025-09-27 23:40:19,066 - src.services - WARNING - +2025-09-27 23:40:19,066 - src.services - WARNING - +2025-09-27 23:40:19,066 - src.services - WARNING - +2025-09-27 23:40:19,067 - src.services - WARNING - +2025-09-27 23:40:19,067 - src.services - WARNING - +2025-09-27 23:40:19,067 - src.services - WARNING - +2025-09-27 23:40:19,067 - src.services - WARNING - +2025-09-27 23:40:19,067 - src.services - WARNING - +2025-09-27 23:40:19,067 - src.services - WARNING - +2025-09-27 23:40:19,067 - src.services - WARNING - +2025-09-27 23:40:19,067 - src.services - WARNING - +2025-09-27 23:40:19,067 - src.services - WARNING - +2025-09-27 23:40:19,067 - src.services - WARNING - +2025-09-27 23:40:19,067 - src.services - WARNING - +2025-09-27 23:40:19,067 - src.services - WARNING - +2025-09-27 23:40:19,067 - src.services - WARNING - +2025-09-27 23:40:19,067 - src.services - WARNING - +2025-09-27 23:40:19,067 - src.services - WARNING - +2025-09-27 23:40:19,068 - src.services - WARNING - +2025-09-27 23:40:19,068 - src.services - WARNING - +2025-09-27 23:40:19,068 - src.services - WARNING - +2025-09-27 23:40:19,068 - src.services - WARNING - +2025-09-27 23:40:19,068 - src.services - WARNING - +2025-09-27 23:40:19,068 - src.services - WARNING - +2025-09-27 23:40:19,068 - src.services - WARNING - +2025-09-27 23:40:19,068 - src.services - WARNING - +2025-09-27 23:40:19,068 - src.services - WARNING - +2025-09-27 23:40:19,068 - src.services - WARNING - +2025-09-27 23:40:19,068 - src.services - WARNING - +2025-09-27 23:40:19,068 - src.services - WARNING - +2025-09-27 23:40:19,068 - src.services - WARNING - +2025-09-27 23:40:19,068 - src.services - WARNING - +2025-09-27 23:40:19,068 - src.services - WARNING - +2025-09-27 23:40:19,068 - src.services - WARNING - +2025-09-27 23:40:19,069 - src.services - WARNING - +2025-09-27 23:40:19,069 - src.services - WARNING - +2025-09-27 23:40:19,069 - src.services - WARNING - +2025-09-27 23:40:19,069 - src.services - WARNING - +2025-09-27 23:40:19,069 - src.services - WARNING - +2025-09-27 23:40:19,069 - src.services - WARNING - +2025-09-27 23:40:19,069 - src.services - WARNING - +2025-09-27 23:40:19,069 - src.services - WARNING - +2025-09-27 23:40:19,069 - src.services - WARNING - +2025-09-27 23:40:19,069 - src.services - WARNING - +2025-09-27 23:40:19,069 - src.services - WARNING - +2025-09-27 23:40:19,069 - src.services - WARNING - +2025-09-27 23:40:19,069 - src.services - WARNING - +2025-09-27 23:40:19,069 - src.services - WARNING - +2025-09-27 23:40:19,069 - src.services - WARNING - +2025-09-27 23:40:19,070 - src.services - WARNING - +2025-09-27 23:40:19,070 - src.services - WARNING - +2025-09-27 23:40:19,070 - src.services - WARNING - +2025-09-27 23:40:19,070 - src.services - WARNING - +2025-09-27 23:40:19,070 - src.services - WARNING - +2025-09-27 23:40:19,070 - src.services - WARNING - +2025-09-27 23:40:19,070 - src.services - WARNING - +2025-09-27 23:40:19,070 - src.services - WARNING - +2025-09-27 23:40:19,070 - src.services - WARNING - +2025-09-27 23:40:19,070 - src.services - WARNING - +2025-09-27 23:40:19,070 - src.services - WARNING - +2025-09-27 23:40:19,070 - src.services - WARNING - +2025-09-27 23:40:19,070 - src.services - WARNING - +2025-09-27 23:40:19,070 - src.services - WARNING - +2025-09-27 23:40:19,070 - src.services - WARNING - +2025-09-27 23:40:19,071 - src.services - WARNING - +2025-09-27 23:40:19,071 - src.services - WARNING - +2025-09-27 23:40:19,071 - src.services - WARNING - +2025-09-27 23:40:19,071 - src.services - WARNING - +2025-09-27 23:40:19,071 - src.services - WARNING - +2025-09-27 23:40:19,071 - src.services - WARNING - +2025-09-27 23:40:19,071 - src.services - WARNING - +2025-09-27 23:40:19,071 - src.services - WARNING - +2025-09-27 23:40:19,071 - src.services - WARNING - +2025-09-27 23:40:19,071 - src.services - WARNING - +2025-09-27 23:40:19,071 - src.services - WARNING - +2025-09-27 23:40:19,071 - src.services - WARNING - +2025-09-27 23:40:19,071 - src.services - WARNING - +2025-09-27 23:40:19,071 - src.services - WARNING - +2025-09-27 23:40:19,071 - src.services - WARNING - +2025-09-27 23:40:19,071 - src.services - WARNING - +2025-09-27 23:40:19,072 - src.services - WARNING - +2025-09-27 23:40:19,072 - src.services - WARNING - +2025-09-27 23:40:19,072 - src.services - WARNING - +2025-09-27 23:40:19,072 - src.services - WARNING - +2025-09-27 23:40:19,072 - src.services - WARNING - +2025-09-27 23:40:19,072 - src.services - WARNING - +2025-09-27 23:40:19,072 - src.services - WARNING - +2025-09-27 23:40:19,072 - src.services - WARNING - +2025-09-27 23:40:19,072 - src.services - WARNING - +2025-09-27 23:40:19,072 - src.services - WARNING - +2025-09-27 23:40:19,072 - src.services - WARNING - +2025-09-27 23:40:19,072 - src.services - WARNING - +2025-09-27 23:40:19,072 - src.services - WARNING - +2025-09-27 23:40:19,072 - src.services - WARNING - +2025-09-27 23:40:19,072 - src.services - WARNING - +2025-09-27 23:40:19,073 - src.services - WARNING - +2025-09-27 23:40:19,073 - src.services - WARNING - +2025-09-27 23:40:19,073 - src.services - WARNING - +2025-09-27 23:40:19,073 - src.services - WARNING - +2025-09-27 23:40:19,073 - src.services - WARNING - +2025-09-27 23:40:19,073 - src.services - WARNING - +2025-09-27 23:40:19,073 - src.services - WARNING - +2025-09-27 23:40:19,073 - src.services - WARNING - +2025-09-27 23:40:19,073 - src.services - WARNING - +2025-09-27 23:40:19,073 - src.services - WARNING - +2025-09-27 23:40:19,073 - src.services - WARNING - +2025-09-27 23:40:19,073 - src.services - WARNING - +2025-09-27 23:40:19,074 - src.services - WARNING - +2025-09-27 23:40:19,074 - src.services - WARNING - +2025-09-27 23:40:19,074 - src.services - WARNING - +2025-09-27 23:40:19,074 - src.services - WARNING - +2025-09-27 23:40:19,074 - src.services - WARNING - +2025-09-27 23:40:19,074 - src.services - WARNING - +2025-09-27 23:40:19,074 - src.services - WARNING - +2025-09-27 23:40:19,074 - src.services - WARNING - +2025-09-27 23:40:19,074 - src.services - WARNING - +2025-09-27 23:40:19,074 - src.services - WARNING - +2025-09-27 23:40:19,074 - src.services - WARNING - +2025-09-27 23:40:19,074 - src.services - WARNING - +2025-09-27 23:40:19,074 - src.services - WARNING - +2025-09-27 23:40:19,074 - src.services - WARNING - +2025-09-27 23:40:19,075 - src.services - WARNING - +2025-09-27 23:40:19,075 - src.services - WARNING - +2025-09-27 23:40:19,075 - src.services - WARNING - +2025-09-27 23:40:19,075 - src.services - WARNING - +2025-09-27 23:40:19,075 - src.services - WARNING - +2025-09-27 23:40:19,075 - src.services - WARNING - +2025-09-27 23:40:19,075 - src.services - WARNING - +2025-09-27 23:40:19,075 - src.services - WARNING - +2025-09-27 23:40:19,075 - src.services - WARNING - +2025-09-27 23:40:19,075 - src.services - WARNING - +2025-09-27 23:40:19,075 - src.services - WARNING - +2025-09-27 23:40:19,075 - src.services - WARNING - +2025-09-27 23:40:19,075 - src.services - WARNING - +2025-09-27 23:40:19,075 - src.services - WARNING - +2025-09-27 23:40:19,075 - src.services - WARNING - +2025-09-27 23:40:19,076 - src.services - WARNING - +2025-09-27 23:40:19,076 - src.services - WARNING - +2025-09-27 23:40:19,076 - src.services - WARNING - +2025-09-27 23:40:19,076 - src.services - WARNING - +2025-09-27 23:40:19,076 - src.services - WARNING - +2025-09-27 23:40:19,076 - src.services - WARNING - +2025-09-27 23:40:19,076 - src.services - WARNING - +2025-09-27 23:40:19,076 - src.services - WARNING - +2025-09-27 23:40:19,076 - src.services - WARNING - +2025-09-27 23:40:19,076 - src.services - WARNING - +2025-09-27 23:40:19,076 - src.services - WARNING - +2025-09-27 23:40:19,076 - src.services - WARNING - +2025-09-27 23:40:19,076 - src.services - WARNING - +2025-09-27 23:40:19,077 - src.services - WARNING - +2025-09-27 23:40:19,077 - src.services - WARNING - +2025-09-27 23:40:19,077 - src.services - WARNING - +2025-09-27 23:40:19,077 - src.services - WARNING - +2025-09-27 23:40:19,077 - src.services - WARNING - +2025-09-27 23:40:19,077 - src.services - WARNING - +2025-09-27 23:40:19,077 - src.services - WARNING - +2025-09-27 23:40:19,077 - src.services - WARNING - +2025-09-27 23:40:19,077 - src.services - WARNING - +2025-09-27 23:40:19,077 - src.services - WARNING - +2025-09-27 23:40:19,077 - src.services - WARNING - +2025-09-27 23:40:19,077 - src.services - WARNING - +2025-09-27 23:40:19,077 - src.services - WARNING - +2025-09-27 23:40:19,077 - src.services - WARNING - +2025-09-27 23:40:19,077 - src.services - WARNING - +2025-09-27 23:40:19,078 - src.services - WARNING - +2025-09-27 23:40:19,078 - src.services - WARNING - +2025-09-27 23:40:19,078 - src.services - WARNING - +2025-09-27 23:40:19,078 - src.services - WARNING - +2025-09-27 23:40:19,078 - src.services - WARNING - +2025-09-27 23:40:19,078 - src.services - WARNING - +2025-09-27 23:40:19,078 - src.services - WARNING - +2025-09-27 23:40:19,078 - src.services - WARNING - +2025-09-27 23:40:19,078 - src.services - WARNING - +2025-09-27 23:40:19,078 - src.services - WARNING - +2025-09-27 23:40:19,078 - src.services - WARNING - +2025-09-27 23:40:19,078 - src.services - WARNING - +2025-09-27 23:40:19,078 - src.services - WARNING - +2025-09-27 23:40:19,078 - src.services - WARNING - +2025-09-27 23:40:19,078 - src.services - WARNING - +2025-09-27 23:40:19,079 - src.services - WARNING - +2025-09-27 23:40:19,079 - src.services - WARNING - +2025-09-27 23:40:19,079 - src.services - WARNING - +2025-09-27 23:40:19,079 - src.services - WARNING - +2025-09-27 23:40:19,079 - src.services - WARNING - +2025-09-27 23:40:19,079 - src.services - WARNING - +2025-09-27 23:40:19,079 - src.services - WARNING - +2025-09-27 23:40:19,079 - src.services - WARNING - +2025-09-27 23:40:19,079 - src.services - WARNING - +2025-09-27 23:40:19,079 - src.services - WARNING - +2025-09-27 23:40:19,079 - src.services - WARNING - +2025-09-27 23:40:19,079 - src.services - WARNING - +2025-09-27 23:40:19,079 - src.services - WARNING - +2025-09-27 23:40:19,080 - src.services - WARNING - +2025-09-27 23:40:19,080 - src.services - WARNING - +2025-09-27 23:40:19,080 - src.services - WARNING - +2025-09-27 23:40:19,080 - src.services - WARNING - +2025-09-27 23:40:19,080 - src.services - WARNING - +2025-09-27 23:40:19,080 - src.services - WARNING - +2025-09-27 23:40:19,080 - src.services - WARNING - +2025-09-27 23:40:19,080 - src.services - WARNING - +2025-09-27 23:40:19,080 - src.services - WARNING - +2025-09-27 23:40:19,080 - src.services - WARNING - +2025-09-27 23:40:19,080 - src.services - WARNING - +2025-09-27 23:40:19,080 - src.services - WARNING - +2025-09-27 23:40:19,080 - src.services - WARNING - +2025-09-27 23:40:19,081 - src.services - WARNING - +2025-09-27 23:40:19,081 - src.services - WARNING - +2025-09-27 23:40:19,081 - src.services - WARNING - +2025-09-27 23:40:19,081 - src.services - WARNING - +2025-09-27 23:40:19,081 - src.services - WARNING - +2025-09-27 23:40:19,081 - src.services - WARNING - +2025-09-27 23:40:19,081 - src.services - WARNING - +2025-09-27 23:40:19,081 - src.services - WARNING - +2025-09-27 23:40:19,081 - src.services - WARNING - +2025-09-27 23:40:19,081 - src.services - WARNING - +2025-09-27 23:40:19,081 - src.services - WARNING - +2025-09-27 23:40:19,081 - src.services - WARNING - +2025-09-27 23:40:19,081 - src.services - WARNING - +2025-09-27 23:40:19,081 - src.services - WARNING - +2025-09-27 23:40:19,081 - src.services - WARNING - +2025-09-27 23:40:19,081 - src.services - WARNING - +2025-09-27 23:40:19,082 - src.services - WARNING - +2025-09-27 23:40:19,082 - src.services - WARNING - +2025-09-27 23:40:19,082 - src.services - WARNING - +2025-09-27 23:40:19,082 - src.services - WARNING - +2025-09-27 23:40:19,082 - src.services - WARNING - +2025-09-27 23:40:19,082 - src.services - WARNING - +2025-09-27 23:40:19,082 - src.services - WARNING - +2025-09-27 23:40:19,082 - src.services - WARNING - +2025-09-27 23:40:19,082 - src.services - WARNING - +2025-09-27 23:40:19,082 - src.services - WARNING - +2025-09-27 23:40:19,082 - src.services - WARNING - +2025-09-27 23:40:19,082 - src.services - WARNING - +2025-09-27 23:40:19,082 - src.services - WARNING - +2025-09-27 23:40:19,082 - src.services - WARNING - +2025-09-27 23:40:19,082 - src.services - WARNING - +2025-09-27 23:40:19,083 - src.services - WARNING - +2025-09-27 23:40:19,083 - src.services - WARNING - +2025-09-27 23:40:19,083 - src.services - WARNING - +2025-09-27 23:40:19,083 - src.services - WARNING - +2025-09-27 23:40:19,083 - src.services - WARNING - +2025-09-27 23:40:19,083 - src.services - WARNING - +2025-09-27 23:40:19,083 - src.services - WARNING - +2025-09-27 23:40:19,083 - src.services - WARNING - +2025-09-27 23:40:19,083 - src.services - WARNING - +2025-09-27 23:40:19,083 - src.services - WARNING - +2025-09-27 23:40:19,083 - src.services - WARNING - +2025-09-27 23:40:19,083 - src.services - WARNING - +2025-09-27 23:40:19,083 - src.services - WARNING - +2025-09-27 23:40:19,083 - src.services - WARNING - +2025-09-27 23:40:19,083 - src.services - WARNING - +2025-09-27 23:40:19,084 - src.services - WARNING - +2025-09-27 23:40:19,084 - src.services - WARNING - +2025-09-27 23:40:19,084 - src.services - WARNING - +2025-09-27 23:40:19,084 - src.services - WARNING - +2025-09-27 23:40:19,084 - src.services - WARNING - +2025-09-27 23:40:19,084 - src.services - WARNING - +2025-09-27 23:40:19,084 - src.services - WARNING - +2025-09-27 23:40:19,084 - src.services - WARNING - +2025-09-27 23:40:19,084 - src.services - WARNING - +2025-09-27 23:40:19,084 - src.services - WARNING - +2025-09-27 23:40:19,084 - src.services - WARNING - +2025-09-27 23:40:19,084 - src.services - WARNING - +2025-09-27 23:40:19,084 - src.services - WARNING - +2025-09-27 23:40:19,084 - src.services - WARNING - +2025-09-27 23:40:19,084 - src.services - WARNING - +2025-09-27 23:40:19,084 - src.services - WARNING - +2025-09-27 23:40:19,085 - src.services - WARNING - +2025-09-27 23:40:19,085 - src.services - WARNING - +2025-09-27 23:40:19,085 - src.services - WARNING - +2025-09-27 23:40:19,085 - src.services - WARNING - +2025-09-27 23:40:19,085 - src.services - WARNING - +2025-09-27 23:40:19,085 - src.services - WARNING - +2025-09-27 23:40:19,085 - src.services - WARNING - +2025-09-27 23:40:19,085 - src.services - WARNING - +2025-09-27 23:40:19,085 - src.services - WARNING - +2025-09-27 23:40:19,085 - src.services - WARNING - +2025-09-27 23:40:19,085 - src.services - WARNING - +2025-09-27 23:40:19,085 - src.services - WARNING - +2025-09-27 23:40:19,085 - src.services - WARNING - +2025-09-27 23:40:19,085 - src.services - WARNING - +2025-09-27 23:40:19,085 - src.services - WARNING - +2025-09-27 23:40:19,086 - src.services - WARNING - +2025-09-27 23:40:19,086 - src.services - WARNING - +2025-09-27 23:40:19,086 - src.services - WARNING - +2025-09-27 23:40:19,086 - src.services - WARNING - +2025-09-27 23:40:19,086 - src.services - WARNING - +2025-09-27 23:40:19,086 - src.services - WARNING - +2025-09-27 23:40:19,086 - src.services - WARNING - +2025-09-27 23:40:19,086 - src.services - WARNING - +2025-09-27 23:40:19,086 - src.services - WARNING - +2025-09-27 23:40:19,086 - src.services - WARNING - +2025-09-27 23:40:19,086 - src.services - WARNING - +2025-09-27 23:40:19,086 - src.services - WARNING - +2025-09-27 23:40:19,086 - src.services - WARNING - +2025-09-27 23:40:19,086 - src.services - WARNING - +2025-09-27 23:40:19,087 - src.services - WARNING - +2025-09-27 23:40:19,087 - src.services - WARNING - +2025-09-27 23:40:19,087 - src.services - WARNING - +2025-09-27 23:40:19,087 - src.services - WARNING - +2025-09-27 23:40:19,087 - src.services - WARNING - +2025-09-27 23:40:19,087 - src.services - WARNING - +2025-09-27 23:40:19,087 - src.services - WARNING - +2025-09-27 23:40:19,087 - src.services - WARNING - +2025-09-27 23:40:19,087 - src.services - WARNING - +2025-09-27 23:40:19,087 - src.services - WARNING - +2025-09-27 23:40:19,087 - src.services - WARNING - +2025-09-27 23:40:19,087 - src.services - WARNING - +2025-09-27 23:40:19,087 - src.services - WARNING - +2025-09-27 23:40:19,087 - src.services - WARNING - +2025-09-27 23:40:19,087 - src.services - WARNING - +2025-09-27 23:40:19,087 - src.services - WARNING - +2025-09-27 23:40:19,088 - src.services - WARNING - +2025-09-27 23:40:19,088 - src.services - WARNING - +2025-09-27 23:40:19,088 - src.services - WARNING - +2025-09-27 23:40:19,088 - src.services - WARNING - +2025-09-27 23:40:19,088 - src.services - WARNING - +2025-09-27 23:40:19,088 - src.services - WARNING - +2025-09-27 23:40:19,088 - src.services - WARNING - +2025-09-27 23:40:19,088 - src.services - WARNING - +2025-09-27 23:40:19,088 - src.services - WARNING - +2025-09-27 23:40:19,088 - src.services - WARNING - +2025-09-27 23:40:19,088 - src.services - WARNING - +2025-09-27 23:40:19,088 - src.services - WARNING - +2025-09-27 23:40:19,088 - src.services - WARNING - +2025-09-27 23:40:19,088 - src.services - WARNING - +2025-09-27 23:40:19,088 - src.services - WARNING - +2025-09-27 23:40:19,089 - src.services - WARNING - +2025-09-27 23:40:19,089 - src.services - WARNING - +2025-09-27 23:40:19,089 - src.services - WARNING - +2025-09-27 23:40:19,089 - src.services - WARNING - +2025-09-27 23:40:19,089 - src.services - WARNING - +2025-09-27 23:40:19,089 - src.services - WARNING - +2025-09-27 23:40:19,089 - src.services - WARNING - +2025-09-27 23:40:19,089 - src.services - WARNING - +2025-09-27 23:40:19,089 - src.services - WARNING - +2025-09-27 23:40:19,089 - src.services - WARNING - +2025-09-27 23:40:19,089 - src.services - WARNING - +2025-09-27 23:40:19,089 - src.services - WARNING - +2025-09-27 23:40:19,089 - src.services - WARNING - +2025-09-27 23:40:19,089 - src.services - WARNING - +2025-09-27 23:40:19,089 - src.services - WARNING - +2025-09-27 23:40:19,089 - src.services - WARNING - +2025-09-27 23:40:19,090 - src.services - WARNING - +2025-09-27 23:40:19,090 - src.services - WARNING - +2025-09-27 23:40:19,090 - src.services - WARNING - +2025-09-27 23:40:19,090 - src.services - WARNING - +2025-09-27 23:40:19,090 - src.services - WARNING - +2025-09-27 23:40:19,090 - src.services - WARNING - +2025-09-27 23:40:19,090 - src.services - WARNING - +2025-09-27 23:40:19,090 - src.services - WARNING - +2025-09-27 23:40:19,090 - src.services - WARNING - +2025-09-27 23:40:19,090 - src.services - WARNING - +2025-09-27 23:40:19,090 - src.services - WARNING - +2025-09-27 23:40:19,090 - src.services - WARNING - +2025-09-27 23:40:19,090 - src.services - WARNING - +2025-09-27 23:40:19,090 - src.services - WARNING - +2025-09-27 23:40:19,090 - src.services - WARNING - +2025-09-27 23:40:19,091 - src.services - WARNING - +2025-09-27 23:40:19,091 - src.services - WARNING - +2025-09-27 23:40:19,091 - src.services - WARNING - +2025-09-27 23:40:19,091 - src.services - WARNING - +2025-09-27 23:40:19,091 - src.services - WARNING - +2025-09-27 23:40:19,091 - src.services - WARNING - +2025-09-27 23:40:19,091 - src.services - WARNING - +2025-09-27 23:40:19,091 - src.services - WARNING - +2025-09-27 23:40:19,091 - src.services - WARNING - +2025-09-27 23:40:19,091 - src.services - WARNING - +2025-09-27 23:40:19,091 - src.services - WARNING - +2025-09-27 23:40:19,091 - src.services - WARNING - +2025-09-27 23:40:19,091 - src.services - WARNING - +2025-09-27 23:40:19,091 - src.services - WARNING - +2025-09-27 23:40:19,091 - src.services - WARNING - +2025-09-27 23:40:19,091 - src.services - WARNING - +2025-09-27 23:40:19,092 - src.services - WARNING - +2025-09-27 23:40:19,092 - src.services - WARNING - +2025-09-27 23:40:19,092 - src.services - WARNING - +2025-09-27 23:40:19,092 - src.services - WARNING - +2025-09-27 23:40:19,092 - src.services - WARNING - +2025-09-27 23:40:19,092 - src.services - WARNING - +2025-09-27 23:40:19,092 - src.services - WARNING - +2025-09-27 23:40:19,092 - src.services - WARNING - +2025-09-27 23:40:19,092 - src.services - WARNING - +2025-09-27 23:40:19,093 - src.services - WARNING - +2025-09-27 23:40:19,093 - src.services - WARNING - +2025-09-27 23:40:19,093 - src.services - WARNING - +2025-09-27 23:40:19,093 - src.services - WARNING - +2025-09-27 23:40:19,093 - src.services - WARNING - +2025-09-27 23:40:19,093 - src.services - WARNING - +2025-09-27 23:40:19,093 - src.services - WARNING - +2025-09-27 23:40:19,093 - src.services - WARNING - +2025-09-27 23:40:19,093 - src.services - WARNING - +2025-09-27 23:40:19,093 - src.services - WARNING - +2025-09-27 23:40:19,093 - src.services - WARNING - +2025-09-27 23:40:19,093 - src.services - WARNING - +2025-09-27 23:40:19,093 - src.services - WARNING - +2025-09-27 23:40:19,093 - src.services - WARNING - +2025-09-27 23:40:19,094 - src.services - WARNING - +2025-09-27 23:40:19,094 - src.services - WARNING - +2025-09-27 23:40:19,094 - src.services - WARNING - +2025-09-27 23:40:19,094 - src.services - WARNING - +2025-09-27 23:40:19,094 - src.services - WARNING - +2025-09-27 23:40:19,094 - src.services - WARNING - +2025-09-27 23:40:19,094 - src.services - WARNING - +2025-09-27 23:40:19,094 - src.services - WARNING - +2025-09-27 23:40:19,094 - src.services - WARNING - +2025-09-27 23:40:19,094 - src.services - WARNING - +2025-09-27 23:40:19,094 - src.services - WARNING - +2025-09-27 23:40:19,094 - src.services - WARNING - +2025-09-27 23:40:19,094 - src.services - WARNING - +2025-09-27 23:40:19,095 - src.services - WARNING - +2025-09-27 23:40:19,095 - src.services - WARNING - +2025-09-27 23:40:19,095 - src.services - WARNING - +2025-09-27 23:40:19,095 - src.services - WARNING - +2025-09-27 23:40:19,095 - src.services - WARNING - +2025-09-27 23:40:19,095 - src.services - WARNING - +2025-09-27 23:40:19,095 - src.services - WARNING - +2025-09-27 23:40:19,095 - src.services - WARNING - +2025-09-27 23:40:19,095 - src.services - WARNING - +2025-09-27 23:40:19,095 - src.services - WARNING - +2025-09-27 23:40:19,095 - src.services - WARNING - +2025-09-27 23:40:19,095 - src.services - WARNING - +2025-09-27 23:40:19,095 - src.services - WARNING - +2025-09-27 23:40:19,095 - src.services - WARNING - +2025-09-27 23:40:19,096 - src.services - WARNING - +2025-09-27 23:40:19,096 - src.services - WARNING - +2025-09-27 23:40:19,096 - src.services - WARNING - +2025-09-27 23:40:19,096 - src.services - WARNING - +2025-09-27 23:40:19,096 - src.services - WARNING - +2025-09-27 23:40:19,096 - src.services - WARNING - +2025-09-27 23:40:19,096 - src.services - WARNING - +2025-09-27 23:40:19,096 - src.services - WARNING - +2025-09-27 23:40:19,096 - src.services - WARNING - +2025-09-27 23:40:19,096 - src.services - WARNING - +2025-09-27 23:40:19,096 - src.services - WARNING - +2025-09-27 23:40:19,096 - src.services - WARNING - +2025-09-27 23:40:19,096 - src.services - WARNING - +2025-09-27 23:40:19,096 - src.services - WARNING - +2025-09-27 23:40:19,097 - src.services - WARNING - +2025-09-27 23:40:19,097 - src.services - WARNING - +2025-09-27 23:40:19,097 - src.services - WARNING - +2025-09-27 23:40:19,097 - src.services - WARNING - +2025-09-27 23:40:19,097 - src.services - WARNING - +2025-09-27 23:40:19,097 - src.services - WARNING - +2025-09-27 23:40:19,097 - src.services - WARNING - +2025-09-27 23:40:19,097 - src.services - WARNING - +2025-09-27 23:40:19,097 - src.services - WARNING - +2025-09-27 23:40:19,097 - src.services - WARNING - +2025-09-27 23:40:19,097 - src.services - WARNING - +2025-09-27 23:40:19,097 - src.services - WARNING - +2025-09-27 23:40:19,097 - src.services - WARNING - +2025-09-27 23:40:19,097 - src.services - WARNING - +2025-09-27 23:40:19,097 - src.services - WARNING - +2025-09-27 23:40:19,098 - src.services - WARNING - +2025-09-27 23:40:19,098 - src.services - WARNING - +2025-09-27 23:40:19,098 - src.services - WARNING - +2025-09-27 23:40:19,098 - src.services - WARNING - +2025-09-27 23:40:19,098 - src.services - WARNING - +2025-09-27 23:40:19,098 - src.services - WARNING - +2025-09-27 23:40:19,098 - src.services - WARNING - +2025-09-27 23:40:19,098 - src.services - WARNING - +2025-09-27 23:40:19,098 - src.services - WARNING - +2025-09-27 23:40:19,098 - src.services - WARNING - +2025-09-27 23:40:19,098 - src.services - WARNING - +2025-09-27 23:40:19,098 - src.services - WARNING - +2025-09-27 23:40:19,098 - src.services - WARNING - +2025-09-27 23:40:19,098 - src.services - WARNING - +2025-09-27 23:40:19,098 - src.services - WARNING - +2025-09-27 23:40:19,098 - src.services - WARNING - +2025-09-27 23:40:19,099 - src.services - WARNING - +2025-09-27 23:40:19,099 - src.services - WARNING - +2025-09-27 23:40:19,099 - src.services - WARNING - +2025-09-27 23:40:19,099 - src.services - WARNING - +2025-09-27 23:40:19,099 - src.services - WARNING - +2025-09-27 23:40:19,099 - src.services - WARNING - +2025-09-27 23:40:19,099 - src.services - WARNING - +2025-09-27 23:40:19,099 - src.services - WARNING - +2025-09-27 23:40:19,099 - src.services - WARNING - +2025-09-27 23:40:19,099 - src.services - WARNING - +2025-09-27 23:40:19,099 - src.services - WARNING - +2025-09-27 23:40:19,099 - src.services - WARNING - +2025-09-27 23:40:19,099 - src.services - WARNING - +2025-09-27 23:40:19,099 - src.services - WARNING - +2025-09-27 23:40:19,100 - src.services - WARNING - +2025-09-27 23:40:19,100 - src.services - WARNING - +2025-09-27 23:40:19,100 - src.services - WARNING - +2025-09-27 23:40:19,100 - src.services - WARNING - +2025-09-27 23:40:19,100 - src.services - WARNING - +2025-09-27 23:40:19,100 - src.services - WARNING - +2025-09-27 23:40:19,100 - src.services - WARNING - +2025-09-27 23:40:19,100 - src.services - WARNING - +2025-09-27 23:40:19,100 - src.services - WARNING - +2025-09-27 23:40:19,100 - src.services - WARNING - +2025-09-27 23:40:19,100 - src.services - WARNING - +2025-09-27 23:40:19,100 - src.services - WARNING - +2025-09-27 23:40:19,100 - src.services - WARNING - +2025-09-27 23:40:19,100 - src.services - WARNING - +2025-09-27 23:40:19,100 - src.services - WARNING - +2025-09-27 23:40:19,101 - src.services - WARNING - +2025-09-27 23:40:19,101 - src.services - WARNING - +2025-09-27 23:40:19,101 - src.services - WARNING - +2025-09-27 23:40:19,101 - src.services - WARNING - +2025-09-27 23:40:19,101 - src.services - WARNING - +2025-09-27 23:40:19,101 - src.services - WARNING - +2025-09-27 23:40:19,101 - src.services - WARNING - +2025-09-27 23:40:19,101 - src.services - WARNING - +2025-09-27 23:40:19,101 - src.services - WARNING - +2025-09-27 23:40:19,101 - src.services - WARNING - +2025-09-27 23:40:19,101 - src.services - WARNING - +2025-09-27 23:40:19,101 - src.services - WARNING - +2025-09-27 23:40:19,101 - src.services - WARNING - +2025-09-27 23:40:19,101 - src.services - WARNING - +2025-09-27 23:40:19,101 - src.services - WARNING - +2025-09-27 23:40:19,101 - src.services - WARNING - +2025-09-27 23:40:19,102 - src.services - WARNING - +2025-09-27 23:40:19,102 - src.services - WARNING - +2025-09-27 23:40:19,102 - src.services - WARNING - +2025-09-27 23:40:19,102 - src.services - WARNING - +2025-09-27 23:40:19,102 - src.services - WARNING - +2025-09-27 23:40:19,102 - src.services - WARNING - +2025-09-27 23:40:19,102 - src.services - WARNING - +2025-09-27 23:40:19,102 - src.services - WARNING - +2025-09-27 23:40:19,102 - src.services - WARNING - +2025-09-27 23:40:19,102 - src.services - WARNING - +2025-09-27 23:40:19,102 - src.services - WARNING - +2025-09-27 23:40:19,102 - src.services - WARNING - +2025-09-27 23:40:19,102 - src.services - WARNING - +2025-09-27 23:40:19,102 - src.services - WARNING - +2025-09-27 23:40:19,102 - src.services - WARNING - +2025-09-27 23:40:19,102 - src.services - WARNING - +2025-09-27 23:40:19,103 - src.services - WARNING - +2025-09-27 23:40:19,103 - src.services - WARNING - +2025-09-27 23:40:19,103 - src.services - WARNING - +2025-09-27 23:40:19,103 - src.services - WARNING - +2025-09-27 23:40:19,103 - src.services - WARNING - +2025-09-27 23:40:19,103 - src.services - WARNING - +2025-09-27 23:40:19,103 - src.services - WARNING - +2025-09-27 23:40:19,103 - src.services - WARNING - +2025-09-27 23:40:19,103 - src.services - WARNING - +2025-09-27 23:40:19,103 - src.services - WARNING - +2025-09-27 23:40:19,103 - src.services - WARNING - +2025-09-27 23:40:19,103 - src.services - WARNING - +2025-09-27 23:40:19,103 - src.services - WARNING - +2025-09-27 23:40:19,103 - src.services - WARNING - +2025-09-27 23:40:19,103 - src.services - WARNING - +2025-09-27 23:40:19,104 - src.services - WARNING - +2025-09-27 23:40:19,104 - src.services - WARNING - +2025-09-27 23:40:19,104 - src.services - WARNING - +2025-09-27 23:40:19,104 - src.services - WARNING - +2025-09-27 23:40:19,104 - src.services - WARNING - +2025-09-27 23:40:19,104 - src.services - WARNING - +2025-09-27 23:40:19,104 - src.services - WARNING - +2025-09-27 23:40:19,104 - src.services - WARNING - +2025-09-27 23:40:19,104 - src.services - WARNING - +2025-09-27 23:40:19,104 - src.services - WARNING - +2025-09-27 23:40:19,104 - src.services - WARNING - +2025-09-27 23:40:19,104 - src.services - WARNING - +2025-09-27 23:40:19,104 - src.services - WARNING - +2025-09-27 23:40:19,104 - src.services - WARNING - +2025-09-27 23:40:19,104 - src.services - WARNING - +2025-09-27 23:40:19,104 - src.services - WARNING - +2025-09-27 23:40:19,105 - src.services - WARNING - +2025-09-27 23:40:19,105 - src.services - WARNING - +2025-09-27 23:40:19,105 - src.services - WARNING - +2025-09-27 23:40:19,105 - src.services - WARNING - +2025-09-27 23:40:19,105 - src.services - WARNING - +2025-09-27 23:40:19,105 - src.services - WARNING - +2025-09-27 23:40:19,105 - src.services - WARNING - +2025-09-27 23:40:19,105 - src.services - WARNING - +2025-09-27 23:40:19,105 - src.services - WARNING - +2025-09-27 23:40:19,105 - src.services - WARNING - +2025-09-27 23:40:19,105 - src.services - WARNING - +2025-09-27 23:40:19,105 - src.services - WARNING - +2025-09-27 23:40:19,105 - src.services - WARNING - +2025-09-27 23:40:19,105 - src.services - WARNING - +2025-09-27 23:40:19,105 - src.services - WARNING - +2025-09-27 23:40:19,106 - src.services - WARNING - +2025-09-27 23:40:19,106 - src.services - WARNING - +2025-09-27 23:40:19,106 - src.services - WARNING - +2025-09-27 23:40:19,106 - src.services - WARNING - +2025-09-27 23:40:19,106 - src.services - WARNING - +2025-09-27 23:40:19,106 - src.services - WARNING - +2025-09-27 23:40:19,106 - src.services - WARNING - +2025-09-27 23:40:19,106 - src.services - WARNING - +2025-09-27 23:40:19,106 - src.services - WARNING - +2025-09-27 23:40:19,106 - src.services - WARNING - +2025-09-27 23:40:19,106 - src.services - WARNING - +2025-09-27 23:40:19,106 - src.services - WARNING - +2025-09-27 23:40:19,106 - src.services - INFO - investment_bank diff --git a/poetry.lock b/poetry.lock index 56365a4..a48cd34 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,235 @@ # This file is automatically @generated by Poetry 2.2.0 and should not be changed by hand. +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +description = "Happy Eyeballs for asyncio" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"}, + {file = "aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558"}, +] + +[[package]] +name = "aiohttp" +version = "3.12.15" +description = "Async http client/server framework (asyncio)" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "aiohttp-3.12.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b6fc902bff74d9b1879ad55f5404153e2b33a82e72a95c89cec5eb6cc9e92fbc"}, + {file = "aiohttp-3.12.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:098e92835b8119b54c693f2f88a1dec690e20798ca5f5fe5f0520245253ee0af"}, + {file = "aiohttp-3.12.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:40b3fee496a47c3b4a39a731954c06f0bd9bd3e8258c059a4beb76ac23f8e421"}, + {file = "aiohttp-3.12.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ce13fcfb0bb2f259fb42106cdc63fa5515fb85b7e87177267d89a771a660b79"}, + {file = "aiohttp-3.12.15-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3beb14f053222b391bf9cf92ae82e0171067cc9c8f52453a0f1ec7c37df12a77"}, + {file = "aiohttp-3.12.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c39e87afe48aa3e814cac5f535bc6199180a53e38d3f51c5e2530f5aa4ec58c"}, + {file = "aiohttp-3.12.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f1b4ce5bc528a6ee38dbf5f39bbf11dd127048726323b72b8e85769319ffc4"}, + {file = "aiohttp-3.12.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1004e67962efabbaf3f03b11b4c43b834081c9e3f9b32b16a7d97d4708a9abe6"}, + {file = "aiohttp-3.12.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8faa08fcc2e411f7ab91d1541d9d597d3a90e9004180edb2072238c085eac8c2"}, + {file = "aiohttp-3.12.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fe086edf38b2222328cdf89af0dde2439ee173b8ad7cb659b4e4c6f385b2be3d"}, + {file = "aiohttp-3.12.15-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:79b26fe467219add81d5e47b4a4ba0f2394e8b7c7c3198ed36609f9ba161aecb"}, + {file = "aiohttp-3.12.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b761bac1192ef24e16706d761aefcb581438b34b13a2f069a6d343ec8fb693a5"}, + {file = "aiohttp-3.12.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e153e8adacfe2af562861b72f8bc47f8a5c08e010ac94eebbe33dc21d677cd5b"}, + {file = "aiohttp-3.12.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:fc49c4de44977aa8601a00edbf157e9a421f227aa7eb477d9e3df48343311065"}, + {file = "aiohttp-3.12.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2776c7ec89c54a47029940177e75c8c07c29c66f73464784971d6a81904ce9d1"}, + {file = "aiohttp-3.12.15-cp310-cp310-win32.whl", hash = "sha256:2c7d81a277fa78b2203ab626ced1487420e8c11a8e373707ab72d189fcdad20a"}, + {file = "aiohttp-3.12.15-cp310-cp310-win_amd64.whl", hash = "sha256:83603f881e11f0f710f8e2327817c82e79431ec976448839f3cd05d7afe8f830"}, + {file = "aiohttp-3.12.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d3ce17ce0220383a0f9ea07175eeaa6aa13ae5a41f30bc61d84df17f0e9b1117"}, + {file = "aiohttp-3.12.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:010cc9bbd06db80fe234d9003f67e97a10fe003bfbedb40da7d71c1008eda0fe"}, + {file = "aiohttp-3.12.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3f9d7c55b41ed687b9d7165b17672340187f87a773c98236c987f08c858145a9"}, + {file = "aiohttp-3.12.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc4fbc61bb3548d3b482f9ac7ddd0f18c67e4225aaa4e8552b9f1ac7e6bda9e5"}, + {file = "aiohttp-3.12.15-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7fbc8a7c410bb3ad5d595bb7118147dfbb6449d862cc1125cf8867cb337e8728"}, + {file = "aiohttp-3.12.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74dad41b3458dbb0511e760fb355bb0b6689e0630de8a22b1b62a98777136e16"}, + {file = "aiohttp-3.12.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b6f0af863cf17e6222b1735a756d664159e58855da99cfe965134a3ff63b0b0"}, + {file = "aiohttp-3.12.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5b7fe4972d48a4da367043b8e023fb70a04d1490aa7d68800e465d1b97e493b"}, + {file = "aiohttp-3.12.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6443cca89553b7a5485331bc9bedb2342b08d073fa10b8c7d1c60579c4a7b9bd"}, + {file = "aiohttp-3.12.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c5f40ec615e5264f44b4282ee27628cea221fcad52f27405b80abb346d9f3f8"}, + {file = "aiohttp-3.12.15-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:2abbb216a1d3a2fe86dbd2edce20cdc5e9ad0be6378455b05ec7f77361b3ab50"}, + {file = "aiohttp-3.12.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:db71ce547012a5420a39c1b744d485cfb823564d01d5d20805977f5ea1345676"}, + {file = "aiohttp-3.12.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ced339d7c9b5030abad5854aa5413a77565e5b6e6248ff927d3e174baf3badf7"}, + {file = "aiohttp-3.12.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:7c7dd29c7b5bda137464dc9bfc738d7ceea46ff70309859ffde8c022e9b08ba7"}, + {file = "aiohttp-3.12.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:421da6fd326460517873274875c6c5a18ff225b40da2616083c5a34a7570b685"}, + {file = "aiohttp-3.12.15-cp311-cp311-win32.whl", hash = "sha256:4420cf9d179ec8dfe4be10e7d0fe47d6d606485512ea2265b0d8c5113372771b"}, + {file = "aiohttp-3.12.15-cp311-cp311-win_amd64.whl", hash = "sha256:edd533a07da85baa4b423ee8839e3e91681c7bfa19b04260a469ee94b778bf6d"}, + {file = "aiohttp-3.12.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:802d3868f5776e28f7bf69d349c26fc0efadb81676d0afa88ed00d98a26340b7"}, + {file = "aiohttp-3.12.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2800614cd560287be05e33a679638e586a2d7401f4ddf99e304d98878c29444"}, + {file = "aiohttp-3.12.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8466151554b593909d30a0a125d638b4e5f3836e5aecde85b66b80ded1cb5b0d"}, + {file = "aiohttp-3.12.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e5a495cb1be69dae4b08f35a6c4579c539e9b5706f606632102c0f855bcba7c"}, + {file = "aiohttp-3.12.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6404dfc8cdde35c69aaa489bb3542fb86ef215fc70277c892be8af540e5e21c0"}, + {file = "aiohttp-3.12.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3ead1c00f8521a5c9070fcb88f02967b1d8a0544e6d85c253f6968b785e1a2ab"}, + {file = "aiohttp-3.12.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6990ef617f14450bc6b34941dba4f12d5613cbf4e33805932f853fbd1cf18bfb"}, + {file = "aiohttp-3.12.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd736ed420f4db2b8148b52b46b88ed038d0354255f9a73196b7bbce3ea97545"}, + {file = "aiohttp-3.12.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c5092ce14361a73086b90c6efb3948ffa5be2f5b6fbcf52e8d8c8b8848bb97c"}, + {file = "aiohttp-3.12.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aaa2234bb60c4dbf82893e934d8ee8dea30446f0647e024074237a56a08c01bd"}, + {file = "aiohttp-3.12.15-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6d86a2fbdd14192e2f234a92d3b494dd4457e683ba07e5905a0b3ee25389ac9f"}, + {file = "aiohttp-3.12.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a041e7e2612041a6ddf1c6a33b883be6a421247c7afd47e885969ee4cc58bd8d"}, + {file = "aiohttp-3.12.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5015082477abeafad7203757ae44299a610e89ee82a1503e3d4184e6bafdd519"}, + {file = "aiohttp-3.12.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:56822ff5ddfd1b745534e658faba944012346184fbfe732e0d6134b744516eea"}, + {file = "aiohttp-3.12.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b2acbbfff69019d9014508c4ba0401822e8bae5a5fdc3b6814285b71231b60f3"}, + {file = "aiohttp-3.12.15-cp312-cp312-win32.whl", hash = "sha256:d849b0901b50f2185874b9a232f38e26b9b3d4810095a7572eacea939132d4e1"}, + {file = "aiohttp-3.12.15-cp312-cp312-win_amd64.whl", hash = "sha256:b390ef5f62bb508a9d67cb3bba9b8356e23b3996da7062f1a57ce1a79d2b3d34"}, + {file = "aiohttp-3.12.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9f922ffd05034d439dde1c77a20461cf4a1b0831e6caa26151fe7aa8aaebc315"}, + {file = "aiohttp-3.12.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ee8a8ac39ce45f3e55663891d4b1d15598c157b4d494a4613e704c8b43112cd"}, + {file = "aiohttp-3.12.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3eae49032c29d356b94eee45a3f39fdf4b0814b397638c2f718e96cfadf4c4e4"}, + {file = "aiohttp-3.12.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97752ff12cc12f46a9b20327104448042fce5c33a624f88c18f66f9368091c7"}, + {file = "aiohttp-3.12.15-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:894261472691d6fe76ebb7fcf2e5870a2ac284c7406ddc95823c8598a1390f0d"}, + {file = "aiohttp-3.12.15-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5fa5d9eb82ce98959fc1031c28198b431b4d9396894f385cb63f1e2f3f20ca6b"}, + {file = "aiohttp-3.12.15-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0fa751efb11a541f57db59c1dd821bec09031e01452b2b6217319b3a1f34f3d"}, + {file = "aiohttp-3.12.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5346b93e62ab51ee2a9d68e8f73c7cf96ffb73568a23e683f931e52450e4148d"}, + {file = "aiohttp-3.12.15-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:049ec0360f939cd164ecbfd2873eaa432613d5e77d6b04535e3d1fbae5a9e645"}, + {file = "aiohttp-3.12.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b52dcf013b57464b6d1e51b627adfd69a8053e84b7103a7cd49c030f9ca44461"}, + {file = "aiohttp-3.12.15-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9b2af240143dd2765e0fb661fd0361a1b469cab235039ea57663cda087250ea9"}, + {file = "aiohttp-3.12.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ac77f709a2cde2cc71257ab2d8c74dd157c67a0558a0d2799d5d571b4c63d44d"}, + {file = "aiohttp-3.12.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:47f6b962246f0a774fbd3b6b7be25d59b06fdb2f164cf2513097998fc6a29693"}, + {file = "aiohttp-3.12.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:760fb7db442f284996e39cf9915a94492e1896baac44f06ae551974907922b64"}, + {file = "aiohttp-3.12.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad702e57dc385cae679c39d318def49aef754455f237499d5b99bea4ef582e51"}, + {file = "aiohttp-3.12.15-cp313-cp313-win32.whl", hash = "sha256:f813c3e9032331024de2eb2e32a88d86afb69291fbc37a3a3ae81cc9917fb3d0"}, + {file = "aiohttp-3.12.15-cp313-cp313-win_amd64.whl", hash = "sha256:1a649001580bdb37c6fdb1bebbd7e3bc688e8ec2b5c6f52edbb664662b17dc84"}, + {file = "aiohttp-3.12.15-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:691d203c2bdf4f4637792efbbcdcd157ae11e55eaeb5e9c360c1206fb03d4d98"}, + {file = "aiohttp-3.12.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e995e1abc4ed2a454c731385bf4082be06f875822adc4c6d9eaadf96e20d406"}, + {file = "aiohttp-3.12.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bd44d5936ab3193c617bfd6c9a7d8d1085a8dc8c3f44d5f1dcf554d17d04cf7d"}, + {file = "aiohttp-3.12.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46749be6e89cd78d6068cdf7da51dbcfa4321147ab8e4116ee6678d9a056a0cf"}, + {file = "aiohttp-3.12.15-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0c643f4d75adea39e92c0f01b3fb83d57abdec8c9279b3078b68a3a52b3933b6"}, + {file = "aiohttp-3.12.15-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0a23918fedc05806966a2438489dcffccbdf83e921a1170773b6178d04ade142"}, + {file = "aiohttp-3.12.15-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:74bdd8c864b36c3673741023343565d95bfbd778ffe1eb4d412c135a28a8dc89"}, + {file = "aiohttp-3.12.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a146708808c9b7a988a4af3821379e379e0f0e5e466ca31a73dbdd0325b0263"}, + {file = "aiohttp-3.12.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7011a70b56facde58d6d26da4fec3280cc8e2a78c714c96b7a01a87930a9530"}, + {file = "aiohttp-3.12.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3bdd6e17e16e1dbd3db74d7f989e8af29c4d2e025f9828e6ef45fbdee158ec75"}, + {file = "aiohttp-3.12.15-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:57d16590a351dfc914670bd72530fd78344b885a00b250e992faea565b7fdc05"}, + {file = "aiohttp-3.12.15-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:bc9a0f6569ff990e0bbd75506c8d8fe7214c8f6579cca32f0546e54372a3bb54"}, + {file = "aiohttp-3.12.15-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:536ad7234747a37e50e7b6794ea868833d5220b49c92806ae2d7e8a9d6b5de02"}, + {file = "aiohttp-3.12.15-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:f0adb4177fa748072546fb650d9bd7398caaf0e15b370ed3317280b13f4083b0"}, + {file = "aiohttp-3.12.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:14954a2988feae3987f1eb49c706bff39947605f4b6fa4027c1d75743723eb09"}, + {file = "aiohttp-3.12.15-cp39-cp39-win32.whl", hash = "sha256:b784d6ed757f27574dca1c336f968f4e81130b27595e458e69457e6878251f5d"}, + {file = "aiohttp-3.12.15-cp39-cp39-win_amd64.whl", hash = "sha256:86ceded4e78a992f835209e236617bffae649371c4a50d5e5a3987f237db84b8"}, + {file = "aiohttp-3.12.15.tar.gz", hash = "sha256:4fc61385e9c98d72fcdf47e6dd81833f47b2f77c114c29cd64a361be57a763a2"}, +] + +[package.dependencies] +aiohappyeyeballs = ">=2.5.0" +aiosignal = ">=1.4.0" +async-timeout = {version = ">=4.0,<6.0", markers = "python_version < \"3.11\""} +attrs = ">=17.3.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +propcache = ">=0.2.0" +yarl = ">=1.17.0,<2.0" + +[package.extras] +speedups = ["Brotli ; platform_python_implementation == \"CPython\"", "aiodns (>=3.3.0)", "brotlicffi ; platform_python_implementation != \"CPython\""] + +[[package]] +name = "aiosignal" +version = "1.4.0" +description = "aiosignal: a list of registered asynchronous callbacks" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e"}, + {file = "aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" +typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""} + +[[package]] +name = "async-timeout" +version = "5.0.1" +description = "Timeout context manager for asyncio programs" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version < \"3.11\"" +files = [ + {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, + {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, +] + +[[package]] +name = "attrs" +version = "25.3.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"}, + {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"}, +] + +[package.extras] +benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] +tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] + +[[package]] +name = "black" +version = "25.9.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "black-25.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ce41ed2614b706fd55fd0b4a6909d06b5bab344ffbfadc6ef34ae50adba3d4f7"}, + {file = "black-25.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2ab0ce111ef026790e9b13bd216fa7bc48edd934ffc4cbf78808b235793cbc92"}, + {file = "black-25.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f96b6726d690c96c60ba682955199f8c39abc1ae0c3a494a9c62c0184049a713"}, + {file = "black-25.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:d119957b37cc641596063cd7db2656c5be3752ac17877017b2ffcdb9dfc4d2b1"}, + {file = "black-25.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:456386fe87bad41b806d53c062e2974615825c7a52159cde7ccaeb0695fa28fa"}, + {file = "black-25.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a16b14a44c1af60a210d8da28e108e13e75a284bf21a9afa6b4571f96ab8bb9d"}, + {file = "black-25.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aaf319612536d502fdd0e88ce52d8f1352b2c0a955cc2798f79eeca9d3af0608"}, + {file = "black-25.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:c0372a93e16b3954208417bfe448e09b0de5cc721d521866cd9e0acac3c04a1f"}, + {file = "black-25.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1b9dc70c21ef8b43248f1d86aedd2aaf75ae110b958a7909ad8463c4aa0880b0"}, + {file = "black-25.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8e46eecf65a095fa62e53245ae2795c90bdecabd53b50c448d0a8bcd0d2e74c4"}, + {file = "black-25.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9101ee58ddc2442199a25cb648d46ba22cd580b00ca4b44234a324e3ec7a0f7e"}, + {file = "black-25.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:77e7060a00c5ec4b3367c55f39cf9b06e68965a4f2e61cecacd6d0d9b7ec945a"}, + {file = "black-25.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0172a012f725b792c358d57fe7b6b6e8e67375dd157f64fa7a3097b3ed3e2175"}, + {file = "black-25.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3bec74ee60f8dfef564b573a96b8930f7b6a538e846123d5ad77ba14a8d7a64f"}, + {file = "black-25.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b756fc75871cb1bcac5499552d771822fd9db5a2bb8db2a7247936ca48f39831"}, + {file = "black-25.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:846d58e3ce7879ec1ffe816bb9df6d006cd9590515ed5d17db14e17666b2b357"}, + {file = "black-25.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ef69351df3c84485a8beb6f7b8f9721e2009e20ef80a8d619e2d1788b7816d47"}, + {file = "black-25.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e3c1f4cd5e93842774d9ee4ef6cd8d17790e65f44f7cdbaab5f2cf8ccf22a823"}, + {file = "black-25.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:154b06d618233fe468236ba1f0e40823d4eb08b26f5e9261526fde34916b9140"}, + {file = "black-25.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:e593466de7b998374ea2585a471ba90553283fb9beefcfa430d84a2651ed5933"}, + {file = "black-25.9.0-py3-none-any.whl", hash = "sha256:474b34c1342cdc157d307b56c4c65bce916480c4a8f6551fdc6bf9b486a7c4ae"}, + {file = "black-25.9.0.tar.gz", hash = "sha256:0474bca9a0dd1b51791fcc507a4e02078a1c63f6d4e4ae5544b9848c7adfb619"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +pytokens = ">=0.1.10" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.10)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "cachetools" +version = "6.2.0" +description = "Extensible memoizing collections and decorators" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "cachetools-6.2.0-py3-none-any.whl", hash = "sha256:1c76a8960c0041fcc21097e357f882197c79da0dbff766e7317890a65d7d8ba6"}, + {file = "cachetools-6.2.0.tar.gz", hash = "sha256:38b328c0889450f05f5e120f56ab68c8abaf424e1275522b138ffc93253f7e32"}, +] + [[package]] name = "certifi" version = "2025.8.3" @@ -101,6 +331,171 @@ files = [ {file = "charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14"}, ] +[[package]] +name = "click" +version = "8.1.8" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +groups = ["main"] +markers = "python_version < \"3.11\"" +files = [ + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "click" +version = "8.3.0" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version >= \"3.11\"" +files = [ + {file = "click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc"}, + {file = "click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "dev"] +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] +markers = {main = "sys_platform == \"win32\" or platform_system == \"Windows\"", dev = "sys_platform == \"win32\""} + +[[package]] +name = "coverage" +version = "7.10.7" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a"}, + {file = "coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5"}, + {file = "coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17"}, + {file = "coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b"}, + {file = "coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87"}, + {file = "coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e"}, + {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e"}, + {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df"}, + {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0"}, + {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13"}, + {file = "coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b"}, + {file = "coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807"}, + {file = "coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59"}, + {file = "coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a"}, + {file = "coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699"}, + {file = "coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d"}, + {file = "coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e"}, + {file = "coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23"}, + {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab"}, + {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82"}, + {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2"}, + {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61"}, + {file = "coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14"}, + {file = "coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2"}, + {file = "coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a"}, + {file = "coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417"}, + {file = "coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973"}, + {file = "coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c"}, + {file = "coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7"}, + {file = "coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6"}, + {file = "coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59"}, + {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b"}, + {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a"}, + {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb"}, + {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1"}, + {file = "coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256"}, + {file = "coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba"}, + {file = "coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf"}, + {file = "coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d"}, + {file = "coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b"}, + {file = "coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e"}, + {file = "coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b"}, + {file = "coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49"}, + {file = "coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911"}, + {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0"}, + {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f"}, + {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c"}, + {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f"}, + {file = "coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698"}, + {file = "coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843"}, + {file = "coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546"}, + {file = "coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c"}, + {file = "coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15"}, + {file = "coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4"}, + {file = "coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0"}, + {file = "coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0"}, + {file = "coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65"}, + {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541"}, + {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6"}, + {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999"}, + {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2"}, + {file = "coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a"}, + {file = "coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb"}, + {file = "coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb"}, + {file = "coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520"}, + {file = "coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32"}, + {file = "coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f"}, + {file = "coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a"}, + {file = "coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360"}, + {file = "coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69"}, + {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14"}, + {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe"}, + {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e"}, + {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd"}, + {file = "coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2"}, + {file = "coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681"}, + {file = "coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880"}, + {file = "coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63"}, + {file = "coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2"}, + {file = "coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d"}, + {file = "coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0"}, + {file = "coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699"}, + {file = "coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9"}, + {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f"}, + {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1"}, + {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0"}, + {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399"}, + {file = "coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235"}, + {file = "coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d"}, + {file = "coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a"}, + {file = "coverage-7.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3"}, + {file = "coverage-7.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c"}, + {file = "coverage-7.10.7-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396"}, + {file = "coverage-7.10.7-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40"}, + {file = "coverage-7.10.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594"}, + {file = "coverage-7.10.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a"}, + {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b"}, + {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3"}, + {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0"}, + {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f"}, + {file = "coverage-7.10.7-cp39-cp39-win32.whl", hash = "sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431"}, + {file = "coverage-7.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07"}, + {file = "coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260"}, + {file = "coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] + [[package]] name = "et-xmlfile" version = "2.0.0" @@ -113,6 +508,139 @@ files = [ {file = "et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54"}, ] +[[package]] +name = "exceptiongroup" +version = "1.3.0" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +groups = ["main", "dev"] +markers = "python_version < \"3.11\"" +files = [ + {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, + {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "frozenlist" +version = "1.7.0" +description = "A list-like structure which implements collections.abc.MutableSequence" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "frozenlist-1.7.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cc4df77d638aa2ed703b878dd093725b72a824c3c546c076e8fdf276f78ee84a"}, + {file = "frozenlist-1.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:716a9973a2cc963160394f701964fe25012600f3d311f60c790400b00e568b61"}, + {file = "frozenlist-1.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0fd1bad056a3600047fb9462cff4c5322cebc59ebf5d0a3725e0ee78955001d"}, + {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3789ebc19cb811163e70fe2bd354cea097254ce6e707ae42e56f45e31e96cb8e"}, + {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af369aa35ee34f132fcfad5be45fbfcde0e3a5f6a1ec0712857f286b7d20cca9"}, + {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac64b6478722eeb7a3313d494f8342ef3478dff539d17002f849101b212ef97c"}, + {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f89f65d85774f1797239693cef07ad4c97fdd0639544bad9ac4b869782eb1981"}, + {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1073557c941395fdfcfac13eb2456cb8aad89f9de27bae29fabca8e563b12615"}, + {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ed8d2fa095aae4bdc7fdd80351009a48d286635edffee66bf865e37a9125c50"}, + {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:24c34bea555fe42d9f928ba0a740c553088500377448febecaa82cc3e88aa1fa"}, + {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:69cac419ac6a6baad202c85aaf467b65ac860ac2e7f2ac1686dc40dbb52f6577"}, + {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:960d67d0611f4c87da7e2ae2eacf7ea81a5be967861e0c63cf205215afbfac59"}, + {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:41be2964bd4b15bf575e5daee5a5ce7ed3115320fb3c2b71fca05582ffa4dc9e"}, + {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:46d84d49e00c9429238a7ce02dc0be8f6d7cd0cd405abd1bebdc991bf27c15bd"}, + {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:15900082e886edb37480335d9d518cec978afc69ccbc30bd18610b7c1b22a718"}, + {file = "frozenlist-1.7.0-cp310-cp310-win32.whl", hash = "sha256:400ddd24ab4e55014bba442d917203c73b2846391dd42ca5e38ff52bb18c3c5e"}, + {file = "frozenlist-1.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:6eb93efb8101ef39d32d50bce242c84bcbddb4f7e9febfa7b524532a239b4464"}, + {file = "frozenlist-1.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:aa51e147a66b2d74de1e6e2cf5921890de6b0f4820b257465101d7f37b49fb5a"}, + {file = "frozenlist-1.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9b35db7ce1cd71d36ba24f80f0c9e7cff73a28d7a74e91fe83e23d27c7828750"}, + {file = "frozenlist-1.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34a69a85e34ff37791e94542065c8416c1afbf820b68f720452f636d5fb990cd"}, + {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a646531fa8d82c87fe4bb2e596f23173caec9185bfbca5d583b4ccfb95183e2"}, + {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:79b2ffbba483f4ed36a0f236ccb85fbb16e670c9238313709638167670ba235f"}, + {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a26f205c9ca5829cbf82bb2a84b5c36f7184c4316617d7ef1b271a56720d6b30"}, + {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bcacfad3185a623fa11ea0e0634aac7b691aa925d50a440f39b458e41c561d98"}, + {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72c1b0fe8fe451b34f12dce46445ddf14bd2a5bcad7e324987194dc8e3a74c86"}, + {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61d1a5baeaac6c0798ff6edfaeaa00e0e412d49946c53fae8d4b8e8b3566c4ae"}, + {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7edf5c043c062462f09b6820de9854bf28cc6cc5b6714b383149745e287181a8"}, + {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d50ac7627b3a1bd2dcef6f9da89a772694ec04d9a61b66cf87f7d9446b4a0c31"}, + {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ce48b2fece5aeb45265bb7a58259f45027db0abff478e3077e12b05b17fb9da7"}, + {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:fe2365ae915a1fafd982c146754e1de6ab3478def8a59c86e1f7242d794f97d5"}, + {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:45a6f2fdbd10e074e8814eb98b05292f27bad7d1883afbe009d96abdcf3bc898"}, + {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:21884e23cffabb157a9dd7e353779077bf5b8f9a58e9b262c6caad2ef5f80a56"}, + {file = "frozenlist-1.7.0-cp311-cp311-win32.whl", hash = "sha256:284d233a8953d7b24f9159b8a3496fc1ddc00f4db99c324bd5fb5f22d8698ea7"}, + {file = "frozenlist-1.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:387cbfdcde2f2353f19c2f66bbb52406d06ed77519ac7ee21be0232147c2592d"}, + {file = "frozenlist-1.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3dbf9952c4bb0e90e98aec1bd992b3318685005702656bc6f67c1a32b76787f2"}, + {file = "frozenlist-1.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f5906d3359300b8a9bb194239491122e6cf1444c2efb88865426f170c262cdb"}, + {file = "frozenlist-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3dabd5a8f84573c8d10d8859a50ea2dec01eea372031929871368c09fa103478"}, + {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa57daa5917f1738064f302bf2626281a1cb01920c32f711fbc7bc36111058a8"}, + {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c193dda2b6d49f4c4398962810fa7d7c78f032bf45572b3e04dd5249dff27e08"}, + {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe2b675cf0aaa6d61bf8fbffd3c274b3c9b7b1623beb3809df8a81399a4a9c4"}, + {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fc5d5cda37f62b262405cf9652cf0856839c4be8ee41be0afe8858f17f4c94b"}, + {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0d5ce521d1dd7d620198829b87ea002956e4319002ef0bc8d3e6d045cb4646e"}, + {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:488d0a7d6a0008ca0db273c542098a0fa9e7dfaa7e57f70acef43f32b3f69dca"}, + {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:15a7eaba63983d22c54d255b854e8108e7e5f3e89f647fc854bd77a237e767df"}, + {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1eaa7e9c6d15df825bf255649e05bd8a74b04a4d2baa1ae46d9c2d00b2ca2cb5"}, + {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4389e06714cfa9d47ab87f784a7c5be91d3934cd6e9a7b85beef808297cc025"}, + {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:73bd45e1488c40b63fe5a7df892baf9e2a4d4bb6409a2b3b78ac1c6236178e01"}, + {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99886d98e1643269760e5fe0df31e5ae7050788dd288947f7f007209b8c33f08"}, + {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:290a172aae5a4c278c6da8a96222e6337744cd9c77313efe33d5670b9f65fc43"}, + {file = "frozenlist-1.7.0-cp312-cp312-win32.whl", hash = "sha256:426c7bc70e07cfebc178bc4c2bf2d861d720c4fff172181eeb4a4c41d4ca2ad3"}, + {file = "frozenlist-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:563b72efe5da92e02eb68c59cb37205457c977aa7a449ed1b37e6939e5c47c6a"}, + {file = "frozenlist-1.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee80eeda5e2a4e660651370ebffd1286542b67e268aa1ac8d6dbe973120ef7ee"}, + {file = "frozenlist-1.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d1a81c85417b914139e3a9b995d4a1c84559afc839a93cf2cb7f15e6e5f6ed2d"}, + {file = "frozenlist-1.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cbb65198a9132ebc334f237d7b0df163e4de83fb4f2bdfe46c1e654bdb0c5d43"}, + {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dab46c723eeb2c255a64f9dc05b8dd601fde66d6b19cdb82b2e09cc6ff8d8b5d"}, + {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6aeac207a759d0dedd2e40745575ae32ab30926ff4fa49b1635def65806fddee"}, + {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bd8c4e58ad14b4fa7802b8be49d47993182fdd4023393899632c88fd8cd994eb"}, + {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04fb24d104f425da3540ed83cbfc31388a586a7696142004c577fa61c6298c3f"}, + {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a5c505156368e4ea6b53b5ac23c92d7edc864537ff911d2fb24c140bb175e60"}, + {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bd7eb96a675f18aa5c553eb7ddc24a43c8c18f22e1f9925528128c052cdbe00"}, + {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:05579bf020096fe05a764f1f84cd104a12f78eaab68842d036772dc6d4870b4b"}, + {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:376b6222d114e97eeec13d46c486facd41d4f43bab626b7c3f6a8b4e81a5192c"}, + {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0aa7e176ebe115379b5b1c95b4096fb1c17cce0847402e227e712c27bdb5a949"}, + {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3fbba20e662b9c2130dc771e332a99eff5da078b2b2648153a40669a6d0e36ca"}, + {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f3f4410a0a601d349dd406b5713fec59b4cee7e71678d5b17edda7f4655a940b"}, + {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e2cdfaaec6a2f9327bf43c933c0319a7c429058e8537c508964a133dffee412e"}, + {file = "frozenlist-1.7.0-cp313-cp313-win32.whl", hash = "sha256:5fc4df05a6591c7768459caba1b342d9ec23fa16195e744939ba5914596ae3e1"}, + {file = "frozenlist-1.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:52109052b9791a3e6b5d1b65f4b909703984b770694d3eb64fad124c835d7cba"}, + {file = "frozenlist-1.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a6f86e4193bb0e235ef6ce3dde5cbabed887e0b11f516ce8a0f4d3b33078ec2d"}, + {file = "frozenlist-1.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:82d664628865abeb32d90ae497fb93df398a69bb3434463d172b80fc25b0dd7d"}, + {file = "frozenlist-1.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:912a7e8375a1c9a68325a902f3953191b7b292aa3c3fb0d71a216221deca460b"}, + {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9537c2777167488d539bc5de2ad262efc44388230e5118868e172dd4a552b146"}, + {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f34560fb1b4c3e30ba35fa9a13894ba39e5acfc5f60f57d8accde65f46cc5e74"}, + {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acd03d224b0175f5a850edc104ac19040d35419eddad04e7cf2d5986d98427f1"}, + {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2038310bc582f3d6a09b3816ab01737d60bf7b1ec70f5356b09e84fb7408ab1"}, + {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c05e4c8e5f36e5e088caa1bf78a687528f83c043706640a92cb76cd6999384"}, + {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:765bb588c86e47d0b68f23c1bee323d4b703218037765dcf3f25c838c6fecceb"}, + {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:32dc2e08c67d86d0969714dd484fd60ff08ff81d1a1e40a77dd34a387e6ebc0c"}, + {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:c0303e597eb5a5321b4de9c68e9845ac8f290d2ab3f3e2c864437d3c5a30cd65"}, + {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a47f2abb4e29b3a8d0b530f7c3598badc6b134562b1a5caee867f7c62fee51e3"}, + {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:3d688126c242a6fabbd92e02633414d40f50bb6002fa4cf995a1d18051525657"}, + {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4e7e9652b3d367c7bd449a727dc79d5043f48b88d0cbfd4f9f1060cf2b414104"}, + {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1a85e345b4c43db8b842cab1feb41be5cc0b10a1830e6295b69d7310f99becaf"}, + {file = "frozenlist-1.7.0-cp313-cp313t-win32.whl", hash = "sha256:3a14027124ddb70dfcee5148979998066897e79f89f64b13328595c4bdf77c81"}, + {file = "frozenlist-1.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3bf8010d71d4507775f658e9823210b7427be36625b387221642725b515dcf3e"}, + {file = "frozenlist-1.7.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cea3dbd15aea1341ea2de490574a4a37ca080b2ae24e4b4f4b51b9057b4c3630"}, + {file = "frozenlist-1.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7d536ee086b23fecc36c2073c371572374ff50ef4db515e4e503925361c24f71"}, + {file = "frozenlist-1.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dfcebf56f703cb2e346315431699f00db126d158455e513bd14089d992101e44"}, + {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:974c5336e61d6e7eb1ea5b929cb645e882aadab0095c5a6974a111e6479f8878"}, + {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c70db4a0ab5ab20878432c40563573229a7ed9241506181bba12f6b7d0dc41cb"}, + {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1137b78384eebaf70560a36b7b229f752fb64d463d38d1304939984d5cb887b6"}, + {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e793a9f01b3e8b5c0bc646fb59140ce0efcc580d22a3468d70766091beb81b35"}, + {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74739ba8e4e38221d2c5c03d90a7e542cb8ad681915f4ca8f68d04f810ee0a87"}, + {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e63344c4e929b1a01e29bc184bbb5fd82954869033765bfe8d65d09e336a677"}, + {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2ea2a7369eb76de2217a842f22087913cdf75f63cf1307b9024ab82dfb525938"}, + {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:836b42f472a0e006e02499cef9352ce8097f33df43baaba3e0a28a964c26c7d2"}, + {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e22b9a99741294b2571667c07d9f8cceec07cb92aae5ccda39ea1b6052ed4319"}, + {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:9a19e85cc503d958abe5218953df722748d87172f71b73cf3c9257a91b999890"}, + {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:f22dac33bb3ee8fe3e013aa7b91dc12f60d61d05b7fe32191ffa84c3aafe77bd"}, + {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9ccec739a99e4ccf664ea0775149f2749b8a6418eb5b8384b4dc0a7d15d304cb"}, + {file = "frozenlist-1.7.0-cp39-cp39-win32.whl", hash = "sha256:b3950f11058310008a87757f3eee16a8e1ca97979833239439586857bc25482e"}, + {file = "frozenlist-1.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:43a82fce6769c70f2f5a06248b614a7d268080a9d20f7457ef10ecee5af82b63"}, + {file = "frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e"}, + {file = "frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f"}, +] + [[package]] name = "idna" version = "3.10" @@ -129,108 +657,206 @@ files = [ all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] [[package]] -name = "numpy" -version = "1.24.4" -description = "Fundamental package for array computing in Python" +name = "iniconfig" +version = "2.1.0" +description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, +] + +[[package]] +name = "multidict" +version = "6.6.4" +description = "multidict implementation" +optional = false +python-versions = ">=3.9" groups = ["main"] -markers = "python_version < \"3.10\"" -files = [ - {file = "numpy-1.24.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0bfb52d2169d58c1cdb8cc1f16989101639b34c7d3ce60ed70b19c63eba0b64"}, - {file = "numpy-1.24.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed094d4f0c177b1b8e7aa9cba7d6ceed51c0e569a5318ac0ca9a090680a6a1b1"}, - {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79fc682a374c4a8ed08b331bef9c5f582585d1048fa6d80bc6c35bc384eee9b4"}, - {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ffe43c74893dbf38c2b0a1f5428760a1a9c98285553c89e12d70a96a7f3a4d6"}, - {file = "numpy-1.24.4-cp310-cp310-win32.whl", hash = "sha256:4c21decb6ea94057331e111a5bed9a79d335658c27ce2adb580fb4d54f2ad9bc"}, - {file = "numpy-1.24.4-cp310-cp310-win_amd64.whl", hash = "sha256:b4bea75e47d9586d31e892a7401f76e909712a0fd510f58f5337bea9572c571e"}, - {file = "numpy-1.24.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f136bab9c2cfd8da131132c2cf6cc27331dd6fae65f95f69dcd4ae3c3639c810"}, - {file = "numpy-1.24.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2926dac25b313635e4d6cf4dc4e51c8c0ebfed60b801c799ffc4c32bf3d1254"}, - {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:222e40d0e2548690405b0b3c7b21d1169117391c2e82c378467ef9ab4c8f0da7"}, - {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7215847ce88a85ce39baf9e89070cb860c98fdddacbaa6c0da3ffb31b3350bd5"}, - {file = "numpy-1.24.4-cp311-cp311-win32.whl", hash = "sha256:4979217d7de511a8d57f4b4b5b2b965f707768440c17cb70fbf254c4b225238d"}, - {file = "numpy-1.24.4-cp311-cp311-win_amd64.whl", hash = "sha256:b7b1fc9864d7d39e28f41d089bfd6353cb5f27ecd9905348c24187a768c79694"}, - {file = "numpy-1.24.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1452241c290f3e2a312c137a9999cdbf63f78864d63c79039bda65ee86943f61"}, - {file = "numpy-1.24.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:04640dab83f7c6c85abf9cd729c5b65f1ebd0ccf9de90b270cd61935eef0197f"}, - {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5425b114831d1e77e4b5d812b69d11d962e104095a5b9c3b641a218abcc050e"}, - {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd80e219fd4c71fc3699fc1dadac5dcf4fd882bfc6f7ec53d30fa197b8ee22dc"}, - {file = "numpy-1.24.4-cp38-cp38-win32.whl", hash = "sha256:4602244f345453db537be5314d3983dbf5834a9701b7723ec28923e2889e0bb2"}, - {file = "numpy-1.24.4-cp38-cp38-win_amd64.whl", hash = "sha256:692f2e0f55794943c5bfff12b3f56f99af76f902fc47487bdfe97856de51a706"}, - {file = "numpy-1.24.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2541312fbf09977f3b3ad449c4e5f4bb55d0dbf79226d7724211acc905049400"}, - {file = "numpy-1.24.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9667575fb6d13c95f1b36aca12c5ee3356bf001b714fc354eb5465ce1609e62f"}, - {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a86ed21e4f87050382c7bc96571755193c4c1392490744ac73d660e8f564a9"}, - {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d11efb4dbecbdf22508d55e48d9c8384db795e1b7b51ea735289ff96613ff74d"}, - {file = "numpy-1.24.4-cp39-cp39-win32.whl", hash = "sha256:6620c0acd41dbcb368610bb2f4d83145674040025e5536954782467100aa8835"}, - {file = "numpy-1.24.4-cp39-cp39-win_amd64.whl", hash = "sha256:befe2bf740fd8373cf56149a5c23a0f601e82869598d41f8e188a0e9869926f8"}, - {file = "numpy-1.24.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:31f13e25b4e304632a4619d0e0777662c2ffea99fcae2029556b17d8ff958aef"}, - {file = "numpy-1.24.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95f7ac6540e95bc440ad77f56e520da5bf877f87dca58bd095288dce8940532a"}, - {file = "numpy-1.24.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e98f220aa76ca2a977fe435f5b04d7b3470c0a2e6312907b37ba6068f26787f2"}, - {file = "numpy-1.24.4.tar.gz", hash = "sha256:80f5e3a4e498641401868df4208b74581206afbee7cf7b8329daae82676d9463"}, +files = [ + {file = "multidict-6.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b8aa6f0bd8125ddd04a6593437bad6a7e70f300ff4180a531654aa2ab3f6d58f"}, + {file = "multidict-6.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b9e5853bbd7264baca42ffc53391b490d65fe62849bf2c690fa3f6273dbcd0cb"}, + {file = "multidict-6.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0af5f9dee472371e36d6ae38bde009bd8ce65ac7335f55dcc240379d7bed1495"}, + {file = "multidict-6.6.4-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:d24f351e4d759f5054b641c81e8291e5d122af0fca5c72454ff77f7cbe492de8"}, + {file = "multidict-6.6.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db6a3810eec08280a172a6cd541ff4a5f6a97b161d93ec94e6c4018917deb6b7"}, + {file = "multidict-6.6.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a1b20a9d56b2d81e2ff52ecc0670d583eaabaa55f402e8d16dd062373dbbe796"}, + {file = "multidict-6.6.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8c9854df0eaa610a23494c32a6f44a3a550fb398b6b51a56e8c6b9b3689578db"}, + {file = "multidict-6.6.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4bb7627fd7a968f41905a4d6343b0d63244a0623f006e9ed989fa2b78f4438a0"}, + {file = "multidict-6.6.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caebafea30ed049c57c673d0b36238b1748683be2593965614d7b0e99125c877"}, + {file = "multidict-6.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ad887a8250eb47d3ab083d2f98db7f48098d13d42eb7a3b67d8a5c795f224ace"}, + {file = "multidict-6.6.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:ed8358ae7d94ffb7c397cecb62cbac9578a83ecefc1eba27b9090ee910e2efb6"}, + {file = "multidict-6.6.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ecab51ad2462197a4c000b6d5701fc8585b80eecb90583635d7e327b7b6923eb"}, + {file = "multidict-6.6.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c5c97aa666cf70e667dfa5af945424ba1329af5dd988a437efeb3a09430389fb"}, + {file = "multidict-6.6.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:9a950b7cf54099c1209f455ac5970b1ea81410f2af60ed9eb3c3f14f0bfcf987"}, + {file = "multidict-6.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:163c7ea522ea9365a8a57832dea7618e6cbdc3cd75f8c627663587459a4e328f"}, + {file = "multidict-6.6.4-cp310-cp310-win32.whl", hash = "sha256:17d2cbbfa6ff20821396b25890f155f40c986f9cfbce5667759696d83504954f"}, + {file = "multidict-6.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:ce9a40fbe52e57e7edf20113a4eaddfacac0561a0879734e636aa6d4bb5e3fb0"}, + {file = "multidict-6.6.4-cp310-cp310-win_arm64.whl", hash = "sha256:01d0959807a451fe9fdd4da3e139cb5b77f7328baf2140feeaf233e1d777b729"}, + {file = "multidict-6.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c7a0e9b561e6460484318a7612e725df1145d46b0ef57c6b9866441bf6e27e0c"}, + {file = "multidict-6.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6bf2f10f70acc7a2446965ffbc726e5fc0b272c97a90b485857e5c70022213eb"}, + {file = "multidict-6.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66247d72ed62d5dd29752ffc1d3b88f135c6a8de8b5f63b7c14e973ef5bda19e"}, + {file = "multidict-6.6.4-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:105245cc6b76f51e408451a844a54e6823bbd5a490ebfe5bdfc79798511ceded"}, + {file = "multidict-6.6.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cbbc54e58b34c3bae389ef00046be0961f30fef7cb0dd9c7756aee376a4f7683"}, + {file = "multidict-6.6.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:56c6b3652f945c9bc3ac6c8178cd93132b8d82dd581fcbc3a00676c51302bc1a"}, + {file = "multidict-6.6.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b95494daf857602eccf4c18ca33337dd2be705bccdb6dddbfc9d513e6addb9d9"}, + {file = "multidict-6.6.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e5b1413361cef15340ab9dc61523e653d25723e82d488ef7d60a12878227ed50"}, + {file = "multidict-6.6.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e167bf899c3d724f9662ef00b4f7fef87a19c22b2fead198a6f68b263618df52"}, + {file = "multidict-6.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:aaea28ba20a9026dfa77f4b80369e51cb767c61e33a2d4043399c67bd95fb7c6"}, + {file = "multidict-6.6.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8c91cdb30809a96d9ecf442ec9bc45e8cfaa0f7f8bdf534e082c2443a196727e"}, + {file = "multidict-6.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1a0ccbfe93ca114c5d65a2471d52d8829e56d467c97b0e341cf5ee45410033b3"}, + {file = "multidict-6.6.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:55624b3f321d84c403cb7d8e6e982f41ae233d85f85db54ba6286f7295dc8a9c"}, + {file = "multidict-6.6.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4a1fb393a2c9d202cb766c76208bd7945bc194eba8ac920ce98c6e458f0b524b"}, + {file = "multidict-6.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:43868297a5759a845fa3a483fb4392973a95fb1de891605a3728130c52b8f40f"}, + {file = "multidict-6.6.4-cp311-cp311-win32.whl", hash = "sha256:ed3b94c5e362a8a84d69642dbeac615452e8af9b8eb825b7bc9f31a53a1051e2"}, + {file = "multidict-6.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:d8c112f7a90d8ca5d20213aa41eac690bb50a76da153e3afb3886418e61cb22e"}, + {file = "multidict-6.6.4-cp311-cp311-win_arm64.whl", hash = "sha256:3bb0eae408fa1996d87247ca0d6a57b7fc1dcf83e8a5c47ab82c558c250d4adf"}, + {file = "multidict-6.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0ffb87be160942d56d7b87b0fdf098e81ed565add09eaa1294268c7f3caac4c8"}, + {file = "multidict-6.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d191de6cbab2aff5de6c5723101705fd044b3e4c7cfd587a1929b5028b9714b3"}, + {file = "multidict-6.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38a0956dd92d918ad5feff3db8fcb4a5eb7dba114da917e1a88475619781b57b"}, + {file = "multidict-6.6.4-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6865f6d3b7900ae020b495d599fcf3765653bc927951c1abb959017f81ae8287"}, + {file = "multidict-6.6.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a2088c126b6f72db6c9212ad827d0ba088c01d951cee25e758c450da732c138"}, + {file = "multidict-6.6.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0f37bed7319b848097085d7d48116f545985db988e2256b2e6f00563a3416ee6"}, + {file = "multidict-6.6.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:01368e3c94032ba6ca0b78e7ccb099643466cf24f8dc8eefcfdc0571d56e58f9"}, + {file = "multidict-6.6.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe323540c255db0bffee79ad7f048c909f2ab0edb87a597e1c17da6a54e493c"}, + {file = "multidict-6.6.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8eb3025f17b0a4c3cd08cda49acf312a19ad6e8a4edd9dbd591e6506d999402"}, + {file = "multidict-6.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bbc14f0365534d35a06970d6a83478b249752e922d662dc24d489af1aa0d1be7"}, + {file = "multidict-6.6.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:75aa52fba2d96bf972e85451b99d8e19cc37ce26fd016f6d4aa60da9ab2b005f"}, + {file = "multidict-6.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4fefd4a815e362d4f011919d97d7b4a1e566f1dde83dc4ad8cfb5b41de1df68d"}, + {file = "multidict-6.6.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:db9801fe021f59a5b375ab778973127ca0ac52429a26e2fd86aa9508f4d26eb7"}, + {file = "multidict-6.6.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a650629970fa21ac1fb06ba25dabfc5b8a2054fcbf6ae97c758aa956b8dba802"}, + {file = "multidict-6.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:452ff5da78d4720d7516a3a2abd804957532dd69296cb77319c193e3ffb87e24"}, + {file = "multidict-6.6.4-cp312-cp312-win32.whl", hash = "sha256:8c2fcb12136530ed19572bbba61b407f655e3953ba669b96a35036a11a485793"}, + {file = "multidict-6.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:047d9425860a8c9544fed1b9584f0c8bcd31bcde9568b047c5e567a1025ecd6e"}, + {file = "multidict-6.6.4-cp312-cp312-win_arm64.whl", hash = "sha256:14754eb72feaa1e8ae528468f24250dd997b8e2188c3d2f593f9eba259e4b364"}, + {file = "multidict-6.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f46a6e8597f9bd71b31cc708195d42b634c8527fecbcf93febf1052cacc1f16e"}, + {file = "multidict-6.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:22e38b2bc176c5eb9c0a0e379f9d188ae4cd8b28c0f53b52bce7ab0a9e534657"}, + {file = "multidict-6.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5df8afd26f162da59e218ac0eefaa01b01b2e6cd606cffa46608f699539246da"}, + {file = "multidict-6.6.4-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:49517449b58d043023720aa58e62b2f74ce9b28f740a0b5d33971149553d72aa"}, + {file = "multidict-6.6.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae9408439537c5afdca05edd128a63f56a62680f4b3c234301055d7a2000220f"}, + {file = "multidict-6.6.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:87a32d20759dc52a9e850fe1061b6e41ab28e2998d44168a8a341b99ded1dba0"}, + {file = "multidict-6.6.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:52e3c8d43cdfff587ceedce9deb25e6ae77daba560b626e97a56ddcad3756879"}, + {file = "multidict-6.6.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ad8850921d3a8d8ff6fbef790e773cecfc260bbfa0566998980d3fa8f520bc4a"}, + {file = "multidict-6.6.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:497a2954adc25c08daff36f795077f63ad33e13f19bfff7736e72c785391534f"}, + {file = "multidict-6.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:024ce601f92d780ca1617ad4be5ac15b501cc2414970ffa2bb2bbc2bd5a68fa5"}, + {file = "multidict-6.6.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a693fc5ed9bdd1c9e898013e0da4dcc640de7963a371c0bd458e50e046bf6438"}, + {file = "multidict-6.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:190766dac95aab54cae5b152a56520fd99298f32a1266d66d27fdd1b5ac00f4e"}, + {file = "multidict-6.6.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:34d8f2a5ffdceab9dcd97c7a016deb2308531d5f0fced2bb0c9e1df45b3363d7"}, + {file = "multidict-6.6.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:59e8d40ab1f5a8597abcef00d04845155a5693b5da00d2c93dbe88f2050f2812"}, + {file = "multidict-6.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:467fe64138cfac771f0e949b938c2e1ada2b5af22f39692aa9258715e9ea613a"}, + {file = "multidict-6.6.4-cp313-cp313-win32.whl", hash = "sha256:14616a30fe6d0a48d0a48d1a633ab3b8bec4cf293aac65f32ed116f620adfd69"}, + {file = "multidict-6.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:40cd05eaeb39e2bc8939451f033e57feaa2ac99e07dbca8afe2be450a4a3b6cf"}, + {file = "multidict-6.6.4-cp313-cp313-win_arm64.whl", hash = "sha256:f6eb37d511bfae9e13e82cb4d1af36b91150466f24d9b2b8a9785816deb16605"}, + {file = "multidict-6.6.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:6c84378acd4f37d1b507dfa0d459b449e2321b3ba5f2338f9b085cf7a7ba95eb"}, + {file = "multidict-6.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0e0558693063c75f3d952abf645c78f3c5dfdd825a41d8c4d8156fc0b0da6e7e"}, + {file = "multidict-6.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3f8e2384cb83ebd23fd07e9eada8ba64afc4c759cd94817433ab8c81ee4b403f"}, + {file = "multidict-6.6.4-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f996b87b420995a9174b2a7c1a8daf7db4750be6848b03eb5e639674f7963773"}, + {file = "multidict-6.6.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc356250cffd6e78416cf5b40dc6a74f1edf3be8e834cf8862d9ed5265cf9b0e"}, + {file = "multidict-6.6.4-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:dadf95aa862714ea468a49ad1e09fe00fcc9ec67d122f6596a8d40caf6cec7d0"}, + {file = "multidict-6.6.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7dd57515bebffd8ebd714d101d4c434063322e4fe24042e90ced41f18b6d3395"}, + {file = "multidict-6.6.4-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:967af5f238ebc2eb1da4e77af5492219fbd9b4b812347da39a7b5f5c72c0fa45"}, + {file = "multidict-6.6.4-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a4c6875c37aae9794308ec43e3530e4aa0d36579ce38d89979bbf89582002bb"}, + {file = "multidict-6.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7f683a551e92bdb7fac545b9c6f9fa2aebdeefa61d607510b3533286fcab67f5"}, + {file = "multidict-6.6.4-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:3ba5aaf600edaf2a868a391779f7a85d93bed147854925f34edd24cc70a3e141"}, + {file = "multidict-6.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:580b643b7fd2c295d83cad90d78419081f53fd532d1f1eb67ceb7060f61cff0d"}, + {file = "multidict-6.6.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:37b7187197da6af3ee0b044dbc9625afd0c885f2800815b228a0e70f9a7f473d"}, + {file = "multidict-6.6.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e1b93790ed0bc26feb72e2f08299691ceb6da5e9e14a0d13cc74f1869af327a0"}, + {file = "multidict-6.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a506a77ddee1efcca81ecbeae27ade3e09cdf21a8ae854d766c2bb4f14053f92"}, + {file = "multidict-6.6.4-cp313-cp313t-win32.whl", hash = "sha256:f93b2b2279883d1d0a9e1bd01f312d6fc315c5e4c1f09e112e4736e2f650bc4e"}, + {file = "multidict-6.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:6d46a180acdf6e87cc41dc15d8f5c2986e1e8739dc25dbb7dac826731ef381a4"}, + {file = "multidict-6.6.4-cp313-cp313t-win_arm64.whl", hash = "sha256:756989334015e3335d087a27331659820d53ba432befdef6a718398b0a8493ad"}, + {file = "multidict-6.6.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:af7618b591bae552b40dbb6f93f5518328a949dac626ee75927bba1ecdeea9f4"}, + {file = "multidict-6.6.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b6819f83aef06f560cb15482d619d0e623ce9bf155115150a85ab11b8342a665"}, + {file = "multidict-6.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4d09384e75788861e046330308e7af54dd306aaf20eb760eb1d0de26b2bea2cb"}, + {file = "multidict-6.6.4-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:a59c63061f1a07b861c004e53869eb1211ffd1a4acbca330e3322efa6dd02978"}, + {file = "multidict-6.6.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:350f6b0fe1ced61e778037fdc7613f4051c8baf64b1ee19371b42a3acdb016a0"}, + {file = "multidict-6.6.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0c5cbac6b55ad69cb6aa17ee9343dfbba903118fd530348c330211dc7aa756d1"}, + {file = "multidict-6.6.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:630f70c32b8066ddfd920350bc236225814ad94dfa493fe1910ee17fe4365cbb"}, + {file = "multidict-6.6.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8d4916a81697faec6cb724a273bd5457e4c6c43d82b29f9dc02c5542fd21fc9"}, + {file = "multidict-6.6.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e42332cf8276bb7645d310cdecca93a16920256a5b01bebf747365f86a1675b"}, + {file = "multidict-6.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f3be27440f7644ab9a13a6fc86f09cdd90b347c3c5e30c6d6d860de822d7cb53"}, + {file = "multidict-6.6.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:21f216669109e02ef3e2415ede07f4f8987f00de8cdfa0cc0b3440d42534f9f0"}, + {file = "multidict-6.6.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:d9890d68c45d1aeac5178ded1d1cccf3bc8d7accf1f976f79bf63099fb16e4bd"}, + {file = "multidict-6.6.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:edfdcae97cdc5d1a89477c436b61f472c4d40971774ac4729c613b4b133163cb"}, + {file = "multidict-6.6.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:0b2e886624be5773e69cf32bcb8534aecdeb38943520b240fed3d5596a430f2f"}, + {file = "multidict-6.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:be5bf4b3224948032a845d12ab0f69f208293742df96dc14c4ff9b09e508fc17"}, + {file = "multidict-6.6.4-cp39-cp39-win32.whl", hash = "sha256:10a68a9191f284fe9d501fef4efe93226e74df92ce7a24e301371293bd4918ae"}, + {file = "multidict-6.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:ee25f82f53262f9ac93bd7e58e47ea1bdcc3393cef815847e397cba17e284210"}, + {file = "multidict-6.6.4-cp39-cp39-win_arm64.whl", hash = "sha256:f9867e55590e0855bcec60d4f9a092b69476db64573c9fe17e92b0c50614c16a"}, + {file = "multidict-6.6.4-py3-none-any.whl", hash = "sha256:27d8f8e125c07cb954e54d75d04905a9bba8a439c1d84aca94949d4d03d8601c"}, + {file = "multidict-6.6.4.tar.gz", hash = "sha256:d2d4e4787672911b48350df02ed3fa3fffdc2f2e8ca06dd6afdf34189b76a9dd"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""} + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, + {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, ] [[package]] name = "numpy" -version = "2.2.6" +version = "2.0.2" description = "Fundamental package for array computing in Python" optional = false -python-versions = ">=3.10" +python-versions = ">=3.9" groups = ["main"] -markers = "python_version == \"3.10\"" -files = [ - {file = "numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb"}, - {file = "numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90"}, - {file = "numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163"}, - {file = "numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf"}, - {file = "numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83"}, - {file = "numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915"}, - {file = "numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680"}, - {file = "numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289"}, - {file = "numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d"}, - {file = "numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3"}, - {file = "numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae"}, - {file = "numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a"}, - {file = "numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42"}, - {file = "numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491"}, - {file = "numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a"}, - {file = "numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf"}, - {file = "numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1"}, - {file = "numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab"}, - {file = "numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47"}, - {file = "numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303"}, - {file = "numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff"}, - {file = "numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c"}, - {file = "numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3"}, - {file = "numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282"}, - {file = "numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87"}, - {file = "numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249"}, - {file = "numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49"}, - {file = "numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de"}, - {file = "numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4"}, - {file = "numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2"}, - {file = "numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84"}, - {file = "numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b"}, - {file = "numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d"}, - {file = "numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566"}, - {file = "numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f"}, - {file = "numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f"}, - {file = "numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868"}, - {file = "numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d"}, - {file = "numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd"}, - {file = "numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c"}, - {file = "numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6"}, - {file = "numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda"}, - {file = "numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40"}, - {file = "numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8"}, - {file = "numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f"}, - {file = "numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa"}, - {file = "numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571"}, - {file = "numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1"}, - {file = "numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff"}, - {file = "numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06"}, - {file = "numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d"}, - {file = "numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db"}, - {file = "numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543"}, - {file = "numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00"}, - {file = "numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd"}, +markers = "python_version < \"3.11\"" +files = [ + {file = "numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece"}, + {file = "numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04"}, + {file = "numpy-2.0.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8c5713284ce4e282544c68d1c3b2c7161d38c256d2eefc93c1d683cf47683e66"}, + {file = "numpy-2.0.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:becfae3ddd30736fe1889a37f1f580e245ba79a5855bff5f2a29cb3ccc22dd7b"}, + {file = "numpy-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2da5960c3cf0df7eafefd806d4e612c5e19358de82cb3c343631188991566ccd"}, + {file = "numpy-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:496f71341824ed9f3d2fd36cf3ac57ae2e0165c143b55c3a035ee219413f3318"}, + {file = "numpy-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a61ec659f68ae254e4d237816e33171497e978140353c0c2038d46e63282d0c8"}, + {file = "numpy-2.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d731a1c6116ba289c1e9ee714b08a8ff882944d4ad631fd411106a30f083c326"}, + {file = "numpy-2.0.2-cp310-cp310-win32.whl", hash = "sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97"}, + {file = "numpy-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131"}, + {file = "numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448"}, + {file = "numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195"}, + {file = "numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57"}, + {file = "numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a"}, + {file = "numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669"}, + {file = "numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951"}, + {file = "numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9"}, + {file = "numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15"}, + {file = "numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4"}, + {file = "numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc"}, + {file = "numpy-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b"}, + {file = "numpy-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e"}, + {file = "numpy-2.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c"}, + {file = "numpy-2.0.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c"}, + {file = "numpy-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692"}, + {file = "numpy-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a"}, + {file = "numpy-2.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c"}, + {file = "numpy-2.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded"}, + {file = "numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5"}, + {file = "numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a"}, + {file = "numpy-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9059e10581ce4093f735ed23f3b9d283b9d517ff46009ddd485f1747eb22653c"}, + {file = "numpy-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:423e89b23490805d2a5a96fe40ec507407b8ee786d66f7328be214f9679df6dd"}, + {file = "numpy-2.0.2-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:2b2955fa6f11907cf7a70dab0d0755159bca87755e831e47932367fc8f2f2d0b"}, + {file = "numpy-2.0.2-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:97032a27bd9d8988b9a97a8c4d2c9f2c15a81f61e2f21404d7e8ef00cb5be729"}, + {file = "numpy-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e795a8be3ddbac43274f18588329c72939870a16cae810c2b73461c40718ab1"}, + {file = "numpy-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b258c385842546006213344c50655ff1555a9338e2e5e02a0756dc3e803dd"}, + {file = "numpy-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fec9451a7789926bcf7c2b8d187292c9f93ea30284802a0ab3f5be8ab36865d"}, + {file = "numpy-2.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9189427407d88ff25ecf8f12469d4d39d35bee1db5d39fc5c168c6f088a6956d"}, + {file = "numpy-2.0.2-cp39-cp39-win32.whl", hash = "sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa"}, + {file = "numpy-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73"}, + {file = "numpy-2.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7f0a0c6f12e07fa94133c8a67404322845220c06a9e80e85999afe727f7438b8"}, + {file = "numpy-2.0.2-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:312950fdd060354350ed123c0e25a71327d3711584beaef30cdaa93320c392d4"}, + {file = "numpy-2.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26df23238872200f63518dd2aa984cfca675d82469535dc7162dc2ee52d9dd5c"}, + {file = "numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385"}, + {file = "numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78"}, ] [[package]] @@ -334,70 +960,17 @@ files = [ et-xmlfile = "*" [[package]] -name = "pandas" -version = "2.0.3" -description = "Powerful data structures for data analysis, time series, and statistics" +name = "packaging" +version = "25.0" +description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" -groups = ["main"] -markers = "python_version < \"3.10\"" -files = [ - {file = "pandas-2.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e4c7c9f27a4185304c7caf96dc7d91bc60bc162221152de697c98eb0b2648dd8"}, - {file = "pandas-2.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f167beed68918d62bffb6ec64f2e1d8a7d297a038f86d4aed056b9493fca407f"}, - {file = "pandas-2.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce0c6f76a0f1ba361551f3e6dceaff06bde7514a374aa43e33b588ec10420183"}, - {file = "pandas-2.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba619e410a21d8c387a1ea6e8a0e49bb42216474436245718d7f2e88a2f8d7c0"}, - {file = "pandas-2.0.3-cp310-cp310-win32.whl", hash = "sha256:3ef285093b4fe5058eefd756100a367f27029913760773c8bf1d2d8bebe5d210"}, - {file = "pandas-2.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:9ee1a69328d5c36c98d8e74db06f4ad518a1840e8ccb94a4ba86920986bb617e"}, - {file = "pandas-2.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b084b91d8d66ab19f5bb3256cbd5ea661848338301940e17f4492b2ce0801fe8"}, - {file = "pandas-2.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37673e3bdf1551b95bf5d4ce372b37770f9529743d2498032439371fc7b7eb26"}, - {file = "pandas-2.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9cb1e14fdb546396b7e1b923ffaeeac24e4cedd14266c3497216dd4448e4f2d"}, - {file = "pandas-2.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9cd88488cceb7635aebb84809d087468eb33551097d600c6dad13602029c2df"}, - {file = "pandas-2.0.3-cp311-cp311-win32.whl", hash = "sha256:694888a81198786f0e164ee3a581df7d505024fbb1f15202fc7db88a71d84ebd"}, - {file = "pandas-2.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:6a21ab5c89dcbd57f78d0ae16630b090eec626360085a4148693def5452d8a6b"}, - {file = "pandas-2.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9e4da0d45e7f34c069fe4d522359df7d23badf83abc1d1cef398895822d11061"}, - {file = "pandas-2.0.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:32fca2ee1b0d93dd71d979726b12b61faa06aeb93cf77468776287f41ff8fdc5"}, - {file = "pandas-2.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:258d3624b3ae734490e4d63c430256e716f488c4fcb7c8e9bde2d3aa46c29089"}, - {file = "pandas-2.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eae3dc34fa1aa7772dd3fc60270d13ced7346fcbcfee017d3132ec625e23bb0"}, - {file = "pandas-2.0.3-cp38-cp38-win32.whl", hash = "sha256:f3421a7afb1a43f7e38e82e844e2bca9a6d793d66c1a7f9f0ff39a795bbc5e02"}, - {file = "pandas-2.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:69d7f3884c95da3a31ef82b7618af5710dba95bb885ffab339aad925c3e8ce78"}, - {file = "pandas-2.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5247fb1ba347c1261cbbf0fcfba4a3121fbb4029d95d9ef4dc45406620b25c8b"}, - {file = "pandas-2.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:81af086f4543c9d8bb128328b5d32e9986e0c84d3ee673a2ac6fb57fd14f755e"}, - {file = "pandas-2.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1994c789bf12a7c5098277fb43836ce090f1073858c10f9220998ac74f37c69b"}, - {file = "pandas-2.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ec591c48e29226bcbb316e0c1e9423622bc7a4eaf1ef7c3c9fa1a3981f89641"}, - {file = "pandas-2.0.3-cp39-cp39-win32.whl", hash = "sha256:04dbdbaf2e4d46ca8da896e1805bc04eb85caa9a82e259e8eed00254d5e0c682"}, - {file = "pandas-2.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:1168574b036cd8b93abc746171c9b4f1b83467438a5e45909fed645cf8692dbc"}, - {file = "pandas-2.0.3.tar.gz", hash = "sha256:c02f372a88e0d17f36d3093a644c73cfc1788e876a7c4bcb4020a77512e2043c"}, +groups = ["main", "dev"] +files = [ + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, ] -[package.dependencies] -numpy = {version = ">=1.20.3", markers = "python_version < \"3.10\""} -python-dateutil = ">=2.8.2" -pytz = ">=2020.1" -tzdata = ">=2022.1" - -[package.extras] -all = ["PyQt5 (>=5.15.1)", "SQLAlchemy (>=1.4.16)", "beautifulsoup4 (>=4.9.3)", "bottleneck (>=1.3.2)", "brotlipy (>=0.7.0)", "fastparquet (>=0.6.3)", "fsspec (>=2021.07.0)", "gcsfs (>=2021.07.0)", "html5lib (>=1.1)", "hypothesis (>=6.34.2)", "jinja2 (>=3.0.0)", "lxml (>=4.6.3)", "matplotlib (>=3.6.1)", "numba (>=0.53.1)", "numexpr (>=2.7.3)", "odfpy (>=1.4.1)", "openpyxl (>=3.0.7)", "pandas-gbq (>=0.15.0)", "psycopg2 (>=2.8.6)", "pyarrow (>=7.0.0)", "pymysql (>=1.0.2)", "pyreadstat (>=1.1.2)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)", "python-snappy (>=0.6.0)", "pyxlsb (>=1.0.8)", "qtpy (>=2.2.0)", "s3fs (>=2021.08.0)", "scipy (>=1.7.1)", "tables (>=3.6.1)", "tabulate (>=0.8.9)", "xarray (>=0.21.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=1.4.3)", "zstandard (>=0.15.2)"] -aws = ["s3fs (>=2021.08.0)"] -clipboard = ["PyQt5 (>=5.15.1)", "qtpy (>=2.2.0)"] -compression = ["brotlipy (>=0.7.0)", "python-snappy (>=0.6.0)", "zstandard (>=0.15.2)"] -computation = ["scipy (>=1.7.1)", "xarray (>=0.21.0)"] -excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.0.7)", "pyxlsb (>=1.0.8)", "xlrd (>=2.0.1)", "xlsxwriter (>=1.4.3)"] -feather = ["pyarrow (>=7.0.0)"] -fss = ["fsspec (>=2021.07.0)"] -gcp = ["gcsfs (>=2021.07.0)", "pandas-gbq (>=0.15.0)"] -hdf5 = ["tables (>=3.6.1)"] -html = ["beautifulsoup4 (>=4.9.3)", "html5lib (>=1.1)", "lxml (>=4.6.3)"] -mysql = ["SQLAlchemy (>=1.4.16)", "pymysql (>=1.0.2)"] -output-formatting = ["jinja2 (>=3.0.0)", "tabulate (>=0.8.9)"] -parquet = ["pyarrow (>=7.0.0)"] -performance = ["bottleneck (>=1.3.2)", "numba (>=0.53.1)", "numexpr (>=2.7.1)"] -plot = ["matplotlib (>=3.6.1)"] -postgresql = ["SQLAlchemy (>=1.4.16)", "psycopg2 (>=2.8.6)"] -spss = ["pyreadstat (>=1.1.2)"] -sql-other = ["SQLAlchemy (>=1.4.16)"] -test = ["hypothesis (>=6.34.2)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)"] -xml = ["lxml (>=4.6.3)"] - [[package]] name = "pandas" version = "2.3.2" @@ -405,7 +978,6 @@ description = "Powerful data structures for data analysis, time series, and stat optional = false python-versions = ">=3.9" groups = ["main"] -markers = "python_version >= \"3.10\"" files = [ {file = "pandas-2.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52bc29a946304c360561974c6542d1dd628ddafa69134a7131fdfd6a5d7a1a35"}, {file = "pandas-2.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:220cc5c35ffaa764dd5bb17cf42df283b5cb7fdf49e10a7b053a06c9cb48ee2b"}, @@ -486,6 +1058,305 @@ sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-d test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] xml = ["lxml (>=4.9.2)"] +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "platformdirs" +version = "4.4.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85"}, + {file = "platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.14.1)"] + +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + +[[package]] +name = "propcache" +version = "0.3.2" +description = "Accelerated property cache" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "propcache-0.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:22d9962a358aedbb7a2e36187ff273adeaab9743373a272976d2e348d08c7770"}, + {file = "propcache-0.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0d0fda578d1dc3f77b6b5a5dce3b9ad69a8250a891760a548df850a5e8da87f3"}, + {file = "propcache-0.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3def3da3ac3ce41562d85db655d18ebac740cb3fa4367f11a52b3da9d03a5cc3"}, + {file = "propcache-0.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bec58347a5a6cebf239daba9bda37dffec5b8d2ce004d9fe4edef3d2815137e"}, + {file = "propcache-0.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55ffda449a507e9fbd4aca1a7d9aa6753b07d6166140e5a18d2ac9bc49eac220"}, + {file = "propcache-0.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64a67fb39229a8a8491dd42f864e5e263155e729c2e7ff723d6e25f596b1e8cb"}, + {file = "propcache-0.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9da1cf97b92b51253d5b68cf5a2b9e0dafca095e36b7f2da335e27dc6172a614"}, + {file = "propcache-0.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5f559e127134b07425134b4065be45b166183fdcb433cb6c24c8e4149056ad50"}, + {file = "propcache-0.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aff2e4e06435d61f11a428360a932138d0ec288b0a31dd9bd78d200bd4a2b339"}, + {file = "propcache-0.3.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4927842833830942a5d0a56e6f4839bc484785b8e1ce8d287359794818633ba0"}, + {file = "propcache-0.3.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6107ddd08b02654a30fb8ad7a132021759d750a82578b94cd55ee2772b6ebea2"}, + {file = "propcache-0.3.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:70bd8b9cd6b519e12859c99f3fc9a93f375ebd22a50296c3a295028bea73b9e7"}, + {file = "propcache-0.3.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2183111651d710d3097338dd1893fcf09c9f54e27ff1a8795495a16a469cc90b"}, + {file = "propcache-0.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fb075ad271405dcad8e2a7ffc9a750a3bf70e533bd86e89f0603e607b93aa64c"}, + {file = "propcache-0.3.2-cp310-cp310-win32.whl", hash = "sha256:404d70768080d3d3bdb41d0771037da19d8340d50b08e104ca0e7f9ce55fce70"}, + {file = "propcache-0.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:7435d766f978b4ede777002e6b3b6641dd229cd1da8d3d3106a45770365f9ad9"}, + {file = "propcache-0.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0b8d2f607bd8f80ddc04088bc2a037fdd17884a6fcadc47a96e334d72f3717be"}, + {file = "propcache-0.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06766d8f34733416e2e34f46fea488ad5d60726bb9481d3cddf89a6fa2d9603f"}, + {file = "propcache-0.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2dc1f4a1df4fecf4e6f68013575ff4af84ef6f478fe5344317a65d38a8e6dc9"}, + {file = "propcache-0.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be29c4f4810c5789cf10ddf6af80b041c724e629fa51e308a7a0fb19ed1ef7bf"}, + {file = "propcache-0.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59d61f6970ecbd8ff2e9360304d5c8876a6abd4530cb752c06586849ac8a9dc9"}, + {file = "propcache-0.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62180e0b8dbb6b004baec00a7983e4cc52f5ada9cd11f48c3528d8cfa7b96a66"}, + {file = "propcache-0.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c144ca294a204c470f18cf4c9d78887810d04a3e2fbb30eea903575a779159df"}, + {file = "propcache-0.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5c2a784234c28854878d68978265617aa6dc0780e53d44b4d67f3651a17a9a2"}, + {file = "propcache-0.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5745bc7acdafa978ca1642891b82c19238eadc78ba2aaa293c6863b304e552d7"}, + {file = "propcache-0.3.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c0075bf773d66fa8c9d41f66cc132ecc75e5bb9dd7cce3cfd14adc5ca184cb95"}, + {file = "propcache-0.3.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5f57aa0847730daceff0497f417c9de353c575d8da3579162cc74ac294c5369e"}, + {file = "propcache-0.3.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:eef914c014bf72d18efb55619447e0aecd5fb7c2e3fa7441e2e5d6099bddff7e"}, + {file = "propcache-0.3.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2a4092e8549031e82facf3decdbc0883755d5bbcc62d3aea9d9e185549936dcf"}, + {file = "propcache-0.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:85871b050f174bc0bfb437efbdb68aaf860611953ed12418e4361bc9c392749e"}, + {file = "propcache-0.3.2-cp311-cp311-win32.whl", hash = "sha256:36c8d9b673ec57900c3554264e630d45980fd302458e4ac801802a7fd2ef7897"}, + {file = "propcache-0.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53af8cb6a781b02d2ea079b5b853ba9430fcbe18a8e3ce647d5982a3ff69f39"}, + {file = "propcache-0.3.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8de106b6c84506b31c27168582cd3cb3000a6412c16df14a8628e5871ff83c10"}, + {file = "propcache-0.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:28710b0d3975117239c76600ea351934ac7b5ff56e60953474342608dbbb6154"}, + {file = "propcache-0.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce26862344bdf836650ed2487c3d724b00fbfec4233a1013f597b78c1cb73615"}, + {file = "propcache-0.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bca54bd347a253af2cf4544bbec232ab982f4868de0dd684246b67a51bc6b1db"}, + {file = "propcache-0.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55780d5e9a2ddc59711d727226bb1ba83a22dd32f64ee15594b9392b1f544eb1"}, + {file = "propcache-0.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:035e631be25d6975ed87ab23153db6a73426a48db688070d925aa27e996fe93c"}, + {file = "propcache-0.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee6f22b6eaa39297c751d0e80c0d3a454f112f5c6481214fcf4c092074cecd67"}, + {file = "propcache-0.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ca3aee1aa955438c4dba34fc20a9f390e4c79967257d830f137bd5a8a32ed3b"}, + {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a4f30862869fa2b68380d677cc1c5fcf1e0f2b9ea0cf665812895c75d0ca3b8"}, + {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b77ec3c257d7816d9f3700013639db7491a434644c906a2578a11daf13176251"}, + {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cab90ac9d3f14b2d5050928483d3d3b8fb6b4018893fc75710e6aa361ecb2474"}, + {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0b504d29f3c47cf6b9e936c1852246c83d450e8e063d50562115a6be6d3a2535"}, + {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:ce2ac2675a6aa41ddb2a0c9cbff53780a617ac3d43e620f8fd77ba1c84dcfc06"}, + {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b4239611205294cc433845b914131b2a1f03500ff3c1ed093ed216b82621e1"}, + {file = "propcache-0.3.2-cp312-cp312-win32.whl", hash = "sha256:df4a81b9b53449ebc90cc4deefb052c1dd934ba85012aa912c7ea7b7e38b60c1"}, + {file = "propcache-0.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7046e79b989d7fe457bb755844019e10f693752d169076138abf17f31380800c"}, + {file = "propcache-0.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ca592ed634a73ca002967458187109265e980422116c0a107cf93d81f95af945"}, + {file = "propcache-0.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ecb0aad4020e275652ba3975740f241bd12a61f1a784df044cf7477a02bc252"}, + {file = "propcache-0.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7f08f1cc28bd2eade7a8a3d2954ccc673bb02062e3e7da09bc75d843386b342f"}, + {file = "propcache-0.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1a342c834734edb4be5ecb1e9fb48cb64b1e2320fccbd8c54bf8da8f2a84c33"}, + {file = "propcache-0.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a544caaae1ac73f1fecfae70ded3e93728831affebd017d53449e3ac052ac1e"}, + {file = "propcache-0.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:310d11aa44635298397db47a3ebce7db99a4cc4b9bbdfcf6c98a60c8d5261cf1"}, + {file = "propcache-0.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c1396592321ac83157ac03a2023aa6cc4a3cc3cfdecb71090054c09e5a7cce3"}, + {file = "propcache-0.3.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cabf5b5902272565e78197edb682017d21cf3b550ba0460ee473753f28d23c1"}, + {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0a2f2235ac46a7aa25bdeb03a9e7060f6ecbd213b1f9101c43b3090ffb971ef6"}, + {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:92b69e12e34869a6970fd2f3da91669899994b47c98f5d430b781c26f1d9f387"}, + {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:54e02207c79968ebbdffc169591009f4474dde3b4679e16634d34c9363ff56b4"}, + {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4adfb44cb588001f68c5466579d3f1157ca07f7504fc91ec87862e2b8e556b88"}, + {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fd3e6019dc1261cd0291ee8919dd91fbab7b169bb76aeef6c716833a3f65d206"}, + {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4c181cad81158d71c41a2bce88edce078458e2dd5ffee7eddd6b05da85079f43"}, + {file = "propcache-0.3.2-cp313-cp313-win32.whl", hash = "sha256:8a08154613f2249519e549de2330cf8e2071c2887309a7b07fb56098f5170a02"}, + {file = "propcache-0.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e41671f1594fc4ab0a6dec1351864713cb3a279910ae8b58f884a88a0a632c05"}, + {file = "propcache-0.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9a3cf035bbaf035f109987d9d55dc90e4b0e36e04bbbb95af3055ef17194057b"}, + {file = "propcache-0.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:156c03d07dc1323d8dacaa221fbe028c5c70d16709cdd63502778e6c3ccca1b0"}, + {file = "propcache-0.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74413c0ba02ba86f55cf60d18daab219f7e531620c15f1e23d95563f505efe7e"}, + {file = "propcache-0.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f066b437bb3fa39c58ff97ab2ca351db465157d68ed0440abecb21715eb24b28"}, + {file = "propcache-0.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1304b085c83067914721e7e9d9917d41ad87696bf70f0bc7dee450e9c71ad0a"}, + {file = "propcache-0.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab50cef01b372763a13333b4e54021bdcb291fc9a8e2ccb9c2df98be51bcde6c"}, + {file = "propcache-0.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fad3b2a085ec259ad2c2842666b2a0a49dea8463579c606426128925af1ed725"}, + {file = "propcache-0.3.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:261fa020c1c14deafd54c76b014956e2f86991af198c51139faf41c4d5e83892"}, + {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:46d7f8aa79c927e5f987ee3a80205c987717d3659f035c85cf0c3680526bdb44"}, + {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:6d8f3f0eebf73e3c0ff0e7853f68be638b4043c65a70517bb575eff54edd8dbe"}, + {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:03c89c1b14a5452cf15403e291c0ccd7751d5b9736ecb2c5bab977ad6c5bcd81"}, + {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:0cc17efde71e12bbaad086d679ce575268d70bc123a5a71ea7ad76f70ba30bba"}, + {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:acdf05d00696bc0447e278bb53cb04ca72354e562cf88ea6f9107df8e7fd9770"}, + {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4445542398bd0b5d32df908031cb1b30d43ac848e20470a878b770ec2dcc6330"}, + {file = "propcache-0.3.2-cp313-cp313t-win32.whl", hash = "sha256:f86e5d7cd03afb3a1db8e9f9f6eff15794e79e791350ac48a8c924e6f439f394"}, + {file = "propcache-0.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9704bedf6e7cbe3c65eca4379a9b53ee6a83749f047808cbb5044d40d7d72198"}, + {file = "propcache-0.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a7fad897f14d92086d6b03fdd2eb844777b0c4d7ec5e3bac0fbae2ab0602bbe5"}, + {file = "propcache-0.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1f43837d4ca000243fd7fd6301947d7cb93360d03cd08369969450cc6b2ce3b4"}, + {file = "propcache-0.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:261df2e9474a5949c46e962065d88eb9b96ce0f2bd30e9d3136bcde84befd8f2"}, + {file = "propcache-0.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e514326b79e51f0a177daab1052bc164d9d9e54133797a3a58d24c9c87a3fe6d"}, + {file = "propcache-0.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d4a996adb6904f85894570301939afeee65f072b4fd265ed7e569e8d9058e4ec"}, + {file = "propcache-0.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:76cace5d6b2a54e55b137669b30f31aa15977eeed390c7cbfb1dafa8dfe9a701"}, + {file = "propcache-0.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31248e44b81d59d6addbb182c4720f90b44e1efdc19f58112a3c3a1615fb47ef"}, + {file = "propcache-0.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abb7fa19dbf88d3857363e0493b999b8011eea856b846305d8c0512dfdf8fbb1"}, + {file = "propcache-0.3.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d81ac3ae39d38588ad0549e321e6f773a4e7cc68e7751524a22885d5bbadf886"}, + {file = "propcache-0.3.2-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:cc2782eb0f7a16462285b6f8394bbbd0e1ee5f928034e941ffc444012224171b"}, + {file = "propcache-0.3.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:db429c19a6c7e8a1c320e6a13c99799450f411b02251fb1b75e6217cf4a14fcb"}, + {file = "propcache-0.3.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:21d8759141a9e00a681d35a1f160892a36fb6caa715ba0b832f7747da48fb6ea"}, + {file = "propcache-0.3.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2ca6d378f09adb13837614ad2754fa8afaee330254f404299611bce41a8438cb"}, + {file = "propcache-0.3.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:34a624af06c048946709f4278b4176470073deda88d91342665d95f7c6270fbe"}, + {file = "propcache-0.3.2-cp39-cp39-win32.whl", hash = "sha256:4ba3fef1c30f306b1c274ce0b8baaa2c3cdd91f645c48f06394068f37d3837a1"}, + {file = "propcache-0.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:7a2368eed65fc69a7a7a40b27f22e85e7627b74216f0846b04ba5c116e191ec9"}, + {file = "propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f"}, + {file = "propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168"}, +] + +[[package]] +name = "pydantic" +version = "1.10.24" +description = "Data validation and settings management using python type hints" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "pydantic-1.10.24-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eef07ea2fba12f9188cfa2c50cb3eaa6516b56c33e2a8cc3cd288b4190ee6c0c"}, + {file = "pydantic-1.10.24-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5a42033fac69b9f1f867ecc3a2159f0e94dceb1abfc509ad57e9e88d49774683"}, + {file = "pydantic-1.10.24-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c626596c1b95dc6d45f7129f10b6743fbb50f29d942d25a22b2ceead670c067d"}, + {file = "pydantic-1.10.24-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8057172868b0d98f95e6fcddcc5f75d01570e85c6308702dd2c50ea673bc197b"}, + {file = "pydantic-1.10.24-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:82f951210ebcdb778b1d93075af43adcd04e9ebfd4f44b1baa8eeb21fbd71e36"}, + {file = "pydantic-1.10.24-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b66e4892d8ae005f436a5c5f1519ecf837574d8414b1c93860fb3c13943d9b37"}, + {file = "pydantic-1.10.24-cp310-cp310-win_amd64.whl", hash = "sha256:50d9f8a207c07f347d4b34806dc576872000d9a60fd481ed9eb78ea8512e0666"}, + {file = "pydantic-1.10.24-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:70152291488f8d2bbcf2027b5c28c27724c78a7949c91b466d28ad75d6d12702"}, + {file = "pydantic-1.10.24-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:956b30638272c51c85caaff76851b60db4b339022c0ee6eca677c41e3646255b"}, + {file = "pydantic-1.10.24-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bed9d6eea5fabbc6978c42e947190c7bd628ddaff3b56fc963fe696c3710ccd6"}, + {file = "pydantic-1.10.24-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:af8e2b3648128b8cadb1a71e2f8092a6f42d4ca123fad7a8d7ce6db8938b1db3"}, + {file = "pydantic-1.10.24-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:076fff9da02ca716e4c8299c68512fdfbeac32fdefc9c160e6f80bdadca0993d"}, + {file = "pydantic-1.10.24-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8f2447ca88a7e14fd4d268857521fb37535c53a367b594fa2d7c2551af905993"}, + {file = "pydantic-1.10.24-cp311-cp311-win_amd64.whl", hash = "sha256:58d42a7c344882c00e3bb7c6c8c6f62db2e3aafa671f307271c45ad96e8ccf7a"}, + {file = "pydantic-1.10.24-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:17e7610119483f03954569c18d4de16f4e92f1585f20975414033ac2d4a96624"}, + {file = "pydantic-1.10.24-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e24435a9970dcb2b35648f2cf57505d4bd414fcca1a404c82e28d948183fe0a6"}, + {file = "pydantic-1.10.24-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a9e92b9c78d7f3cfa085c21c110e7000894446e24a836d006aabfc6ae3f1813"}, + {file = "pydantic-1.10.24-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef14dfa7c98b314a3e449e92df6f1479cafe74c626952f353ff0176b075070de"}, + {file = "pydantic-1.10.24-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52219b4e70c1db185cfd103a804e416384e1c8950168a2d4f385664c7c35d21a"}, + {file = "pydantic-1.10.24-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ce0986799248082e9a5a026c9b5d2f9fa2e24d2afb9b0eace9104334a58fdc1"}, + {file = "pydantic-1.10.24-cp312-cp312-win_amd64.whl", hash = "sha256:874a78e4ed821258295a472e325eee7de3d91ba7a61d0639ce1b0367a3c63d4c"}, + {file = "pydantic-1.10.24-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:265788a1120285c4955f8b3d52b3ea6a52c7a74db097c4c13a4d3567f0c6df3c"}, + {file = "pydantic-1.10.24-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d255bebd927e5f1e026b32605684f7b6fc36a13e62b07cb97b29027b91657def"}, + {file = "pydantic-1.10.24-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6e45dbc79a44e34c2c83ef1fcb56ff663040474dcf4dfc452db24a1de0f7574"}, + {file = "pydantic-1.10.24-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:af31565b12a7db5bfa5fe8c3a4f8fda4d32f5c2929998b1b241f1c22e9ab6e69"}, + {file = "pydantic-1.10.24-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9c377fc30d9ca40dbff5fd79c5a5e1f0d6fff040fa47a18851bb6b0bd040a5d8"}, + {file = "pydantic-1.10.24-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b644d6f14b2ce617d6def21622f9ba73961a16b7dffdba7f6692e2f66fa05d00"}, + {file = "pydantic-1.10.24-cp313-cp313-win_amd64.whl", hash = "sha256:0cbbf306124ae41cc153fdc2559b37faa1bec9a23ef7b082c1756d1315ceffe6"}, + {file = "pydantic-1.10.24-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7c8bbad6037a87effe9f3739bdf39851add6e0f7e101d103a601c504892ffa70"}, + {file = "pydantic-1.10.24-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f154a8a46a0d950c055254f8f010ba07e742ac4404a3b6e281a31913ac45ccd0"}, + {file = "pydantic-1.10.24-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f25d2f792afcd874cc8339c1da1cc52739f4f3d52993ed1f6c263ef2afadc47"}, + {file = "pydantic-1.10.24-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:49a6f0178063f15eaea6cbcb2dba04db0b73db9834bc7b1e1c4dbea28c7cd22f"}, + {file = "pydantic-1.10.24-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:bb3df10be3c7d264947180615819aeec0916f19650f2ba7309ed1fe546ead0d2"}, + {file = "pydantic-1.10.24-cp37-cp37m-win_amd64.whl", hash = "sha256:fa0ebefc169439267e4b4147c7d458908788367640509ed32c90a91a63ebb579"}, + {file = "pydantic-1.10.24-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d1a5ef77efeb54def2695f2b8f4301aae8c7aa2b334bd15f61c18ef54317621"}, + {file = "pydantic-1.10.24-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:02f7a25e8949d8ca568e4bcef2ffed7881d7843286e7c3488bdd3b67f092059c"}, + {file = "pydantic-1.10.24-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5da2775712dda8b89e701ed2a72d5d81d23dbc6af84089da8a0f61a0be439c8c"}, + {file = "pydantic-1.10.24-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:75259be0558ca3af09192ad7b18557f2e9033ad4cbd48c252131f5292f6374fd"}, + {file = "pydantic-1.10.24-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:1a1ae996daa3d43c530b8d0bacc7e2d9cb55e3991f0e6b7cc2cb61a0fb9f6667"}, + {file = "pydantic-1.10.24-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:34109b0afa63b36eec2f2b115694e48ae5ee52f7d3c1baa0be36f80e586bda52"}, + {file = "pydantic-1.10.24-cp38-cp38-win_amd64.whl", hash = "sha256:4d7336bfcdb8cb58411e6b498772ba2cff84a2ce92f389bae3a8f1bb2c840c49"}, + {file = "pydantic-1.10.24-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:25fb9a69a21d711deb5acefdab9ff8fb49e6cc77fdd46d38217d433bff2e3de2"}, + {file = "pydantic-1.10.24-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6af36a8fb3072526b5b38d3f341b12d8f423188e7d185f130c0079fe02cdec7f"}, + {file = "pydantic-1.10.24-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fc35569dfd15d3b3fc06a22abee0a45fdde0784be644e650a8769cd0b2abd94"}, + {file = "pydantic-1.10.24-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fac7fbcb65171959973f3136d0792c3d1668bc01fd414738f0898b01f692f1b4"}, + {file = "pydantic-1.10.24-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fc3f4a6544517380658b63b144c7d43d5276a343012913b7e5d18d9fba2f12bb"}, + {file = "pydantic-1.10.24-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:415c638ca5fd57b915a62dd38c18c8e0afe5adf5527be6f8ce16b4636b616816"}, + {file = "pydantic-1.10.24-cp39-cp39-win_amd64.whl", hash = "sha256:a5bf94042efbc6ab56b18a5921f426ebbeefc04f554a911d76029e7be9057d01"}, + {file = "pydantic-1.10.24-py3-none-any.whl", hash = "sha256:093768eba26db55a88b12f3073017e3fdee319ef60d3aef5c6c04a4e484db193"}, + {file = "pydantic-1.10.24.tar.gz", hash = "sha256:7e6d1af1bd3d2312079f28c9baf2aafb4a452a06b50717526e5ac562e37baa53"}, +] + +[package.dependencies] +typing-extensions = ">=4.2.0" + +[package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] + +[[package]] +name = "pytest" +version = "7.4.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +groups = ["main", "dev"] +files = [ + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "0.23.8" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pytest_asyncio-0.23.8-py3-none-any.whl", hash = "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2"}, + {file = "pytest_asyncio-0.23.8.tar.gz", hash = "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3"}, +] + +[package.dependencies] +pytest = ">=7.0.0,<9" + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + +[[package]] +name = "pytest-cov" +version = "4.1.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, + {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + +[[package]] +name = "pytest-mock" +version = "3.15.1" +description = "Thin-wrapper around the mock package for easier use with pytest" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d"}, + {file = "pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f"}, +] + +[package.dependencies] +pytest = ">=6.2.5" + +[package.extras] +dev = ["pre-commit", "pytest-asyncio", "tox"] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -503,35 +1374,33 @@ six = ">=1.5" [[package]] name = "python-dotenv" -version = "1.0.1" +version = "1.1.1" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] -markers = "python_version < \"3.10\"" files = [ - {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, - {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, + {file = "python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc"}, + {file = "python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab"}, ] [package.extras] cli = ["click (>=5.0)"] [[package]] -name = "python-dotenv" -version = "1.1.1" -description = "Read key-value pairs from a .env file and set them as environment variables" +name = "pytokens" +version = "0.1.10" +description = "A Fast, spec compliant Python 3.12+ tokenizer that runs on older Pythons." optional = false -python-versions = ">=3.9" +python-versions = ">=3.8" groups = ["main"] -markers = "python_version >= \"3.10\"" files = [ - {file = "python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc"}, - {file = "python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab"}, + {file = "pytokens-0.1.10-py3-none-any.whl", hash = "sha256:db7b72284e480e69fb085d9f251f66b3d2df8b7166059261258ff35f50fb711b"}, + {file = "pytokens-0.1.10.tar.gz", hash = "sha256:c9a4bfa0be1d26aebce03e6884ba454e842f186a59ea43a6d3b25af58223c044"}, ] [package.extras] -cli = ["click (>=5.0)"] +dev = ["black", "build", "mypy", "pytest", "pytest-cov", "setuptools", "tox", "twine", "wheel"] [[package]] name = "pytz" @@ -545,29 +1414,6 @@ files = [ {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, ] -[[package]] -name = "requests" -version = "2.32.4" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=3.8" -groups = ["main"] -markers = "python_version < \"3.10\"" -files = [ - {file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"}, - {file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"}, -] - -[package.dependencies] -certifi = ">=2017.4.17" -charset_normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - [[package]] name = "requests" version = "2.32.5" @@ -575,7 +1421,6 @@ description = "Python HTTP for Humans." optional = false python-versions = ">=3.9" groups = ["main"] -markers = "python_version >= \"3.10\"" files = [ {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, @@ -603,6 +1448,62 @@ files = [ {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] +[[package]] +name = "tomli" +version = "2.2.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +markers = "python_version < \"3.11\"" +files = [ + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, +] +markers = {dev = "python_version < \"3.11\""} + [[package]] name = "tzdata" version = "2025.2" @@ -617,15 +1518,14 @@ files = [ [[package]] name = "urllib3" -version = "2.2.3" +version = "2.5.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] -markers = "python_version < \"3.10\"" files = [ - {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, - {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, + {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, + {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, ] [package.extras] @@ -635,25 +1535,125 @@ socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] [[package]] -name = "urllib3" -version = "2.5.0" -description = "HTTP library with thread-safe connection pooling, file post, and more." +name = "yarl" +version = "1.20.1" +description = "Yet another URL library" optional = false python-versions = ">=3.9" groups = ["main"] -markers = "python_version >= \"3.10\"" files = [ - {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, - {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, + {file = "yarl-1.20.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6032e6da6abd41e4acda34d75a816012717000fa6839f37124a47fcefc49bec4"}, + {file = "yarl-1.20.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2c7b34d804b8cf9b214f05015c4fee2ebe7ed05cf581e7192c06555c71f4446a"}, + {file = "yarl-1.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0c869f2651cc77465f6cd01d938d91a11d9ea5d798738c1dc077f3de0b5e5fed"}, + {file = "yarl-1.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62915e6688eb4d180d93840cda4110995ad50c459bf931b8b3775b37c264af1e"}, + {file = "yarl-1.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:41ebd28167bc6af8abb97fec1a399f412eec5fd61a3ccbe2305a18b84fb4ca73"}, + {file = "yarl-1.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21242b4288a6d56f04ea193adde174b7e347ac46ce6bc84989ff7c1b1ecea84e"}, + {file = "yarl-1.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bea21cdae6c7eb02ba02a475f37463abfe0a01f5d7200121b03e605d6a0439f8"}, + {file = "yarl-1.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f8a891e4a22a89f5dde7862994485e19db246b70bb288d3ce73a34422e55b23"}, + {file = "yarl-1.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd803820d44c8853a109a34e3660e5a61beae12970da479cf44aa2954019bf70"}, + {file = "yarl-1.20.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b982fa7f74c80d5c0c7b5b38f908971e513380a10fecea528091405f519b9ebb"}, + {file = "yarl-1.20.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:33f29ecfe0330c570d997bcf1afd304377f2e48f61447f37e846a6058a4d33b2"}, + {file = "yarl-1.20.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:835ab2cfc74d5eb4a6a528c57f05688099da41cf4957cf08cad38647e4a83b30"}, + {file = "yarl-1.20.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:46b5e0ccf1943a9a6e766b2c2b8c732c55b34e28be57d8daa2b3c1d1d4009309"}, + {file = "yarl-1.20.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:df47c55f7d74127d1b11251fe6397d84afdde0d53b90bedb46a23c0e534f9d24"}, + {file = "yarl-1.20.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76d12524d05841276b0e22573f28d5fbcb67589836772ae9244d90dd7d66aa13"}, + {file = "yarl-1.20.1-cp310-cp310-win32.whl", hash = "sha256:6c4fbf6b02d70e512d7ade4b1f998f237137f1417ab07ec06358ea04f69134f8"}, + {file = "yarl-1.20.1-cp310-cp310-win_amd64.whl", hash = "sha256:aef6c4d69554d44b7f9d923245f8ad9a707d971e6209d51279196d8e8fe1ae16"}, + {file = "yarl-1.20.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:47ee6188fea634bdfaeb2cc420f5b3b17332e6225ce88149a17c413c77ff269e"}, + {file = "yarl-1.20.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d0f6500f69e8402d513e5eedb77a4e1818691e8f45e6b687147963514d84b44b"}, + {file = "yarl-1.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a8900a42fcdaad568de58887c7b2f602962356908eedb7628eaf6021a6e435b"}, + {file = "yarl-1.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bad6d131fda8ef508b36be3ece16d0902e80b88ea7200f030a0f6c11d9e508d4"}, + {file = "yarl-1.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:df018d92fe22aaebb679a7f89fe0c0f368ec497e3dda6cb81a567610f04501f1"}, + {file = "yarl-1.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f969afbb0a9b63c18d0feecf0db09d164b7a44a053e78a7d05f5df163e43833"}, + {file = "yarl-1.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:812303eb4aa98e302886ccda58d6b099e3576b1b9276161469c25803a8db277d"}, + {file = "yarl-1.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98c4a7d166635147924aa0bf9bfe8d8abad6fffa6102de9c99ea04a1376f91e8"}, + {file = "yarl-1.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12e768f966538e81e6e7550f9086a6236b16e26cd964cf4df35349970f3551cf"}, + {file = "yarl-1.20.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fe41919b9d899661c5c28a8b4b0acf704510b88f27f0934ac7a7bebdd8938d5e"}, + {file = "yarl-1.20.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8601bc010d1d7780592f3fc1bdc6c72e2b6466ea34569778422943e1a1f3c389"}, + {file = "yarl-1.20.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:daadbdc1f2a9033a2399c42646fbd46da7992e868a5fe9513860122d7fe7a73f"}, + {file = "yarl-1.20.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:03aa1e041727cb438ca762628109ef1333498b122e4c76dd858d186a37cec845"}, + {file = "yarl-1.20.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:642980ef5e0fa1de5fa96d905c7e00cb2c47cb468bfcac5a18c58e27dbf8d8d1"}, + {file = "yarl-1.20.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:86971e2795584fe8c002356d3b97ef6c61862720eeff03db2a7c86b678d85b3e"}, + {file = "yarl-1.20.1-cp311-cp311-win32.whl", hash = "sha256:597f40615b8d25812f14562699e287f0dcc035d25eb74da72cae043bb884d773"}, + {file = "yarl-1.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:26ef53a9e726e61e9cd1cda6b478f17e350fb5800b4bd1cd9fe81c4d91cfeb2e"}, + {file = "yarl-1.20.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdcc4cd244e58593a4379fe60fdee5ac0331f8eb70320a24d591a3be197b94a9"}, + {file = "yarl-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b29a2c385a5f5b9c7d9347e5812b6f7ab267193c62d282a540b4fc528c8a9d2a"}, + {file = "yarl-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1112ae8154186dfe2de4732197f59c05a83dc814849a5ced892b708033f40dc2"}, + {file = "yarl-1.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90bbd29c4fe234233f7fa2b9b121fb63c321830e5d05b45153a2ca68f7d310ee"}, + {file = "yarl-1.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:680e19c7ce3710ac4cd964e90dad99bf9b5029372ba0c7cbfcd55e54d90ea819"}, + {file = "yarl-1.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a979218c1fdb4246a05efc2cc23859d47c89af463a90b99b7c56094daf25a16"}, + {file = "yarl-1.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255b468adf57b4a7b65d8aad5b5138dce6a0752c139965711bdcb81bc370e1b6"}, + {file = "yarl-1.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a97d67108e79cfe22e2b430d80d7571ae57d19f17cda8bb967057ca8a7bf5bfd"}, + {file = "yarl-1.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8570d998db4ddbfb9a590b185a0a33dbf8aafb831d07a5257b4ec9948df9cb0a"}, + {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:97c75596019baae7c71ccf1d8cc4738bc08134060d0adfcbe5642f778d1dca38"}, + {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1c48912653e63aef91ff988c5432832692ac5a1d8f0fb8a33091520b5bbe19ef"}, + {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4c3ae28f3ae1563c50f3d37f064ddb1511ecc1d5584e88c6b7c63cf7702a6d5f"}, + {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c5e9642f27036283550f5f57dc6156c51084b458570b9d0d96100c8bebb186a8"}, + {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2c26b0c49220d5799f7b22c6838409ee9bc58ee5c95361a4d7831f03cc225b5a"}, + {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564ab3d517e3d01c408c67f2e5247aad4019dcf1969982aba3974b4093279004"}, + {file = "yarl-1.20.1-cp312-cp312-win32.whl", hash = "sha256:daea0d313868da1cf2fac6b2d3a25c6e3a9e879483244be38c8e6a41f1d876a5"}, + {file = "yarl-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:48ea7d7f9be0487339828a4de0360d7ce0efc06524a48e1810f945c45b813698"}, + {file = "yarl-1.20.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0b5ff0fbb7c9f1b1b5ab53330acbfc5247893069e7716840c8e7d5bb7355038a"}, + {file = "yarl-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:14f326acd845c2b2e2eb38fb1346c94f7f3b01a4f5c788f8144f9b630bfff9a3"}, + {file = "yarl-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f60e4ad5db23f0b96e49c018596707c3ae89f5d0bd97f0ad3684bcbad899f1e7"}, + {file = "yarl-1.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49bdd1b8e00ce57e68ba51916e4bb04461746e794e7c4d4bbc42ba2f18297691"}, + {file = "yarl-1.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:66252d780b45189975abfed839616e8fd2dbacbdc262105ad7742c6ae58f3e31"}, + {file = "yarl-1.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59174e7332f5d153d8f7452a102b103e2e74035ad085f404df2e40e663a22b28"}, + {file = "yarl-1.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3968ec7d92a0c0f9ac34d5ecfd03869ec0cab0697c91a45db3fbbd95fe1b653"}, + {file = "yarl-1.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1a4fbb50e14396ba3d375f68bfe02215d8e7bc3ec49da8341fe3157f59d2ff5"}, + {file = "yarl-1.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11a62c839c3a8eac2410e951301309426f368388ff2f33799052787035793b02"}, + {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:041eaa14f73ff5a8986b4388ac6bb43a77f2ea09bf1913df7a35d4646db69e53"}, + {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:377fae2fef158e8fd9d60b4c8751387b8d1fb121d3d0b8e9b0be07d1b41e83dc"}, + {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1c92f4390e407513f619d49319023664643d3339bd5e5a56a3bebe01bc67ec04"}, + {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d25ddcf954df1754ab0f86bb696af765c5bfaba39b74095f27eececa049ef9a4"}, + {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:909313577e9619dcff8c31a0ea2aa0a2a828341d92673015456b3ae492e7317b"}, + {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:793fd0580cb9664548c6b83c63b43c477212c0260891ddf86809e1c06c8b08f1"}, + {file = "yarl-1.20.1-cp313-cp313-win32.whl", hash = "sha256:468f6e40285de5a5b3c44981ca3a319a4b208ccc07d526b20b12aeedcfa654b7"}, + {file = "yarl-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:495b4ef2fea40596bfc0affe3837411d6aa3371abcf31aac0ccc4bdd64d4ef5c"}, + {file = "yarl-1.20.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f60233b98423aab21d249a30eb27c389c14929f47be8430efa7dbd91493a729d"}, + {file = "yarl-1.20.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6f3eff4cc3f03d650d8755c6eefc844edde99d641d0dcf4da3ab27141a5f8ddf"}, + {file = "yarl-1.20.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:69ff8439d8ba832d6bed88af2c2b3445977eba9a4588b787b32945871c2444e3"}, + {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cf34efa60eb81dd2645a2e13e00bb98b76c35ab5061a3989c7a70f78c85006d"}, + {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8e0fe9364ad0fddab2688ce72cb7a8e61ea42eff3c7caeeb83874a5d479c896c"}, + {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f64fbf81878ba914562c672024089e3401974a39767747691c65080a67b18c1"}, + {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6342d643bf9a1de97e512e45e4b9560a043347e779a173250824f8b254bd5ce"}, + {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56dac5f452ed25eef0f6e3c6a066c6ab68971d96a9fb441791cad0efba6140d3"}, + {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7d7f497126d65e2cad8dc5f97d34c27b19199b6414a40cb36b52f41b79014be"}, + {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:67e708dfb8e78d8a19169818eeb5c7a80717562de9051bf2413aca8e3696bf16"}, + {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:595c07bc79af2494365cc96ddeb772f76272364ef7c80fb892ef9d0649586513"}, + {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7bdd2f80f4a7df852ab9ab49484a4dee8030023aa536df41f2d922fd57bf023f"}, + {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c03bfebc4ae8d862f853a9757199677ab74ec25424d0ebd68a0027e9c639a390"}, + {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:344d1103e9c1523f32a5ed704d576172d2cabed3122ea90b1d4e11fe17c66458"}, + {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:88cab98aa4e13e1ade8c141daeedd300a4603b7132819c484841bb7af3edce9e"}, + {file = "yarl-1.20.1-cp313-cp313t-win32.whl", hash = "sha256:b121ff6a7cbd4abc28985b6028235491941b9fe8fe226e6fdc539c977ea1739d"}, + {file = "yarl-1.20.1-cp313-cp313t-win_amd64.whl", hash = "sha256:541d050a355bbbc27e55d906bc91cb6fe42f96c01413dd0f4ed5a5240513874f"}, + {file = "yarl-1.20.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e42ba79e2efb6845ebab49c7bf20306c4edf74a0b20fc6b2ccdd1a219d12fad3"}, + {file = "yarl-1.20.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:41493b9b7c312ac448b7f0a42a089dffe1d6e6e981a2d76205801a023ed26a2b"}, + {file = "yarl-1.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f5a5928ff5eb13408c62a968ac90d43f8322fd56d87008b8f9dabf3c0f6ee983"}, + {file = "yarl-1.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30c41ad5d717b3961b2dd785593b67d386b73feca30522048d37298fee981805"}, + {file = "yarl-1.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:59febc3969b0781682b469d4aca1a5cab7505a4f7b85acf6db01fa500fa3f6ba"}, + {file = "yarl-1.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d2b6fb3622b7e5bf7a6e5b679a69326b4279e805ed1699d749739a61d242449e"}, + {file = "yarl-1.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:749d73611db8d26a6281086f859ea7ec08f9c4c56cec864e52028c8b328db723"}, + {file = "yarl-1.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9427925776096e664c39e131447aa20ec738bdd77c049c48ea5200db2237e000"}, + {file = "yarl-1.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff70f32aa316393eaf8222d518ce9118148eddb8a53073c2403863b41033eed5"}, + {file = "yarl-1.20.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c7ddf7a09f38667aea38801da8b8d6bfe81df767d9dfc8c88eb45827b195cd1c"}, + {file = "yarl-1.20.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:57edc88517d7fc62b174fcfb2e939fbc486a68315d648d7e74d07fac42cec240"}, + {file = "yarl-1.20.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:dab096ce479d5894d62c26ff4f699ec9072269d514b4edd630a393223f45a0ee"}, + {file = "yarl-1.20.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:14a85f3bd2d7bb255be7183e5d7d6e70add151a98edf56a770d6140f5d5f4010"}, + {file = "yarl-1.20.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c89b5c792685dd9cd3fa9761c1b9f46fc240c2a3265483acc1565769996a3f8"}, + {file = "yarl-1.20.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:69e9b141de5511021942a6866990aea6d111c9042235de90e08f94cf972ca03d"}, + {file = "yarl-1.20.1-cp39-cp39-win32.whl", hash = "sha256:b5f307337819cdfdbb40193cad84978a029f847b0a357fbe49f712063cfc4f06"}, + {file = "yarl-1.20.1-cp39-cp39-win_amd64.whl", hash = "sha256:eae7bfe2069f9c1c5b05fc7fe5d612e5bbc089a39309904ee8b829e322dcad00"}, + {file = "yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77"}, + {file = "yarl-1.20.1.tar.gz", hash = "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac"}, ] -[package.extras] -brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] -h2 = ["h2 (>=4,<5)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" +propcache = ">=0.2.1" [metadata] lock-version = "2.1" -python-versions = "^3.8" -content-hash = "9badf4e23decfc843ed04ca2f4d28d84d39dcf2982b95c01a89b78c668c706df" +python-versions = "^3.9" +content-hash = "f7a7687a4b766dfe6b494236768695d541b587629fc4f7be3b74fa18534258b2" diff --git a/pyproject.toml b/pyproject.toml index 3b9b19b..b08b769 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,38 +1,25 @@ [tool.poetry] name = "transaction-analyzer" -version = "0.1.0" -description = "Анализатор банковских транзакций" +version = "1.0.0" +description = "Bank transaction analysis application" authors = ["Your Name "] +readme = "README.md" [tool.poetry.dependencies] -python = "^3.8" -pandas = ">=2.0.0,<3.0.0" -openpyxl = ">=3.1.0,<4.0.0" -requests = ">=2.31.0,<3.0.0" -python-dotenv = ">=1.0.0,<2.0.0" -python-dateutil = ">=2.8.2,<3.0.0" +python = "^3.9" +pandas = "^2.0.0" +openpyxl = "^3.1.0" +requests = "^2.31.0" +python-dateutil = "^2.8.2" +pydantic = "^1.10.12" +python-dotenv = "^1.0.0" +aiohttp = "^3.8.0" +cachetools = ">=5.3.0" +pytest-asyncio = ">=0.21.0" +black = ">=23.12.1" -[poetry.group.dev.dependencies] -pytest = "^7.0.0" -pytest-cov = "^4.0.0" -flake8 = "^5.0.0" -black = "^22.0.0" -isort = "^5.10.0" -mypy = "^0.991.0" +[tool.poetry.group.dev.dependencies] +pytest = "^7.4.0" +pytest-mock = "^3.11.0" +pytest-cov = "^4.1.0" -[tool.black] -line-length = 119 -exclude = ''' -/( - \.git - | __pycache__ -)/ -''' - -[tool.isort] -line_length = 119 - -[tool.mypy] -disallow_untyped_defs = true -warn_return_any = true -exclude = ['venv/'] diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index c99e92b..0000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Для разработки -pytest>=7.4.0,<8.0.0 -pytest-cov>=4.1.0,<5.0.0 -pytest-mock>=3.11.1,<4.0.0 -flake8>=6.0.0,<7.0.0 -black>=23.7.0,<24.0.0 -isort>=5.12.0,<6.0.0 -mypy>=1.5.0,<2.0.0 -pre-commit>=3.3.0,<4.0.0 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 9eaa878..61b4b19 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,12 @@ -pandas>=2.0.0,<3.0.0 -openpyxl>=3.1.0,<4.0.0 -requests>=2.31.0,<3.0.0 -python-dotenv>=1.0.0,<2.0.0 -python-dateutil>=2.8.2,<3.0.0 +pandas>=2.0.0 +openpyxl>=3.1.0 +requests>=2.31.0 +python-dateutil>=2.8.2 +pydantic>=2.0.0 +python-dotenv>=1.0.0 +aiohttp>=3.8.0 +cachetools>=5.3.0 +pytest>=7.4.0 +pytest-asyncio>=0.21.0 +pytest-mock>=3.11.0 +black>=23.12.1 diff --git a/run_app.py b/run_app.py new file mode 100644 index 0000000..0323643 --- /dev/null +++ b/run_app.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +""" +Упрощенный запуск приложения +""" + +import os +import sys +from pathlib import Path + +# Добавляем src в путь Python +sys.path.insert(0, str(Path(__file__).parent / 'src')) + +# Создаем необходимые директории +Path('logs').mkdir(exist_ok=True) +Path('reports').mkdir(exist_ok=True) +Path('data').mkdir(exist_ok=True) + + +def main(): + """Основная функция""" + try: + from src.views import main_page, events_page + from src.utils import load_transactions, load_user_settings + + print("🚀 Запуск Transaction Analyzer...") + + # Загрузка данных + df = load_transactions() + settings = load_user_settings() + + # Генерация данных для веб-страниц + current_time = "2023-12-20 15:30:00" + + print("\n=== ГЛАВНАЯ СТРАНИЦА ===") + main_data = main_page(current_time) + print(f"Приветствие: {main_data['greeting']}") + print(f"Карты: {len(main_data['cards'])}") + print(f"Топ транзакций: {len(main_data['top_transactions'])}") + + print("\n=== СТРАНИЦА СОБЫТИЙ ===") + events_data = events_page("2023-12-20", "M") + print(f"Расходы: {events_data['expenses']['total_amount']}") + print(f"Поступления: {events_data['income']['total_amount']}") + + print("\n✅ Приложение успешно запущено!") + + except Exception as e: + print(f"❌ Ошибка: {e}") + import traceback + traceback.print_exc() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/api_client.py b/src/api_client.py new file mode 100644 index 0000000..0629b72 --- /dev/null +++ b/src/api_client.py @@ -0,0 +1,214 @@ +import aiohttp +import asyncio +import json +import cachetools +from datetime import datetime +from typing import Dict, List, Any, Optional +import logging +from .config import settings + +logger = logging.getLogger(__name__) + +# Кэш для API запросов +cache = cachetools.TTLCache(maxsize=100, ttl=settings.cache_ttl) + + +class APIClient: + """Клиент для работы с внешними API""" + + def __init__(self): + self.session: Optional[aiohttp.ClientSession] = None + + async def __aenter__(self): + self.session = aiohttp.ClientSession() + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + if self.session: + await self.session.close() + + async def get_currency_rates(self, currencies: List[str]) -> List[Dict[str, float]]: + """Получение актуальных курсов валют""" + cache_key = f"currency_rates_{'_'.join(sorted(currencies))}" + + if cache_key in cache: + logger.info("Using cached currency rates") + return cache[cache_key] + + try: + # Пробуем разные API по очереди + rates = await self._get_currency_rates_exchangerate(currencies) + if not rates: + rates = await self._get_currency_rates_currencyapi(currencies) + + if rates: + cache[cache_key] = rates + return rates + else: + # Fallback к статическим данным + return self._get_fallback_currency_rates(currencies) + + except Exception as e: + logger.error(f"Error fetching currency rates: {e}") + return self._get_fallback_currency_rates(currencies) + + async def _get_currency_rates_exchangerate(self, currencies: List[str]) -> List[Dict[str, float]]: + """Получение курсов валют через ExchangeRate API""" + try: + base_currency = "RUB" + target_currencies = [c for c in currencies if c != "RUB"] + + if not target_currencies: + return [{"currency": "RUB", "rate": 1.0}] + + async with self.session.get( + f"{settings.exchangerate_url}?base={base_currency}&symbols={','.join(target_currencies)}" + ) as response: + if response.status == 200: + data = await response.json() + rates = [{"currency": "RUB", "rate": 1.0}] + + for currency in target_currencies: + if currency in data.get("rates", {}): + rate = 1 / data["rates"][currency] # Конвертируем из RUB в валюту + rates.append({"currency": currency, "rate": round(rate, 4)}) + + return rates + except Exception as e: + logger.warning(f"ExchangeRate API failed: {e}") + return [] + + async def _get_currency_rates_currencyapi(self, currencies: List[str]) -> List[Dict[str, float]]: + """Получение курсов валют через CurrencyAPI""" + try: + if not settings.currency_api_key: + return [] + + base_currency = "RUB" + target_currencies = [c for c in currencies if c != "RUB"] + + async with self.session.get( + f"{settings.currency_api_url}?apikey={settings.currency_api_key}&base_currency={base_currency}" + ) as response: + if response.status == 200: + data = await response.json() + rates = [{"currency": "RUB", "rate": 1.0}] + + for currency in target_currencies: + if currency in data.get("data", {}): + rate = 1 / data["data"][currency]["value"] + rates.append({"currency": currency, "rate": round(rate, 4)}) + + return rates + except Exception as e: + logger.warning(f"CurrencyAPI failed: {e}") + return [] + + async def get_stock_prices(self, stocks: List[str]) -> List[Dict[str, float]]: + """Получение актуальных цен акций""" + cache_key = f"stock_prices_{'_'.join(sorted(stocks))}" + + if cache_key in cache: + logger.info("Using cached stock prices") + return cache[cache_key] + + try: + prices = await self._get_stock_prices_alphavantage(stocks) + if prices: + cache[cache_key] = prices + return prices + else: + return self._get_fallback_stock_prices(stocks) + + except Exception as e: + logger.error(f"Error fetching stock prices: {e}") + return self._get_fallback_stock_prices(stocks) + + async def _get_stock_prices_alphavantage(self, stocks: List[str]) -> List[Dict[str, float]]: + """Получение цен акций через Alpha Vantage""" + try: + prices = [] + + for stock in stocks: + # Для демо-режима используем лимитированные запросы + if settings.alpha_vantage_api_key == "demo": + # Используем fallback данные для демо + continue + + async with self.session.get( + f"{settings.alpha_vantage_url}?function=GLOBAL_QUOTE&symbol={stock}&apikey={settings.alpha_vantage_api_key}" + ) as response: + if response.status == 200: + data = await response.json() + quote = data.get("Global Quote", {}) + if quote: + price = float(quote.get("05. price", 0)) + prices.append({"stock": stock, "price": round(price, 2)}) + + return prices if prices else [] + + except Exception as e: + logger.warning(f"Alpha Vantage API failed: {e}") + return [] + + def _get_fallback_currency_rates(self, currencies: List[str]) -> List[Dict[str, float]]: + """Резервные данные о курсах валют""" + fallback_rates = { + "USD": 93.45, + "EUR": 101.23, + "GBP": 117.89, + "CNY": 12.87, + "JPY": 0.63, + "RUB": 1.0 + } + + rates = [] + for currency in currencies: + rate = fallback_rates.get(currency, 1.0) + rates.append({"currency": currency, "rate": rate}) + + logger.info("Using fallback currency rates") + return rates + + def _get_fallback_stock_prices(self, stocks: List[str]) -> List[Dict[str, float]]: + """Резервные данные о ценах акций""" + fallback_prices = { + "AAPL": 178.72, + "AMZN": 145.63, + "GOOGL": 138.21, + "MSFT": 374.51, + "TSLA": 235.49, + "META": 351.95, + "NVDA": 477.76, + "NFLX": 485.13 + } + + prices = [] + for stock in stocks: + price = fallback_prices.get(stock, 0.0) + if price > 0: + prices.append({"stock": stock, "price": price}) + + logger.info("Using fallback stock prices") + return prices + + +# Синхронная обертка для удобства использования +class SyncAPIClient: + """Синхронная обертка для APIClient""" + + @staticmethod + def get_currency_rates(currencies: List[str]) -> List[Dict[str, float]]: + async def _fetch(): + async with APIClient() as client: + return await client.get_currency_rates(currencies) + + return asyncio.run(_fetch()) + + @staticmethod + def get_stock_prices(stocks: List[str]) -> List[Dict[str, float]]: + async def _fetch(): + async with APIClient() as client: + return await client.get_stock_prices(stocks) + + return asyncio.run(_fetch()) diff --git a/src/config.py b/src/config.py new file mode 100644 index 0000000..7f3cdd4 --- /dev/null +++ b/src/config.py @@ -0,0 +1,42 @@ +import os +from typing import List, Dict, Any +from pydantic import BaseSettings, validator +from dotenv import load_dotenv + +load_dotenv() + + +class Settings(BaseSettings): + """Настройки приложения""" + + # API Keys + alpha_vantage_api_key: str = os.getenv("ALPHA_VANTAGE_API_KEY", "demo") + exchangerate_api_key: str = os.getenv("EXCHANGERATE_API_KEY", "") + currency_api_key: str = os.getenv("CURRENCY_API_KEY", "") + + # Paths + data_file_path: str = "data/operations.xlsx" + user_settings_path: str = "user_settings.json" + + # API endpoints + alpha_vantage_url: str = "https://www.alphavantage.co/query" + exchangerate_url: str = "https://api.exchangerate.host/latest" + currency_api_url: str = "https://api.currencyapi.com/v3/latest" + + # Cache settings + cache_ttl: int = 3600 # 1 hour + + # Date formats to try + date_formats: List[str] = [ + "%d.%m.%Y", + "%Y-%m-%d", + "%d/%m/%Y", + "%m/%d/%Y", + "%Y.%m.%d" + ] + + class Config: + env_file = ".env" + + +settings = Settings() diff --git a/src/logs/transaction_analyzer.log b/src/logs/transaction_analyzer.log new file mode 100644 index 0000000..d827da6 --- /dev/null +++ b/src/logs/transaction_analyzer.log @@ -0,0 +1,60 @@ +2025-09-27 22:32:18,219 - __main__ - INFO - ... +2025-09-27 22:32:18,219 - src.utils - INFO - data/operations.xlsx +2025-09-27 22:32:18,220 - src.utils - ERROR - : [Errno 2] No such file or directory: 'data/operations.xlsx' +2025-09-27 22:32:18,220 - __main__ - ERROR - : [Errno 2] No such file or directory: 'data/operations.xlsx' +2025-09-27 22:32:20,607 - __main__ - INFO - ... +2025-09-27 22:32:20,608 - src.utils - INFO - data/operations.xlsx +2025-09-27 22:32:20,608 - src.utils - ERROR - : [Errno 2] No such file or directory: 'data/operations.xlsx' +2025-09-27 22:32:20,609 - __main__ - ERROR - : [Errno 2] No such file or directory: 'data/operations.xlsx' +2025-09-27 22:36:18,943 - __main__ - INFO - ... +2025-09-27 22:36:18,943 - src.utils - INFO - data/operations.xlsx +2025-09-27 22:36:18,943 - src.utils - ERROR - : [Errno 2] No such file or directory: 'data/operations.xlsx' +2025-09-27 22:36:18,943 - __main__ - ERROR - : [Errno 2] No such file or directory: 'data/operations.xlsx' +2025-09-27 22:39:34,228 - __main__ - INFO - ... +2025-09-27 22:39:34,229 - src.utils - INFO - data/operations.xlsx +2025-09-27 22:39:34,229 - src.utils - ERROR - : [Errno 2] No such file or directory: 'data/operations.xlsx' +2025-09-27 22:39:34,229 - __main__ - ERROR - : [Errno 2] No such file or directory: 'data/operations.xlsx' +2025-09-27 22:39:38,088 - __main__ - INFO - ... +2025-09-27 22:39:38,088 - src.utils - INFO - data/operations.xlsx +2025-09-27 22:39:38,088 - src.utils - ERROR - : [Errno 2] No such file or directory: 'data/operations.xlsx' +2025-09-27 22:39:38,088 - __main__ - ERROR - : [Errno 2] No such file or directory: 'data/operations.xlsx' +2025-09-27 22:40:17,643 - __main__ - INFO - ... +2025-09-27 22:40:17,644 - src.utils - INFO - data/operations.xlsx +2025-09-27 22:40:17,645 - src.utils - ERROR - : [Errno 2] No such file or directory: 'data/operations.xlsx' +2025-09-27 22:40:17,645 - __main__ - ERROR - : [Errno 2] No such file or directory: 'data/operations.xlsx' +2025-09-27 22:40:50,211 - __main__ - INFO - ... +2025-09-27 22:40:50,212 - src.utils - INFO - data/operations.xlsx +2025-09-27 22:40:50,213 - src.utils - ERROR - : [Errno 2] No such file or directory: 'data/operations.xlsx' +2025-09-27 22:40:50,213 - __main__ - ERROR - : [Errno 2] No such file or directory: 'data/operations.xlsx' +2025-09-27 22:41:04,925 - __main__ - INFO - ... +2025-09-27 22:41:04,926 - src.utils - INFO - data/operations.xlsx +2025-09-27 22:41:04,927 - src.utils - ERROR - : [Errno 2] No such file or directory: 'data/operations.xlsx' +2025-09-27 22:41:04,927 - __main__ - ERROR - : [Errno 2] No such file or directory: 'data/operations.xlsx' +2025-09-27 22:42:25,867 - __main__ - INFO - ... +2025-09-27 22:42:25,867 - src.utils - INFO - data/operations.xlsx +2025-09-27 22:42:25,867 - src.utils - ERROR - : [Errno 2] No such file or directory: 'data/operations.xlsx' +2025-09-27 22:42:25,867 - __main__ - ERROR - : [Errno 2] No such file or directory: 'data/operations.xlsx' +2025-09-27 22:42:43,338 - __main__ - INFO - ... +2025-09-27 22:42:43,338 - src.utils - INFO - data/operations.xlsx +2025-09-27 22:42:43,338 - src.utils - ERROR - : [Errno 2] No such file or directory: 'data/operations.xlsx' +2025-09-27 22:42:43,338 - __main__ - ERROR - : [Errno 2] No such file or directory: 'data/operations.xlsx' +2025-09-27 23:12:13,905 - __main__ - INFO - ... +2025-09-27 23:12:13,905 - src.utils - INFO - data/operations.xlsx +2025-09-27 23:12:13,906 - src.utils - ERROR - : [Errno 2] No such file or directory: 'data/operations.xlsx' +2025-09-27 23:12:13,906 - __main__ - ERROR - : [Errno 2] No such file or directory: 'data/operations.xlsx' +2025-09-27 23:12:36,049 - __main__ - INFO - ... +2025-09-27 23:12:36,049 - src.utils - INFO - data/operations.xlsx +2025-09-27 23:12:36,049 - src.utils - ERROR - : [Errno 2] No such file or directory: 'data/operations.xlsx' +2025-09-27 23:12:36,049 - __main__ - ERROR - : [Errno 2] No such file or directory: 'data/operations.xlsx' +2025-09-27 23:21:59,058 - __main__ - INFO - ... +2025-09-27 23:21:59,058 - src.utils - INFO - data/operations.xlsx +2025-09-27 23:21:59,058 - src.utils - ERROR - : [Errno 2] No such file or directory: 'data/operations.xlsx' +2025-09-27 23:21:59,058 - __main__ - ERROR - : [Errno 2] No such file or directory: 'data/operations.xlsx' +2025-09-27 23:22:19,758 - __main__ - INFO - ... +2025-09-27 23:22:19,759 - src.utils - INFO - data/operations.xlsx +2025-09-27 23:22:19,759 - src.utils - ERROR - : [Errno 2] No such file or directory: 'data/operations.xlsx' +2025-09-27 23:22:19,759 - __main__ - ERROR - : [Errno 2] No such file or directory: 'data/operations.xlsx' +2025-09-27 23:34:17,097 - __main__ - INFO - ... +2025-09-27 23:34:17,097 - src.utils - INFO - data/operations.xlsx +2025-09-27 23:34:17,097 - src.utils - ERROR - : [Errno 2] No such file or directory: 'data/operations.xlsx' +2025-09-27 23:34:17,097 - __main__ - ERROR - : [Errno 2] No such file or directory: 'data/operations.xlsx' diff --git a/src/main.py b/src/main.py index e42784b..54fcec7 100644 --- a/src/main.py +++ b/src/main.py @@ -1,52 +1,180 @@ +#!/usr/bin/env python3 +""" +Основной модуль приложения для анализа транзакций +""" + +import os +import logging +import argparse import json -import pandas as pd -from .utils import load_transactions -from .views import home_page -from .services import investment_bank, simple_search, find_phone_transactions -from .reports import spending_by_category, spending_by_weekday +from datetime import datetime +from pathlib import Path + +# Создаем необходимые директории +Path('logs').mkdir(exist_ok=True) +Path('reports').mkdir(exist_ok=True) +Path('data').mkdir(exist_ok=True) + +# Настройка логирования +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler('logs/transaction_analyzer.log'), + logging.StreamHandler() + ] +) + +logger = logging.getLogger(__name__) + + +class TransactionAnalyzer: + """Основной класс приложения""" + + def __init__(self, data_file: str = None): + from src.config import settings + self.data_file = data_file or settings.data_file_path + self.transactions_df = None + self.settings = None + + def load_data(self): + """Загрузка данных""" + from src.utils import load_transactions, load_user_settings + logger.info("Загрузка данных...") + self.transactions_df = load_transactions(self.data_file) + self.settings = load_user_settings() + logger.info("Данные успешно загружены") + + def generate_main_page(self, date_time: str) -> dict: + """Генерация данных для главной страницы""" + from src.views import main_page + return main_page(date_time, self.data_file) + + def generate_events_page(self, date: str, period: str = 'M') -> dict: + """Генерация данных для страницы событий""" + from src.views import events_page + return events_page(date, period, self.data_file) + + def analyze_cashback_categories(self, year: int, month: int) -> dict: + """Анализ выгодных категорий кешбэка""" + from src.services import profitable_cashback_categories + cashback_rules = self.settings.get('cashback_rules', {'default': 0.01}) + return profitable_cashback_categories( + self.transactions_df, year, month, cashback_rules + ) + + def calculate_investment(self, month: str, limit: int) -> float: + """Расчет инвесткопилки""" + from src.services import investment_bank + transactions_list = self.transactions_df.to_dict('records') + return investment_bank(month, transactions_list, limit) + + def search_transactions(self, search_string: str) -> list: + """Поиск транзакций""" + from src.services import simple_search + transactions_list = self.transactions_df.to_dict('records') + return simple_search(transactions_list, search_string) + + def generate_reports(self): + """Генерация всех отчетов""" + from src.reports import ReportGenerator # Добавлен импорт + + reports = {} + + # Отчет по категориям + reports['spending_by_category'] = ReportGenerator.spending_by_category( + self.transactions_df, 'Супермаркеты' + ) + + # Отчет по дням недели + reports['spending_by_weekday'] = ReportGenerator.spending_by_weekday( + self.transactions_df + ) + + # Отчет по рабочим/выходным дням + reports['spending_by_workday'] = ReportGenerator.spending_by_workday( + self.transactions_df + ) + + # Сводный отчет + reports['monthly_summary'] = ReportGenerator.monthly_summary( + self.transactions_df + ) + + return reports def main(): - """Основная функция приложения.""" + """Основная функция приложения""" + parser = argparse.ArgumentParser(description='Анализатор банковских транзакций') + parser.add_argument('--data-file', help='Путь к файлу с транзакциями') + parser.add_argument('--command', choices=['web', 'report', 'analyze', 'test'], + default='web', help='Режим работы') + + args = parser.parse_args() + try: - # Загрузка данных - transactions_df = load_transactions('data/operations.xlsx') + # Инициализация анализатора + analyzer = TransactionAnalyzer(args.data_file) + analyzer.load_data() + + if args.command == 'web': + # Пример генерации данных для веб-страниц + current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + + main_data = analyzer.generate_main_page(current_time) + events_data = analyzer.generate_events_page(current_time.split()[0]) - # Загрузка настроек пользователя - with open('user_settings.json', 'r') as f: - user_settings = json.load(f) + print("=== ДАННЫЕ ДЛЯ ГЛАВНОЙ СТРАНИЦЫ ===") + print(json.dumps(main_data, ensure_ascii=False, indent=2)) - # Демонстрация функциональности - print("=== Анализатор транзакций ===") + print("\n=== ДАННЫЕ ДЛЯ СТРАНИЦЫ СОБЫТИЙ ===") + print(json.dumps(events_data, ensure_ascii=False, indent=2)) - # Главная страница - home_data = home_page("2023-12-20 15:30:00", transactions_df, user_settings) - print("Главная страница сгенерирована") + elif args.command == 'report': + # Генерация отчетов + reports = analyzer.generate_reports() + print("=== ОТЧЕТЫ СГЕНЕРИРОВАНЫ ===") - # Конвертация DataFrame в список словарей для сервисов - transactions_list = transactions_df.to_dict('records') + for report_name, report_data in reports.items(): + print(f"\n--- {report_name} ---") + if isinstance(report_data, dict): + print(json.dumps(report_data, ensure_ascii=False, indent=2)) + elif hasattr(report_data, 'head'): # DataFrame + print(report_data.head()) + else: + print(f"Тип данных: {type(report_data)}") + print(report_data) - # Сервисы - savings = investment_bank("2023-12", transactions_list, 50) - print(f"Инвесткопилка: {savings} руб.") + print(f"\n📊 Отчеты сохранены в папке 'reports/'") - search_results = simple_search("кафе", transactions_list) - print(f"Найдено транзакций по запросу 'кафе': {len(search_results)}") + elif args.command == 'analyze': + # Анализ данных + current_year = datetime.now().year + current_month = datetime.now().month - phone_transactions = find_phone_transactions(transactions_list) - print(f"Найдено транзакций с телефонами: {len(phone_transactions)}") + cashback_analysis = analyzer.analyze_cashback_categories(current_year, current_month) + investment = analyzer.calculate_investment( + f"{current_year}-{current_month:02d}", 50 + ) - # Отчеты - category_spending = spending_by_category(transactions_df, "Супермаркеты") - print("Отчет по тратам по категории создан") + print("=== АНАЛИЗ ДАННЫХ ===") + print(f"Выгодные категории кешбэка: {cashback_analysis}") + print(f"Инвесткопилка за месяц: {investment} руб.") - weekday_spending = spending_by_weekday(transactions_df) - print("Отчет по тратам по дням недели создан") + elif args.command == 'test': + # Простой тест функциональности + print("=== ТЕСТ ФУНКЦИОНАЛЬНОСТИ ===") + print(f"Загружено транзакций: {len(analyzer.transactions_df)}") + print(f"Настройки: {list(analyzer.settings.keys())}") - print("\nВсе функции выполнены успешно!") + # Тест поиска + search_results = analyzer.search_transactions("магазин") + print(f"Результатов поиска 'магазин': {len(search_results)}") except Exception as e: - print(f"Ошибка: {e}") + logger.error(f"Ошибка приложения: {e}") + raise if __name__ == "__main__": diff --git a/src/reports.py b/src/reports.py index 4e47cbf..f2a4727 100644 --- a/src/reports.py +++ b/src/reports.py @@ -1,33 +1,42 @@ +import pandas as pd import json import logging -import pandas as pd from datetime import datetime, timedelta -from typing import Dict, List, Any, Optional, Callable +from typing import Optional, Callable, Any, Dict from functools import wraps +import os +from .utils import load_transactions logger = logging.getLogger(__name__) -def report(func: Optional[Callable] = None, filename: Optional[str] = None): - """Декоратор для записи результатов отчетов в файл.""" +def report_decorator(filename: Optional[str] = None): + """Декоратор для сохранения отчетов в файл""" - def decorator(report_func): - @wraps(report_func) + def decorator(func: Callable) -> Callable: + @wraps(func) def wrapper(*args, **kwargs): - result = report_func(*args, **kwargs) + result = func(*args, **kwargs) - # Генерация имени файла - if filename: - file_path = filename + # Генерация имени файла если не указано + if filename is None: + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + func_name = func.__name__ + report_filename = f"reports/{func_name}_{timestamp}.json" else: - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - file_path = f"{report_func.__name__}_{timestamp}.json" + report_filename = filename + + # Создаем директорию если не существует + os.makedirs(os.path.dirname(report_filename), exist_ok=True) - # Запись в файл + # Сохранение результата try: - with open(file_path, 'w', encoding='utf-8') as f: - json.dump(result, f, ensure_ascii=False, indent=2) - logger.info(f"Отчет сохранен в файл: {file_path}") + with open(report_filename, 'w', encoding='utf-8') as f: + if isinstance(result, pd.DataFrame): + json.dump(result.to_dict('records'), f, ensure_ascii=False, indent=2) + else: + json.dump(result, f, ensure_ascii=False, indent=2) + logger.info(f"Отчет сохранен в {report_filename}") except Exception as e: logger.error(f"Ошибка сохранения отчета: {e}") @@ -35,70 +44,203 @@ def wrapper(*args, **kwargs): return wrapper - if func is None: - return decorator - else: - return decorator(func) - - -@report -def spending_by_category(transactions: pd.DataFrame, category: str, - date: Optional[str] = None) -> Dict[str, float]: - """Анализ трат по категории за последние 3 месяца.""" - try: - if date is None: - date = datetime.now().strftime("%Y-%m-%d") - - target_date = datetime.strptime(date, "%Y-%m-%d") - three_months_ago = target_date - timedelta(days=90) - - # Фильтрация транзакций - transactions['Дата операции'] = pd.to_datetime(transactions['Дата операции']) - filtered_df = transactions[ - (transactions['Дата операции'] >= three_months_ago) & - (transactions['Дата операции'] <= target_date) & - (transactions['Категория'] == category) - ] - - # Группировка по месяцам - filtered_df['Месяц'] = filtered_df['Дата операции'].dt.to_period('M') - monthly_spending = filtered_df.groupby('Месяц')['Сумма операции'].sum().abs() - - result = {str(month): round(amount, 2) for month, amount in monthly_spending.items()} - logger.info(f"Проанализированы траты по категории '{category}'") - - return result - except Exception as e: - logger.error(f"Ошибка анализа трат по категории: {e}") - return {} - - -@report -def spending_by_weekday(transactions: pd.DataFrame, date: Optional[str] = None) -> Dict[str, float]: - """Средние траты по дням недели за последние 3 месяца.""" - try: - if date is None: - date = datetime.now().strftime("%Y-%m-%d") - - target_date = datetime.strptime(date, "%Y-%m-%d") - three_months_ago = target_date - timedelta(days=90) - - # Фильтрация транзакций - transactions['Дата операции'] = pd.to_datetime(transactions['Дата операции']) - filtered_df = transactions[ - (transactions['Дата операции'] >= three_months_ago) & - (transactions['Дата операции'] <= target_date) - ] - - # Анализ по дням недели - filtered_df['День недели'] = filtered_df['Дата операции'].dt.day_name() - weekday_spending = filtered_df.groupby('День недели')['Сумма операции'].mean().abs() - - days_order = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] - result = {day: round(weekday_spending.get(day, 0), 2) for day in days_order} - - logger.info("Проанализированы траты по дням недели") - return result - except Exception as e: - logger.error(f"Ошибка анализа трат по дням недели: {e}") - return {} \ No newline at end of file + return decorator + + +class ReportGenerator: + """Генератор отчетов""" + + @staticmethod + @report_decorator() + def spending_by_category(transactions: pd.DataFrame, + category: str, + date: Optional[str] = None) -> pd.DataFrame: + """Траты по категории за последние 3 месяца""" + try: + if date is None: + target_date = datetime.now() + else: + target_date = datetime.strptime(date, '%Y-%m-%d') + + # Расчет даты начала периода (3 месяца назад) + start_date = target_date - timedelta(days=90) + + # Фильтрация данных + mask = (transactions['Дата операции'] >= start_date) & \ + (transactions['Дата операции'] <= target_date) & \ + (transactions['Категория'] == category) & \ + (transactions['Статус'] == 'OK') & \ + (transactions['Сумма операции'] > 0) + + filtered_data = transactions.loc[mask].copy() + + if filtered_data.empty: + return pd.DataFrame(columns=['Месяц', 'Сумма']) + + # Группировка по месяцам + filtered_data['Месяц'] = filtered_data['Дата операции'].dt.to_period('M') + monthly_spending = filtered_data.groupby('Месяц')['Сумма операции'].sum().reset_index() + monthly_spending['Месяц'] = monthly_spending['Месяц'].astype(str) + monthly_spending['Сумма'] = monthly_spending['Сумма операции'].round(2) + + return monthly_spending[['Месяц', 'Сумма']] + + except Exception as e: + logger.error(f"Ошибка в spending_by_category: {e}") + return pd.DataFrame() + + @staticmethod + @report_decorator() + def spending_by_weekday(transactions: pd.DataFrame, + date: Optional[str] = None) -> pd.DataFrame: + """Средние траты по дням недели""" + try: + if date is None: + target_date = datetime.now() + else: + target_date = datetime.strptime(date, '%Y-%m-%d') + + start_date = target_date - timedelta(days=90) + + mask = (transactions['Дата операции'] >= start_date) & \ + (transactions['Дата операции'] <= target_date) & \ + (transactions['Статус'] == 'OK') & \ + (transactions['Сумма операции'] > 0) + + filtered_data = transactions.loc[mask].copy() + + if filtered_data.empty: + return pd.DataFrame(columns=['День недели', 'Средняя сумма']) + + # Маппинг дней недели на русский + day_mapping = { + 0: 'Понедельник', + 1: 'Вторник', + 2: 'Среда', + 3: 'Четверг', + 4: 'Пятница', + 5: 'Суббота', + 6: 'Воскресенье' + } + + filtered_data['День недели'] = filtered_data['Дата операции'].dt.weekday.map(day_mapping) + avg_spending = filtered_data.groupby('День недели')['Сумма операции'].mean().reset_index() + avg_spending['Средняя сумма'] = avg_spending['Сумма операции'].round(2) + + # Сортировка по порядку дней недели + day_order = list(day_mapping.values()) + avg_spending['День недели'] = pd.Categorical(avg_spending['День недели'], categories=day_order, + ordered=True) + avg_spending = avg_spending.sort_values('День недели') + + return avg_spending[['День недели', 'Средняя сумма']] + + except Exception as e: + logger.error(f"Ошибка в spending_by_weekday: {e}") + return pd.DataFrame() + + @staticmethod + @report_decorator() + def spending_by_workday(transactions: pd.DataFrame, + date: Optional[str] = None) -> pd.DataFrame: + """Средние траты в рабочие/выходные дни""" + try: + if date is None: + target_date = datetime.now() + else: + target_date = datetime.strptime(date, '%Y-%m-%d') + + start_date = target_date - timedelta(days=90) + + mask = (transactions['Дата операции'] >= start_date) & \ + (transactions['Дата операции'] <= target_date) & \ + (transactions['Статус'] == 'OK') & \ + (transactions['Сумма операции'] > 0) + + filtered_data = transactions.loc[mask].copy() + + if filtered_data.empty: + return pd.DataFrame(columns=['Тип дня', 'Средняя сумма']) + + filtered_data['День недели'] = filtered_data['Дата операции'].dt.weekday + filtered_data['Тип дня'] = filtered_data['День недели'].apply( + lambda x: 'Выходной' if x >= 5 else 'Рабочий' + ) + + avg_spending = filtered_data.groupby('Тип дня')['Сумма операции'].mean().reset_index() + avg_spending['Средняя сумма'] = avg_spending['Сумма операции'].round(2) + + return avg_spending[['Тип дня', 'Средняя сумма']] + + except Exception as e: + logger.error(f"Ошибка в spending_by_workday: {e}") + return pd.DataFrame() + + @staticmethod + @report_decorator() + def monthly_summary(transactions: pd.DataFrame, + months: int = 6) -> Dict[str, Any]: + """Сводный отчет за несколько месяцев""" + try: + end_date = datetime.now() + start_date = end_date - timedelta(days=30 * months) + + mask = (transactions['Дата операции'] >= start_date) & \ + (transactions['Дата операции'] <= end_date) + + filtered_data = transactions.loc[mask].copy() + + if filtered_data.empty: + return {"error": "Нет данных за указанный период"} + + # Расходы по месяцам + filtered_data['Месяц'] = filtered_data['Дата операции'].dt.to_period('M') + expenses = filtered_data[filtered_data['Сумма операции'] > 0] + income = filtered_data[filtered_data['Сумма операции'] < 0] + + monthly_expenses = expenses.groupby('Месяц')['Сумма операции'].sum() + monthly_income = income.groupby('Месяц')['Сумма операции'].sum().abs() + + # Топ категории расходов + top_categories = expenses.groupby('Категория')['Сумма операции'].sum().nlargest(5) + + report = { + "period": f"{start_date.strftime('%Y-%m')} - {end_date.strftime('%Y-%m')}", + "total_expenses": round(expenses['Сумма операции'].sum(), 2), + "total_income": round(income['Сумма операции'].sum().abs(), 2), + "monthly_expenses": { + month.strftime('%Y-%m'): round(amount, 2) + for month, amount in monthly_expenses.items() + }, + "monthly_income": { + month.strftime('%Y-%m'): round(amount, 2) + for month, amount in monthly_income.items() + }, + "top_categories": { + category: round(amount, 2) + for category, amount in top_categories.items() + } + } + + return report + + except Exception as e: + logger.error(f"Ошибка в monthly_summary: {e}") + return {"error": str(e)} + + +# Функции-обертки для совместимости +def spending_by_category(transactions: pd.DataFrame, + category: str, + date: Optional[str] = None) -> pd.DataFrame: + return ReportGenerator.spending_by_category(transactions, category, date) + + +def spending_by_weekday(transactions: pd.DataFrame, + date: Optional[str] = None) -> pd.DataFrame: + return ReportGenerator.spending_by_weekday(transactions, date) + + +def spending_by_workday(transactions: pd.DataFrame, + date: Optional[str] = None) -> pd.DataFrame: + return ReportGenerator.spending_by_workday(transactions, date) diff --git a/src/services.py b/src/services.py index 0225262..63e0295 100644 --- a/src/services.py +++ b/src/services.py @@ -1,74 +1,233 @@ -import json -import logging import re +import logging from datetime import datetime -from typing import Dict, List, Any, Optional +from typing import Dict, List, Any, Callable, Optional +from functools import wraps, reduce +import operator +import pandas as pd logger = logging.getLogger(__name__) -def investment_bank(month: str, transactions: List[Dict[str, Any]], limit: int) -> float: - """Расчет суммы для инвесткопилки через округление трат.""" - try: - total_savings = 0.0 +class CashbackAnalyzer: + """Анализатор выгодности категорий кешбэка""" + + def __init__(self, cashback_rules: Dict[str, float]): + self.cashback_rules = cashback_rules + + def analyze_profitable_categories(self, data: pd.DataFrame, year: int, + month: int) -> Dict[str, float]: + """Анализ выгодности категорий повышенного кешбэка""" + try: + # Фильтрация данных по году и месяцу + mask = (data['Дата операции'].dt.year == year) & \ + (data['Дата операции'].dt.month == month) & \ + (data['Статус'] == 'OK') & \ + (data['Сумма операции'] > 0) # Только расходы + + filtered_data = data.loc[mask] + + if filtered_data.empty: + logger.warning(f"Нет данных за {month}/{year}") + return {} + + # Группировка по категориям и расчет потенциального кешбэка + cashback_analysis = {} + + for category in filtered_data['Категория'].unique(): + if pd.isna(category) or category == '': + continue + + category_data = filtered_data[filtered_data['Категория'] == category] + total_spent = category_data['Сумма операции'].sum() + + # Расчет кешбэка по сложной логике + from .utils import calculate_cashback + potential_cashback = sum( + calculate_cashback(row['Сумма операции'], category, self.cashback_rules) + for _, row in category_data.iterrows() + ) + + cashback_analysis[category] = round(potential_cashback, 2) + + # Сортировка по убыванию кешбэка + return dict(sorted(cashback_analysis.items(), + key=lambda x: x[1], reverse=True)) + + except Exception as e: + logger.error(f"Ошибка анализа категорий кешбэка: {e}") + return {} + + +class InvestmentCalculator: + """Калькулятор инвесткопилки""" + + @staticmethod + def investment_bank(month: str, transactions: List[Dict[str, Any]], + limit: int) -> float: + """Расчет суммы для инвесткопилки""" + try: + if limit not in [10, 50, 100]: + raise ValueError("Лимит округления должен быть 10, 50 или 100") + + target_month = datetime.strptime(month, '%Y-%m') + total_savings = 0.0 + + for transaction in transactions: + # Валидация транзакции + if not InvestmentCalculator._validate_transaction(transaction): + continue + + trans_date = datetime.strptime(transaction['Дата операции'], '%Y-%m-%d') + + if (trans_date.year == target_month.year and + trans_date.month == target_month.month): + + amount = float(transaction['Сумма операции']) + if amount > 0: # Только расходы + # Округление вверх до ближайшего кратного limit + rounded_amount = ((amount + limit - 1) // limit) * limit + savings = rounded_amount - amount + total_savings += savings + + return round(total_savings, 2) + + except Exception as e: + logger.error(f"Ошибка расчета инвесткопилки: {e}") + return 0.0 + + @staticmethod + def _validate_transaction(transaction: Dict[str, Any]) -> bool: + """Валидация транзакции""" + required_fields = ['Дата операции', 'Сумма операции'] + + for field in required_fields: + if field not in transaction: + logger.warning(f"Отсутствует поле {field} в транзакции") + return False + + try: + datetime.strptime(transaction['Дата операции'], '%Y-%m-%d') + float(transaction['Сумма операции']) + return True + except (ValueError, TypeError): + logger.warning("Некорректные данные в транзакции") + return False + + +class TransactionSearcher: + """Поисковик транзакций""" + + @staticmethod + def simple_search(transactions: List[Dict[str, Any]], + search_string: str) -> List[Dict[str, Any]]: + """Простой поиск по описанию и категории""" + if not search_string or len(search_string.strip()) < 2: + raise ValueError("Строка поиска должна содержать минимум 2 символа") + + def search_filter(transaction: Dict[str, Any]) -> bool: + description = str(transaction.get('Описание', '')).lower() + category = str(transaction.get('Категория', '')).lower() + search_lower = search_string.lower() + + return (search_lower in description or + search_lower in category) + + return list(filter(search_filter, transactions)) + + @staticmethod + def search_by_phone(transactions: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + """Поиск транзакций с телефонными номерами""" + # Паттерны для российских мобильных номеров + phone_patterns = [ + r'\+7\s?\d{3}\s?\d{3}[\s-]?\d{2}[\s-]?\d{2}', # +7 XXX XXX-XX-XX + r'8\s?\d{3}\s?\d{3}[\s-]?\d{2}[\s-]?\d{2}', # 8 XXX XXX-XX-XX + r'\d{3}[\s-]?\d{3}[\s-]?\d{2}[\s-]?\d{2}' # XXX XXX-XX-XX + ] + + def phone_filter(transaction: Dict[str, Any]) -> bool: + description = str(transaction.get('Описание', '')) + + for pattern in phone_patterns: + if re.search(pattern, description): + return True + return False + + return list(filter(phone_filter, transactions)) + + @staticmethod + def search_by_person_transfers(transactions: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + """Поиск переводов физическим лицам""" + # Паттерн для имени и фамилии с инициалом: "Имя Ф." + name_pattern = r'[А-Я][а-я]+\s[А-Я]\.' + + def transfer_filter(transaction: Dict[str, Any]) -> bool: + category = transaction.get('Категория', '') + description = str(transaction.get('Описание', '')) + + return (category == 'Переводы' and + bool(re.search(name_pattern, description))) + + return list(filter(transfer_filter, transactions)) + + +# Функциональные утилиты +def compose(*functions: Callable) -> Callable: + """Композиция функций""" + return reduce(lambda f, g: lambda x: f(g(x)), functions) + + +def pipe(value: Any, *functions: Callable) -> Any: + """Конвейерная обработка значения через функции""" + return compose(*functions)(value) + - for transaction in transactions: - # Проверяем, что транзакция в нужном месяце - trans_date = datetime.strptime(transaction['Дата операции'], '%Y-%m-%d') - if trans_date.strftime('%Y-%m') != month: - continue +# Декораторы для логирования +def log_service_call(service_name: str): + """Декоратор для логирования вызовов сервисов""" - amount = abs(transaction['Сумма операции']) - if amount > 0: # Только расходы - rounded_amount = _round_up_to_nearest(amount, limit) - savings = rounded_amount - amount - total_savings += savings + def decorator(func: Callable) -> Callable: + @wraps(func) + def wrapper(*args, **kwargs): + logger.info(f"Вызов сервиса {service_name}") + try: + result = func(*args, **kwargs) + logger.info(f"Сервис {service_name} выполнен успешно") + return result + except Exception as e: + logger.error(f"Ошибка в сервисе {service_name}: {e}") + raise - logger.info(f"Сумма для инвесткопилки за {month}: {total_savings}") - return round(total_savings, 2) - except Exception as e: - logger.error(f"Ошибка расчета инвесткопилки: {e}") - return 0.0 + return wrapper + return decorator -def _round_up_to_nearest(amount: float, limit: int) -> float: - """Округление до ближайшего кратного limit.""" - return ((amount + limit - 1) // limit) * limit +# Экспорт основных функций с декораторами +@log_service_call("profitable_cashback_categories") +def profitable_cashback_categories(data: pd.DataFrame, year: int, + month: int, cashback_rules: Dict[str, float]) -> Dict[str, float]: + analyzer = CashbackAnalyzer(cashback_rules) + return analyzer.analyze_profitable_categories(data, year, month) -def simple_search(query: str, transactions: List[Dict[str, Any]]) -> List[Dict[str, Any]]: - """Простой поиск транзакций по описанию или категории.""" - try: - results = [] - query_lower = query.lower() - for transaction in transactions: - description = transaction.get('Описание', '').lower() - category = transaction.get('Категория', '').lower() +@log_service_call("investment_bank") +def investment_bank(month: str, transactions: List[Dict[str, Any]], + limit: int) -> float: + return InvestmentCalculator.investment_bank(month, transactions, limit) - if query_lower in description or query_lower in category: - results.append(transaction) - logger.info(f"Найдено {len(results)} транзакций по запросу '{query}'") - return results - except Exception as e: - logger.error(f"Ошибка поиска: {e}") - return [] +@log_service_call("simple_search") +def simple_search(transactions: List[Dict[str, Any]], + search_string: str) -> List[Dict[str, Any]]: + return TransactionSearcher.simple_search(transactions, search_string) -def find_phone_transactions(transactions: List[Dict[str, Any]]) -> List[Dict[str, Any]]: - """Поиск транзакций с телефонными номерами в описании.""" - try: - phone_pattern = r'\+7\s?\(?\d{3}\)?\s?\d{3}[\s-]?\d{2}[\s-]?\d{2}' - results = [] +@log_service_call("search_by_phone") +def search_by_phone(transactions: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + return TransactionSearcher.search_by_phone(transactions) - for transaction in transactions: - description = transaction.get('Описание', '') - if re.search(phone_pattern, description): - results.append(transaction) - logger.info(f"Найдено {len(results)} транзакций с телефонными номерами") - return results - except Exception as e: - logger.error(f"Ошибка поиска телефонных номеров: {e}") - return [] +@log_service_call("search_by_person_transfers") +def search_by_person_transfers(transactions: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + return TransactionSearcher.search_by_person_transfers(transactions) \ No newline at end of file diff --git a/src/utils.py b/src/utils.py index 8a0a74d..593d344 100644 --- a/src/utils.py +++ b/src/utils.py @@ -1,103 +1,298 @@ +import pandas as pd import json import logging -import pandas as pd -import requests from datetime import datetime, timedelta -from typing import Dict, List, Any, Optional -import os -from dotenv import load_dotenv - -load_dotenv() +from typing import Dict, List, Any, Optional, Tuple, Union +from dateutil.parser import parse +from dateutil.relativedelta import relativedelta +import re +from .config import settings -logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) -def load_transactions(file_path: str) -> pd.DataFrame: - """Загрузка транзакций из Excel файла.""" +class DataValidator: + """Валидатор данных транзакций""" + + @staticmethod + def validate_transaction_data(df: pd.DataFrame) -> Tuple[pd.DataFrame, List[str]]: + """Проверка и очистка данных транзакций""" + errors = [] + + # Проверка обязательных колонок + required_columns = ['Дата операции', 'Сумма операции', 'Статус'] + missing_columns = [col for col in required_columns if col not in df.columns] + if missing_columns: + raise ValueError(f"Отсутствуют обязательные колонки: {missing_columns}") + + # Копируем данные для очистки + clean_df = df.copy() + + # Обработка дат + clean_df, date_errors = DataValidator._process_dates(clean_df) + errors.extend(date_errors) + + # Обработка числовых полей + clean_df, numeric_errors = DataValidator._process_numeric_fields(clean_df) + errors.extend(numeric_errors) + + # Обработка текстовых полей + clean_df, text_errors = DataValidator._process_text_fields(clean_df) + errors.extend(text_errors) + + # Удаление дубликатов + initial_count = len(clean_df) + clean_df = clean_df.drop_duplicates() + if len(clean_df) < initial_count: + errors.append(f"Удалено {initial_count - len(clean_df)} дубликатов") + + # Удаление строк с критическими ошибками + initial_count = len(clean_df) + clean_df = clean_df.dropna(subset=['Дата операции', 'Сумма операции', 'Статус']) + if len(clean_df) < initial_count: + errors.append(f"Удалено {initial_count - len(clean_df)} строк с некорректными данными") + + return clean_df, errors + + @staticmethod + def _process_dates(df: pd.DataFrame) -> Tuple[pd.DataFrame, List[str]]: + """Обработка и валидация дат""" + errors = [] + clean_df = df.copy() + + date_columns = ['Дата операции', 'Дата платежа'] + + for col in date_columns: + if col not in clean_df.columns: + continue + + original_non_null = clean_df[col].notna().sum() + + # Пробуем разные форматы дат + for date_format in settings.date_formats: + try: + clean_df[col] = pd.to_datetime( + clean_df[col], + format=date_format, + errors='coerce' + ) + # Если удалось преобразовать большинство дат, используем этот формат + if clean_df[col].notna().sum() > original_non_null * 0.8: + break + except: + continue + + # Убираем устаревший параметр infer_datetime_format + # Просто используем errors='coerce' для оставшихся проблемных значений + if clean_df[col].isna().any(): + clean_df[col] = pd.to_datetime(clean_df[col], errors='coerce') + + # Проверяем разумность дат (не в будущем и не слишком в прошлом) + if col in clean_df.columns and clean_df[col].notna().any(): + max_date = datetime.now() + timedelta(days=1) # Завтра + min_date = datetime(2000, 1, 1) # 2000 год + + invalid_dates = clean_df[ + (clean_df[col] > max_date) | (clean_df[col] < min_date) + ] + + if len(invalid_dates) > 0: + errors.append(f"Найдено {len(invalid_dates)} некорректных дат в колонке {col}") + clean_df.loc[invalid_dates.index, col] = pd.NaT + + return clean_df, errors + + @staticmethod + def _process_numeric_fields(df: pd.DataFrame) -> Tuple[pd.DataFrame, List[str]]: + """Обработка числовых полей""" + errors = [] + clean_df = df.copy() + + numeric_columns = ['Сумма операции', 'Сумма платежа', 'Кешбэк', 'Бонусы (включая кешбэк)', + 'Округление на «Инвесткопилку»', 'Сумма операции с округлением'] + + for col in numeric_columns: + if col not in clean_df.columns: + continue + + # Заменяем запятые на точки и преобразуем в числа + clean_df[col] = pd.to_numeric( + clean_df[col].astype(str).str.replace(',', '.'), + errors='coerce' + ) + + # Проверяем на выбросы (суммы больше 10 млн) + if clean_df[col].notna().any(): + outliers = clean_df[clean_df[col].abs() > 10000000] + if len(outliers) > 0: + errors.append(f"Найдено {len(outliers)} выбросов в колонке {col}") + + return clean_df, errors + + @staticmethod + def _process_text_fields(df: pd.DataFrame) -> Tuple[pd.DataFrame, List[str]]: + """Обработка текстовых полей""" + errors = [] + clean_df = df.copy() + + text_columns = ['Статус', 'Категория', 'Описание', 'Номер карты'] + + for col in text_columns: + if col not in clean_df.columns: + continue + + clean_df[col] = clean_df[col].astype(str).str.strip() + + # Замена NaN строк + clean_df[col] = clean_df[col].replace('nan', '').replace('None', '') + + # Проверка на слишком длинные тексты + if col == 'Описание': + too_long = clean_df[clean_df[col].str.len() > 500] + if len(too_long) > 0: + errors.append(f"Найдено {len(too_long)} очень длинных описаний") + clean_df.loc[too_long.index, col] = clean_df.loc[too_long.index, col].str[:500] + + return clean_df, errors + + +def load_transactions(file_path: str = settings.data_file_path) -> pd.DataFrame: + """Загрузка и валидация транзакций из Excel файла""" try: + logger.info(f"Загрузка данных из {file_path}") + + # Чтение файла df = pd.read_excel(file_path) - logger.info(f"Успешно загружено {len(df)} транзакций") - return df + + if df.empty: + raise ValueError("Файл не содержит данных") + + # Валидация и очистка данных + clean_df, errors = DataValidator.validate_transaction_data(df) + + if errors: + logger.warning(f"Обнаружены проблемы при загрузке данных: {errors}") + + logger.info(f"Успешно загружено {len(clean_df)} транзакций") + return clean_df + except Exception as e: - logger.error(f"Ошибка загрузки файла: {e}") + logger.error(f"Ошибка загрузки транзакций: {e}") raise -def filter_transactions_by_date(df: pd.DataFrame, date_str: str) -> pd.DataFrame: - """Фильтрация транзакций по дате.""" +def get_date_range(date_str: str, period: str = 'M') -> Tuple[datetime, datetime]: + """Получение диапазона дат для анализа""" try: - target_date = datetime.strptime(date_str, "%Y-%m-%d %H:%M:%S") - start_of_month = target_date.replace(day=1, hour=0, minute=0, second=0) + # Парсим дату с учетом времени + if ' ' in date_str: + date = datetime.strptime(date_str, '%Y-%m-%d %H:%M:%S') + else: + date = datetime.strptime(date_str, '%Y-%m-%d') - df['Дата операции'] = pd.to_datetime(df['Дата операции']) - filtered_df = df[(df['Дата операции'] >= start_of_month) & - (df['Дата операции'] <= target_date)] + if period == 'W': # Неделя + start_date = date - timedelta(days=date.weekday()) + start_date = start_date.replace(hour=0, minute=0, second=0, microsecond=0) + end_date = start_date + timedelta(days=6) + elif period == 'M': # Месяц + start_date = date.replace(day=1, hour=0, minute=0, second=0, microsecond=0) + next_month = date.replace(day=28) + timedelta(days=4) + end_date = min(next_month.replace(day=1) - timedelta(days=1), date) + elif period == 'Y': # Год + start_date = date.replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0) + end_date = date + elif period == 'ALL': # Все данные + start_date = datetime(2000, 1, 1) + end_date = date + else: + raise ValueError(f"Неизвестный период: {period}") - logger.info(f"Отфильтровано {len(filtered_df)} транзакций за период") - return filtered_df - except Exception as e: - logger.error(f"Ошибка фильтрации по дате: {e}") + return start_date, end_date + + except ValueError as e: + logger.error(f"Ошибка парсинга даты {date_str}: {e}") raise +def filter_transactions_by_date(df: pd.DataFrame, start_date: datetime, + end_date: datetime) -> pd.DataFrame: + """Фильтрация транзакций по диапазону дат""" + mask = (df['Дата операции'] >= start_date) & (df['Дата операции'] <= end_date) + filtered_df = df.loc[mask].copy() + + logger.info(f"Отфильтровано {len(filtered_df)} транзакций за период {start_date.date()} - {end_date.date()}") + return filtered_df + + def get_greeting(time_str: str) -> str: - """Получение приветствия в зависимости от времени суток.""" + """Получение приветствия в зависимости от времени""" try: - time_obj = datetime.strptime(time_str, "%Y-%m-%d %H:%M:%S").time() + if ' ' in time_str: + hour = datetime.strptime(time_str, '%Y-%m-%d %H:%M:%S').hour + else: + hour = datetime.strptime(time_str, '%Y-%m-%d').hour - if time_obj.hour < 6: - return "Доброй ночи" - elif time_obj.hour < 12: + if 5 <= hour < 12: return "Доброе утро" - elif time_obj.hour < 18: + elif 12 <= hour < 17: return "Добрый день" - else: + elif 17 <= hour < 23: return "Добрый вечер" - except Exception as e: - logger.error(f"Ошибка определения приветствия: {e}") - return "Добрый день" + else: + return "Доброй ночи" + except ValueError: + return "Добрый день" # По умолчанию -def get_currency_rates(currencies: List[str]) -> List[Dict[str, Any]]: - """Получение курсов валют через API.""" + +def load_user_settings() -> Dict[str, Any]: + """Загрузка пользовательских настроек""" try: - # Заглушка для демонстрации - в реальном проекте используйте реальное API - rates = [] - for currency in currencies: - if currency == "USD": - rates.append({"currency": currency, "rate": 73.21}) - elif currency == "EUR": - rates.append({"currency": currency, "rate": 87.08}) - else: - rates.append({"currency": currency, "rate": 1.0}) - - logger.info("Курсы валют успешно получены") - return rates - except Exception as e: - logger.error(f"Ошибка получения курсов валют: {e}") - return [] + with open(settings.user_settings_path, 'r', encoding='utf-8') as f: + settings_data = json.load(f) + # Валидация настроек + required_sections = ['user_currencies', 'user_stocks'] + for section in required_sections: + if section not in settings_data: + raise ValueError(f"Отсутствует обязательный раздел {section} в настройках") + + return settings_data + + except FileNotFoundError: + logger.error(f"Файл настроек {settings.user_settings_path} не найден") + raise + except json.JSONDecodeError as e: + logger.error(f"Ошибка парсинга JSON в настройках: {e}") + raise -def get_stock_prices(stocks: List[str]) -> List[Dict[str, Any]]: - """Получение цен акций через API.""" + +def calculate_cashback(amount: float, category: str, cashback_rules: Dict[str, float]) -> float: + """Расчет кешбэка по сложной логике""" try: - # Заглушка для демонстрации - prices = [] - stock_prices = { - "AAPL": 150.12, - "AMZN": 3173.18, - "GOOGL": 2742.39, - "MSFT": 296.71, - "TSLA": 1007.08 - } - - for stock in stocks: - price = stock_prices.get(stock, 0.0) - prices.append({"stock": stock, "price": price}) - - logger.info("Цены акций успешно получены") - return prices + # Базовая ставка + base_rate = cashback_rules.get('default', 0.01) + + # Повышенный кешбэк для категорий + category_rate = cashback_rules.get(category, base_rate) + + # Дополнительный бонус для больших покупок (ИСПРАВЛЕНО) + bonus_rate = 0.0 + if amount > 10000: + bonus_rate = 0.02 # +2% для покупок > 10,000 + elif amount > 5000: + bonus_rate = 0.01 # +1% для покупок > 5,000 + + total_rate = category_rate + bonus_rate + + # Ограничение максимального кешбэка 15% + total_rate = min(total_rate, 0.15) + + cashback = amount * total_rate + + # Округление до 2 знаков + return round(cashback, 2) + except Exception as e: - logger.error(f"Ошибка получения цен акций: {e}") - return [] + logger.warning(f"Ошибка расчета кешбэка: {e}") + return round(amount * 0.01, 2) # Fallback 1% diff --git a/src/views.py b/src/views.py index e856e82..716d004 100644 --- a/src/views.py +++ b/src/views.py @@ -1,80 +1,242 @@ -import json import logging +from typing import Dict, List, Any, Optional + import pandas as pd -from datetime import datetime -from typing import Dict, List, Any -from .utils import get_greeting, get_currency_rates, get_stock_prices, filter_transactions_by_date +import pytest + +from .api_client import SyncAPIClient +from .utils import (load_transactions, get_date_range, filter_transactions_by_date, + get_greeting, load_user_settings, calculate_cashback) logger = logging.getLogger(__name__) -def home_page(date_str: str, transactions_df: pd.DataFrame, user_settings: Dict[str, Any]) -> Dict[str, Any]: - """Главная страница с анализом транзакций.""" - try: - # Фильтрация транзакций - filtered_df = filter_transactions_by_date(transactions_df, date_str) +class DataProcessor: + """Процессор данных для веб-страниц""" + + @staticmethod + def process_main_page_data(df: pd.DataFrame, date_time: str, + settings: Dict[str, Any]) -> Dict[str, Any]: + """Обработка данных для главной страницы""" + try: + start_date, end_date = get_date_range(date_time, 'M') + filtered_df = filter_transactions_by_date(df, start_date, end_date) + + cashback_rules = settings.get('cashback_rules', {'default': 0.01}) + + return { + 'greeting': get_greeting(date_time), + 'cards': DataProcessor._get_cards_data(filtered_df, cashback_rules), + 'top_transactions': DataProcessor._get_top_transactions(filtered_df, 5), + 'currency_rates': SyncAPIClient.get_currency_rates(settings['user_currencies']), + 'stock_prices': SyncAPIClient.get_stock_prices(settings['user_stocks']) + } + except Exception as e: + logger.error(f"Ошибка обработки данных главной страницы: {e}") + raise + + @staticmethod + def process_events_page_data(df: pd.DataFrame, date: str, period: str, + settings: Dict[str, Any]) -> Dict[str, Any]: + """Обработка данных для страницы событий""" + try: + start_date, end_date = get_date_range(date, period) + filtered_df = filter_transactions_by_date(df, start_date, end_date) + + return { + 'expenses': DataProcessor._get_expenses_data(filtered_df), + 'income': DataProcessor._get_income_data(filtered_df), + 'currency_rates': SyncAPIClient.get_currency_rates(settings['user_currencies']), + 'stock_prices': SyncAPIClient.get_stock_prices(settings['user_stocks']) + } + except Exception as e: + logger.error(f"Ошибка обработки данных страницы событий: {e}") + raise + + @staticmethod + def _get_cards_data(df: pd.DataFrame, cashback_rules: Dict[str, float]) -> List[Dict[str, Any]]: + """Данные по картам""" + cards_data = [] + + # Получаем уникальные номера карт + card_numbers = [card for card in df['Номер карты'].unique() + if not pd.isna(card) and str(card).strip() != ''] + + for card in card_numbers: + card_df = df[df['Номер карты'] == card] + # Только успешные операции расходов + expenses_df = card_df[(card_df['Статус'] == 'OK') & + (card_df['Сумма операции'] > 0)] + + if expenses_df.empty: + continue + + total_spent = expenses_df['Сумма операции'].sum() + + # Расчет общего кешбэка по сложной логике + total_cashback = 0 + for _, transaction in expenses_df.iterrows(): + category = transaction.get('Категория', '') + amount = transaction['Сумма операции'] + total_cashback += calculate_cashback(amount, category, cashback_rules) + + cards_data.append({ + 'last_digits': str(card)[-4:], + 'total_spent': round(total_spent, 2), + 'cashback': round(total_cashback, 2) + }) + + # Сортировка по убыванию общей суммы расходов + return sorted(cards_data, key=lambda x: x['total_spent'], reverse=True) + + @staticmethod + def _get_top_transactions(df: pd.DataFrame, limit: int) -> List[Dict[str, Any]]: + """Топ транзакций по сумме платежа""" + # Берем абсолютное значение для сравнения (учитываем и доходы и расходы) + df['Абсолютная сумма'] = df['Сумма платежа'].abs() + top_df = df.nlargest(limit, 'Абсолютная сумма') + + transactions = [] + for _, row in top_df.iterrows(): + transactions.append({ + 'date': row['Дата операции'].strftime('%d.%m.%Y'), + 'amount': round(row['Сумма платежа'], 2), + 'category': row.get('Категория', 'Не указана'), + 'description': row.get('Описание', '')[:100] # Ограничение длины + }) + + return transactions - # Приветствие - greeting = get_greeting(date_str) + @staticmethod + def _get_expenses_data(df: pd.DataFrame) -> Dict[str, Any]: + """Данные по расходам""" + expenses_df = df[(df['Статус'] == 'OK') & (df['Сумма операции'] > 0)] - # Анализ по картам - cards_analysis = _analyze_cards(filtered_df) + if expenses_df.empty: + return { + 'total_amount': 0, + 'main': [], + 'transfers_and_cash': [] + } - # Топ транзакций - top_transactions = _get_top_transactions(filtered_df) + total_amount = expenses_df['Сумма операции'].sum() - # Курсы валют и акции - currency_rates = get_currency_rates(user_settings.get("user_currencies", [])) - stock_prices = get_stock_prices(user_settings.get("user_stocks", [])) + # Основные категории (топ-6 + остальное) + category_expenses = expenses_df.groupby('Категория')['Сумма операции'].sum() + top_categories = category_expenses.nlargest(6) + other_categories = category_expenses.iloc[6:].sum() if len(category_expenses) > 6 else 0 + + main_categories = [ + {'category': cat, 'amount': round(amount, 0)} + for cat, amount in top_categories.items() if not pd.isna(cat) + ] + + if other_categories > 0: + main_categories.append({'category': 'Остальное', 'amount': round(other_categories, 0)}) + + # Переводы и наличные + transfers_cash = expenses_df[expenses_df['Категория'].isin(['Наличные', 'Переводы'])] + transfers_data = transfers_cash.groupby('Категория')['Сумма операции'].sum() + + transfers_list = [ + {'category': cat, 'amount': round(amount, 0)} + for cat, amount in transfers_data.items() + ] return { - "greeting": greeting, - "cards": cards_analysis, - "top_transactions": top_transactions, - "currency_rates": currency_rates, - "stock_prices": stock_prices + 'total_amount': round(total_amount, 0), + 'main': main_categories, + 'transfers_and_cash': transfers_list } - except Exception as e: - logger.error(f"Ошибка генерации главной страницы: {e}") - return {"error": str(e)} + @staticmethod + def _get_income_data(df: pd.DataFrame) -> Dict[str, Any]: + """Данные по поступлениям""" + income_df = df[(df['Статус'] == 'OK') & (df['Сумма операции'] < 0)] -def _analyze_cards(df: pd.DataFrame) -> List[Dict[str, Any]]: - """Анализ транзакций по картам.""" - cards_analysis = [] + if income_df.empty: + return { + 'total_amount': 0, + 'main': [] + } - # Группировка по последним цифрам карты - if 'Номер карты' in df.columns: - for card in df['Номер карты'].dropna().unique(): - card_transactions = df[df['Номер карты'] == card] - total_spent = card_transactions['Сумма операции'].sum() - cashback = total_spent * 0.01 # 1% кешбэк + # Преобразуем отрицательные суммы в положительные + income_df = income_df.copy() + income_df['Сумма операции'] = income_df['Сумма операции'].abs() - cards_analysis.append({ - "last_digits": str(card)[-4:], - "total_spent": round(total_spent, 2), - "cashback": round(cashback, 2) - }) + total_amount = income_df['Сумма операции'].sum() - return cards_analysis + category_income = income_df.groupby('Категория')['Сумма операции'].sum() + main_income = [ + {'category': cat, 'amount': round(amount, 0)} + for cat, amount in category_income.items() if not pd.isna(cat) + ] -def _get_top_transactions(df: pd.DataFrame, top_n: int = 5) -> List[Dict[str, Any]]: - """Получение топ-N транзакций по сумме.""" + return { + 'total_amount': round(total_amount, 0), + 'main': sorted(main_income, key=lambda x: x['amount'], reverse=True) + } + + +def main_page(date_time: str, data_file: Optional[str] = None) -> Dict[str, Any]: + """Главная страница - генерация JSON данных""" try: - # Берем абсолютные значения для сортировки - df_sorted = df.nlargest(top_n, 'Сумма операции', keep='first') - - top_transactions = [] - for _, row in df_sorted.iterrows(): - top_transactions.append({ - "date": row['Дата операции'].strftime("%d.%m.%Y"), - "amount": round(row['Сумма операции'], 2), - "category": row.get('Категория', 'Неизвестно'), - "description": row.get('Описание', '') - }) + df = load_transactions(data_file) if data_file else load_transactions() + settings = load_user_settings() + + processor = DataProcessor() + return processor.process_main_page_data(df, date_time, settings) + + except Exception as e: + logger.error(f"Ошибка генерации главной страницы: {e}") + return {'error': str(e), 'greeting': 'Добрый день'} + + +def events_page(date: str, period: str = 'M', + data_file: Optional[str] = None) -> Dict[str, Any]: + """Страница событий - генерация JSON данных""" + try: + df = load_transactions(data_file) if data_file else load_transactions() + settings = load_user_settings() + + processor = DataProcessor() + return processor.process_events_page_data(df, date, period, settings) - return top_transactions except Exception as e: - logger.error(f"Ошибка получения топ транзакций: {e}") - return [] + logger.error(f"Ошибка генерации страницы событий: {e}") + return {'error': str(e)} + + +# tests/test_views.py - исправим тестовые данные +@pytest.fixture +def sample_transactions(): + return pd.DataFrame({ + 'Дата операции': pd.to_datetime(['2023-12-01', '2023-12-05', '2023-12-10']), + 'Номер карты': ['1234567812345814', '1234567812345814', '1234567812347512'], + 'Статус': ['OK', 'OK', 'OK'], # Все успешные операции + 'Сумма операции': [1000.0, 500.0, 300.0], # Все расходы (положительные) + 'Категория': ['Супермаркеты', 'Фастфуд', 'Транспорт'], + 'Описание': ['Магазин', 'Кафе', 'Такси'], + 'Сумма платежа': [1000.0, 500.0, 300.0] + }) + + +def test_data_processor(sample_transactions, sample_settings): + processor = DataProcessor() + + result = processor.process_main_page_data( + sample_transactions, '2023-12-20 15:30:00', sample_settings + ) + + # Теперь должно быть 2 карты с расходами + assert len(result['cards']) == 2 + assert len(result['top_transactions']) == 3 + + # Проверяем расчет кешбэка (ИСПРАВЛЕНО ожидание) + card_1 = result['cards'][0] + assert 'last_digits' in card_1 + assert 'total_spent' in card_1 + assert 'cashback' in card_1 + # 1% от 1500 (1000 + 500) = 15.0 + assert card_1['cashback'] == 15.0 diff --git a/test_app.py b/test_app.py new file mode 100644 index 0000000..e40b542 --- /dev/null +++ b/test_app.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +""" +Основной модуль приложения для анализа транзакций +""" + +import os +import logging +import argparse +import json +from datetime import datetime +from pathlib import Path + +# Создаем необходимые директории +Path('logs').mkdir(exist_ok=True) +Path('reports').mkdir(exist_ok=True) +Path('data').mkdir(exist_ok=True) + +# Настройка логирования +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler('logs/transaction_analyzer.log'), + logging.StreamHandler() + ] +) + +logger = logging.getLogger(__name__) + + +class TransactionAnalyzer: + """Основной класс приложения""" + + def __init__(self, data_file: str = None): + from src.config import settings + self.data_file = data_file or settings.data_file_path + self.transactions_df = None + self.settings = None + + def load_data(self): + """Загрузка данных""" + from src.utils import load_transactions, load_user_settings + logger.info("Загрузка данных...") + self.transactions_df = load_transactions(self.data_file) + self.settings = load_user_settings() + logger.info("Данные успешно загружены") + + def generate_main_page(self, date_time: str) -> dict: + """Генерация данных для главной страницы""" + from src.views import main_page + return main_page(date_time, self.data_file) + + def generate_events_page(self, date: str, period: str = 'M') -> dict: + """Генерация данных для страницы событий""" + from src.views import events_page + return events_page(date, period, self.data_file) + + def analyze_cashback_categories(self, year: int, month: int) -> dict: + """Анализ выгодных категорий кешбэка""" + from src.services import profitable_cashback_categories + cashback_rules = self.settings.get('cashback_rules', {'default': 0.01}) + return profitable_cashback_categories( + self.transactions_df, year, month, cashback_rules + ) + + def calculate_investment(self, month: str, limit: int) -> float: + """Расчет инвесткопилки""" + from src.services import investment_bank + transactions_list = self.transactions_df.to_dict('records') + return investment_bank(month, transactions_list, limit) + + def search_transactions(self, search_string: str) -> list: + """Поиск транзакций""" + from src.services import simple_search + transactions_list = self.transactions_df.to_dict('records') + return simple_search(transactions_list, search_string) + + def generate_reports(self): + """Генерация всех отчетов""" + from src.reports import ReportGenerator # Добавлен импорт + + reports = {} + + # Отчет по категориям + reports['spending_by_category'] = ReportGenerator.spending_by_category( + self.transactions_df, 'Супермаркеты' + ) + + # Отчет по дням недели + reports['spending_by_weekday'] = ReportGenerator.spending_by_weekday( + self.transactions_df + ) + + # Отчет по рабочим/выходным дням + reports['spending_by_workday'] = ReportGenerator.spending_by_workday( + self.transactions_df + ) + + # Сводный отчет + reports['monthly_summary'] = ReportGenerator.monthly_summary( + self.transactions_df + ) + + return reports + + +def main(): + """Основная функция приложения""" + parser = argparse.ArgumentParser(description='Анализатор банковских транзакций') + parser.add_argument('--data-file', help='Путь к файлу с транзакциями') + parser.add_argument('--command', choices=['web', 'report', 'analyze', 'test'], + default='web', help='Режим работы') + + args = parser.parse_args() + + try: + # Инициализация анализатора + analyzer = TransactionAnalyzer(args.data_file) + analyzer.load_data() + + if args.command == 'web': + # Пример генерации данных для веб-страниц + current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + + main_data = analyzer.generate_main_page(current_time) + events_data = analyzer.generate_events_page(current_time.split()[0]) + + print("=== ДАННЫЕ ДЛЯ ГЛАВНОЙ СТРАНИЦЫ ===") + print(json.dumps(main_data, ensure_ascii=False, indent=2)) + + print("\n=== ДАННЫЕ ДЛЯ СТРАНИЦЫ СОБЫТИЙ ===") + print(json.dumps(events_data, ensure_ascii=False, indent=2)) + + elif args.command == 'report': + # Генерация отчетов + reports = analyzer.generate_reports() + print("=== ОТЧЕТЫ СГЕНЕРИРОВАНЫ ===") + + for report_name, report_data in reports.items(): + print(f"\n--- {report_name} ---") + if isinstance(report_data, dict): + print(json.dumps(report_data, ensure_ascii=False, indent=2)) + elif hasattr(report_data, 'head'): # DataFrame + print(report_data.head()) + else: + print(f"Тип данных: {type(report_data)}") + print(report_data) + + print(f"\n📊 Отчеты сохранены в папке 'reports/'") + + elif args.command == 'analyze': + # Анализ данных + current_year = datetime.now().year + current_month = datetime.now().month + + cashback_analysis = analyzer.analyze_cashback_categories(current_year, current_month) + investment = analyzer.calculate_investment( + f"{current_year}-{current_month:02d}", 50 + ) + + print("=== АНАЛИЗ ДАННЫХ ===") + print(f"Выгодные категории кешбэка: {cashback_analysis}") + print(f"Инвесткопилка за месяц: {investment} руб.") + + elif args.command == 'test': + # Простой тест функциональности + print("=== ТЕСТ ФУНКЦИОНАЛЬНОСТИ ===") + print(f"Загружено транзакций: {len(analyzer.transactions_df)}") + print(f"Настройки: {list(analyzer.settings.keys())}") + + # Тест поиска + search_results = analyzer.search_transactions("магазин") + print(f"Результатов поиска 'магазин': {len(search_results)}") + + except Exception as e: + logger.error(f"Ошибка приложения: {e}") + raise + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..6f8e834 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,38 @@ +import pytest +import pandas as pd +from datetime import datetime, timedelta +import os + +@pytest.fixture +def sample_transactions(): + """Фикстура с примером транзакций""" + dates = pd.date_range(start='2023-01-01', end='2023-12-31', freq='D') + return pd.DataFrame({ + 'Дата операции': dates[:100], + 'Номер карты': ['1234567812345814'] * 50 + ['1234567812347512'] * 50, + 'Статус': ['OK'] * 100, + 'Сумма операции': [1000.0, 500.0] * 50, + 'Категория': ['Супермаркеты', 'Фастфуд'] * 50, + 'Описание': [f'Транзакция {i}' for i in range(100)], + 'Сумма платежа': [1000.0, 500.0] * 50 + }) + +@pytest.fixture +def sample_settings(): + """Фикстура с настройками""" + return { + "user_currencies": ["USD", "EUR"], + "user_stocks": ["AAPL", "AMZN"], + "cashback_rules": { + "Супермаркеты": 0.05, + "Фастфуд": 0.03, + "default": 0.01 + } + } + +@pytest.fixture(autouse=True) +def setup_teardown(): + """Фикстура для настройки перед каждым тестом""" + # Настройка перед тестом + yield + # Очистка после теста \ No newline at end of file diff --git a/tests/test_api_client.py b/tests/test_api_client.py new file mode 100644 index 0000000..18e9f3a --- /dev/null +++ b/tests/test_api_client.py @@ -0,0 +1,47 @@ + +import pytest +from unittest.mock import AsyncMock, patch +import asyncio + + +class TestAPIClient: + @pytest.mark.asyncio + async def test_get_currency_rates(self): + async with AsyncMock() as mock_session: + with patch('aiohttp.ClientSession', return_value=mock_session): + from src.api_client import APIClient + client = APIClient() + client.session = mock_session + + # Мок успешного ответа + mock_response = AsyncMock() + mock_response.status = 200 + mock_response.json.return_value = { + "rates": {"USD": 0.0107, "EUR": 0.0099}, + "base": "RUB" + } + mock_session.get.return_value.__aenter__.return_value = mock_response + + rates = await client.get_currency_rates(["USD", "EUR"]) + # Ожидаем только запрошенные валюты, RUB не должен включаться если не запрошен + assert len(rates) == 2 + assert any(rate['currency'] == 'USD' for rate in rates) + assert any(rate['currency'] == 'EUR' for rate in rates) + + @pytest.mark.asyncio + async def test_get_currency_rates_fallback(self): + async with AsyncMock() as mock_session: + with patch('aiohttp.ClientSession', return_value=mock_session): + from src.api_client import APIClient + client = APIClient() + client.session = mock_session + + # Мок неудачного ответа + mock_response = AsyncMock() + mock_response.status = 500 + mock_session.get.return_value.__aenter__.return_value = mock_response + + rates = await client.get_currency_rates(["USD", "EUR"]) + # Fallback должен вернуть только запрошенные валюты + assert len(rates) == 2 + assert all(rate['rate'] > 0 for rate in rates) diff --git a/tests/test_services.py b/tests/test_services.py index 1454c13..46b76e6 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -29,6 +29,7 @@ def test_investment_bank(sample_transactions_list): def test_simple_search(sample_transactions_list): """Тест простого поиска.""" - results = simple_search("кафе", sample_transactions_list) + # Исправляем порядок аргументов: сначала транзакции, потом строка поиска + results = simple_search(sample_transactions_list, "кафе") assert len(results) == 1 assert results[0]['Категория'] == 'Кафе' diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..f2ac33d --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,87 @@ +# tests/test_utils.py +import pytest +from src.utils import ( + get_greeting, get_date_range, calculate_cashback, + DataValidator, load_user_settings +) +from datetime import datetime +import pandas as pd +import json +import tempfile +import os + + +class TestUtils: + def test_get_greeting(self): + assert get_greeting('2023-12-20 08:30:00') == 'Доброе утро' + assert get_greeting('2023-12-20 14:30:00') == 'Добрый день' + assert get_greeting('2023-12-20 20:30:00') == 'Добрый вечер' + assert get_greeting('2023-12-20 02:30:00') == 'Доброй ночи' + + def test_get_date_range(self): + start, end = get_date_range('2023-12-20 15:30:00', 'M') + assert start == datetime(2023, 12, 1, 0, 0, 0) # Исправлено: добавили время + assert end == datetime(2023, 12, 20, 15, 30, 0) + + start, end = get_date_range('2023-12-20', 'W') + assert start.weekday() == 0 # Понедельник + assert end.weekday() == 6 # Воскресенье + + def test_calculate_cashback(self): + cashback_rules = { + 'Супермаркеты': 0.05, + 'default': 0.01 + } + + # Базовая логика + assert calculate_cashback(1000, 'Супермаркеты', cashback_rules) == 50.0 + assert calculate_cashback(1000, 'Другое', cashback_rules) == 10.0 + + # Бонус за большие покупки - ИСПРАВЛЕНО ожидание + # 6000 * (5% + 1%) = 6000 * 0.06 = 360.0 + assert calculate_cashback(6000, 'Супермаркеты', cashback_rules) == 360.0 + # 11000 * (5% + 2%) = 11000 * 0.07 = 770.0 + assert calculate_cashback(11000, 'Супермаркеты', cashback_rules) == 770.0 + + # Ограничение максимального кешбэка + high_cashback_rules = {'default': 0.20} + assert calculate_cashback(1000, 'Тест', high_cashback_rules) == 150.0 # Максимум 15% + + def test_data_validator(self): + validator = DataValidator() + + # Тестовые данные с корректными данными + test_data = pd.DataFrame({ + 'Дата операции': ['15.01.2023', '20.02.2023', '10.03.2023'], # Только корректные даты + 'Сумма операции': ['1000.50', '2000', '1500.75'], + 'Статус': ['OK', 'OK', 'OK'] + }) + + clean_data, errors = validator.validate_transaction_data(test_data) + + assert len(clean_data) == 3 # Все строки должны быть корректными + assert isinstance(errors, list) + + def test_load_user_settings(self): + # Создаем временный файл настроек + with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f: + json.dump({ + "user_currencies": ["USD", "EUR"], + "user_stocks": ["AAPL", "AMZN"] + }, f) + temp_path = f.name + + try: + # Мокаем путь к настройкам + import src.utils + original_path = src.utils.settings.user_settings_path + src.utils.settings.user_settings_path = temp_path + + settings = load_user_settings() + assert 'user_currencies' in settings + assert 'user_stocks' in settings + + finally: + src.utils.settings.user_settings_path = original_path + os.unlink(temp_path) + diff --git a/tests/test_views.py b/tests/test_views.py index 9549685..3193ef3 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -1,47 +1,86 @@ +# tests/test_views.py import pytest +from src.views import main_page, events_page, DataProcessor +from unittest.mock import patch, MagicMock import pandas as pd from datetime import datetime -from src.views import home_page -from src.utils import get_greeting - - -@pytest.fixture -def sample_transactions(): - """Фикстура с тестовыми транзакциями.""" - return pd.DataFrame({ - 'Дата операции': ['2023-12-01', '2023-12-15', '2023-11-20'], - 'Номер карты': ['1234567812345678', '1234567812345678', '8765432187654321'], - 'Сумма операции': [1000.0, 500.0, 2000.0], - 'Категория': ['Супермаркеты', 'Кафе', 'Транспорт'], - 'Описание': ['Покупка в магазине', 'Обед в кафе', 'Такси'] - }) - - -@pytest.fixture -def user_settings(): - """Фикстура с настройками пользователя.""" - return { - "user_currencies": ["USD", "EUR"], - "user_stocks": ["AAPL", "GOOGL"] - } - - -def test_home_page(sample_transactions, user_settings): - """Тест главной страницы.""" - result = home_page("2023-12-20 15:30:00", sample_transactions, user_settings) - - assert "greeting" in result - assert "cards" in result - assert "top_transactions" in result - assert len(result["cards"]) > 0 - - -@pytest.mark.parametrize("time_str,expected_greeting", [ - ("2023-12-20 08:30:00", "Доброе утро"), - ("2023-12-20 14:30:00", "Добрый день"), - ("2023-12-20 20:30:00", "Добрый вечер"), - ("2023-12-20 02:30:00", "Доброй ночи"), -]) -def test_get_greeting(time_str, expected_greeting): - """Тест получения приветствия.""" - assert get_greeting(time_str) == expected_greeting + + +class TestViews: + @pytest.fixture + def sample_transactions(self): + """Фикстура с корректными тестовыми данными""" + return pd.DataFrame({ + 'Дата операции': pd.to_datetime(['2023-12-01', '2023-12-05', '2023-12-10', '2023-12-15']), + 'Номер карты': ['1234567812345814', '1234567812345814', '1234567812347512', '1234567812347512'], + 'Статус': ['OK', 'OK', 'OK', 'OK'], + 'Сумма операции': [1000.0, 500.0, 300.0, 200.0], # Все расходы (положительные) + 'Категория': ['Супермаркеты', 'Фастфуд', 'Транспорт', 'Развлечения'], + 'Описание': ['Магазин', 'Кафе', 'Такси', 'Кино'], + 'Сумма платежа': [1000.0, 500.0, 300.0, 200.0] + }) + + @pytest.fixture + def sample_settings(self): + return { + "user_currencies": ["USD", "EUR"], + "user_stocks": ["AAPL", "AMZN"], + "cashback_rules": {"default": 0.01} + } + + @patch('src.views.SyncAPIClient.get_currency_rates') + @patch('src.views.SyncAPIClient.get_stock_prices') + def test_main_page(self, mock_stocks, mock_currency, sample_transactions, sample_settings): + mock_currency.return_value = [{"currency": "USD", "rate": 93.45}] + mock_stocks.return_value = [{"stock": "AAPL", "price": 178.72}] + + with patch('src.views.load_transactions', return_value=sample_transactions): + with patch('src.views.load_user_settings', return_value=sample_settings): + result = main_page('2023-12-20 15:30:00') + + assert 'greeting' in result + assert 'cards' in result + assert 'top_transactions' in result + assert 'currency_rates' in result + assert 'stock_prices' in result + assert result['greeting'] == 'Добрый день' + + def test_data_processor(self, sample_transactions, sample_settings): + processor = DataProcessor() + + result = processor.process_main_page_data( + sample_transactions, '2023-12-20 15:30:00', sample_settings + ) + + # Теперь должно быть 2 карты с расходами (обе карты имеют расходы) + assert len(result['cards']) == 2 + assert len(result['top_transactions']) == 4 + + # Проверяем расчет кешбэка + # Карта 5814: 1000 + 500 = 1500 * 1% = 15.0 + # Карта 7512: 300 + 200 = 500 * 1% = 5.0 + + # Находим карты по last_digits + card_5814 = next(card for card in result['cards'] if card['last_digits'] == '5814') + card_7512 = next(card for card in result['cards'] if card['last_digits'] == '7512') + + assert card_5814['total_spent'] == 1500.0 + assert card_5814['cashback'] == 15.0 + assert card_7512['total_spent'] == 500.0 + assert card_7512['cashback'] == 5.0 + + @patch('src.views.SyncAPIClient.get_currency_rates') + @patch('src.views.SyncAPIClient.get_stock_prices') + def test_events_page(self, mock_stocks, mock_currency, sample_transactions, sample_settings): + mock_currency.return_value = [{"currency": "USD", "rate": 93.45}] + mock_stocks.return_value = [{"stock": "AAPL", "price": 178.72}] + + with patch('src.views.load_transactions', return_value=sample_transactions): + with patch('src.views.load_user_settings', return_value=sample_settings): + result = events_page('2023-12-20', 'M') + + assert 'expenses' in result + assert 'income' in result + # Все операции расходы, поэтому income = 0 + assert result['expenses']['total_amount'] == 2000 # 1000+500+300+200 + assert result['income']['total_amount'] == 0 # Нет отрицательных сумм From 50568fcac119d10afd73f06ec73ed5a8bb200766 Mon Sep 17 00:00:00 2001 From: ilya_kim Date: Sun, 28 Sep 2025 00:55:03 +0300 Subject: [PATCH 02/12] appdate README.md --- README.md | 176 +++++- logs/transaction_analyzer.log | 1057 +++++++++++++++++++++++++++++++++ 2 files changed, 1229 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7466ee3..80e16ba 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,177 @@ - 🔍 Поиск и фильтрация транзакций - 💹 Интеграция с API курсов валют и акций -## Установка - -1. Клонируйте репозиторий: +# Установка +## 1. Клонирование и настройка +### Клонируйте репозиторий: ```bash git clone -cd transaction-analyzer \ No newline at end of file +cd transaction-analyzer +``` + +### Установите зависимости: +```bash +poetry install +``` + +### Активируйте виртуальное окружение +```bash +poetry shell +``` + +## 2. Подготовка данных + +### Вариант A: Используйте тестовые данные + +Сгенерируйте тестовые данные +```bash +python data/sample_data.py +``` +### Вариант B: Загрузите свои данные +1. Поместите ваш файл с транзакциями в папку data/ + +2. Переименуйте его в operations.xlsx + +## 3. Запуск приложения + +Запуск в режиме веб-страниц (генерирует JSON для фронтенда) +```bash +python -m src.main --command web +``` + +Запуск в режиме отчетов (генерирует отчеты в папку reports/) +```bash +python -m src.main --command report +``` + +Запуск в режиме анализа (кешбэк и инвесткопилка) +```bash +python -m src.main --command analyze +``` + +Простой тест функциональности +```bash +python -m src.main --command test +``` + +## Настройки приложения + +### Файл user_settings.json содержит пользовательские настройки: + +Файл user_settings.json содержит пользовательские настройки: + +```json +{ + "user_currencies": ["USD", "EUR", "GBP"], + "user_stocks": ["AAPL", "AMZN", "GOOGL", "MSFT", "TSLA"], + "cashback_rules": { + "Супермаркеты": 0.05, + "Фастфуд": 0.03, + "Транспорт": 0.02, + "default": 0.01 + } +} +``` + +### Параметры настроек: + +* user_currencies - список валют для отслеживания курсов + +* user_stocks - список акций для отслеживания цен + +* cashback_rules - правила расчета кешбэка по категориям + +# Веб-страницы API +## Главная страница +```python +# Генерирует JSON для главной страницы +from src.views import main_page + +data = main_page("2023-12-20 15:30:00") +``` + +### Формат ответа: +```json +{ + "greeting": "Добрый день", + "cards": [ + { + "last_digits": "5814", + "total_spent": 1262.00, + "cashback": 12.62 + } + ], + "top_transactions": [...], + "currency_rates": [...], + "stock_prices": [...] +} +``` +## Страница событий +```python +# Генерирует JSON для страницы событий +from src.views import events_page + +data = events_page("2023-12-20", "M") # M - месяц, W - неделя, Y - год, ALL - все данные +``` + +# Сервисы поиска + +## Простой поиск +```python +from src.services import simple_search + +# Поиск по описанию и категории +results = simple_search(transactions, "магазин") +``` + +## Поиск по телефонным номерам +```python +from src.services import search_by_phone + +# Поиск транзакций с номерами телефонов +results = search_by_phone(transactions) +``` +## Поиск переводов физлицам +```python +from src.services import search_by_person_transfers + +# Поиск переводов физическим лицам +results = search_by_person_transfers(transactions) +``` + +# Генерация отчетов + +## Траты по категории +```python +from src.reports import spending_by_category + +# Траты по категории за последние 3 месяца +report = spending_by_category(transactions, "Супермаркеты", "2023-12-20") +``` +## Траты по дням недели +```python +from src.reports import spending_by_weekday + +# Средние траты по дням недели +report = spending_by_weekday(transactions) +``` +## Все отчеты через главный модуль +```bash +python -m src.main --command report +``` +#### Отчеты сохраняются в папку reports/ в формате JSON. + +# Тестирование + +```bash +# Запуск всех тестов +pytest tests/ -v +``` +```bash +# Запуск конкретного модуля тестов +pytest tests/test_views.py -v +``` +```bash +# Запуск с покрытием кода +pytest tests/ --cov=src --cov-report=html +``` diff --git a/logs/transaction_analyzer.log b/logs/transaction_analyzer.log index c67d25c..d75d15f 100644 --- a/logs/transaction_analyzer.log +++ b/logs/transaction_analyzer.log @@ -1071,3 +1071,1060 @@ 2025-09-27 23:40:19,106 - src.services - WARNING - 2025-09-27 23:40:19,106 - src.services - WARNING - 2025-09-27 23:40:19,106 - src.services - INFO - investment_bank +2025-09-27 23:45:29,262 - __main__ - INFO - ... +2025-09-27 23:45:29,262 - src.utils - INFO - data/operations.xlsx +2025-09-27 23:45:29,513 - src.utils - INFO - 1000 +2025-09-27 23:45:29,514 - __main__ - INFO - +2025-09-27 23:45:29,741 - src.utils - INFO - data/operations.xlsx +2025-09-27 23:45:29,871 - src.utils - INFO - 1000 +2025-09-27 23:45:29,873 - src.utils - INFO - 0 2025-09-01 - 2025-09-27 +2025-09-27 23:45:31,924 - src.api_client - INFO - Using fallback stock prices +2025-09-27 23:45:31,926 - src.utils - INFO - data/operations.xlsx +2025-09-27 23:45:32,054 - src.utils - INFO - 1000 +2025-09-27 23:45:32,055 - src.utils - INFO - 0 2025-09-01 - 2025-09-27 +2025-09-27 23:45:32,056 - src.api_client - INFO - Using cached currency rates +2025-09-27 23:45:33,406 - src.api_client - INFO - Using fallback stock prices +2025-09-28 00:43:06,698 - __main__ - INFO - ... +2025-09-28 00:43:06,699 - src.utils - INFO - data/operations.xlsx +2025-09-28 00:43:06,949 - src.utils - INFO - 1000 +2025-09-28 00:43:06,949 - __main__ - INFO - +2025-09-28 00:43:06,952 - src.reports - INFO - reports/spending_by_category_20250928_004306.json +2025-09-28 00:43:06,954 - src.reports - INFO - reports/spending_by_weekday_20250928_004306.json +2025-09-28 00:43:06,956 - src.reports - INFO - reports/spending_by_workday_20250928_004306.json +2025-09-28 00:43:06,957 - src.reports - INFO - reports/monthly_summary_20250928_004306.json +2025-09-28 00:48:04,163 - __main__ - INFO - ... +2025-09-28 00:48:04,163 - src.utils - INFO - data/operations.xlsx +2025-09-28 00:48:04,422 - src.utils - INFO - 1000 +2025-09-28 00:48:04,423 - __main__ - INFO - +2025-09-28 00:48:04,427 - src.reports - INFO - reports/spending_by_category_20250928_004804.json +2025-09-28 00:48:04,429 - src.reports - INFO - reports/spending_by_weekday_20250928_004804.json +2025-09-28 00:48:04,431 - src.reports - INFO - reports/spending_by_workday_20250928_004804.json +2025-09-28 00:48:04,432 - src.reports - INFO - reports/monthly_summary_20250928_004804.json +2025-09-28 00:49:29,974 - __main__ - INFO - ... +2025-09-28 00:49:29,975 - src.utils - INFO - data/operations.xlsx +2025-09-28 00:49:30,227 - src.utils - INFO - 1000 +2025-09-28 00:49:30,228 - __main__ - INFO - +2025-09-28 00:49:30,470 - src.utils - INFO - data/operations.xlsx +2025-09-28 00:49:30,596 - src.utils - INFO - 1000 +2025-09-28 00:49:30,598 - src.utils - INFO - 0 2025-09-01 - 2025-09-28 +2025-09-28 00:49:32,451 - src.api_client - INFO - Using fallback stock prices +2025-09-28 00:49:32,452 - src.utils - INFO - data/operations.xlsx +2025-09-28 00:49:32,585 - src.utils - INFO - 1000 +2025-09-28 00:49:32,586 - src.utils - INFO - 0 2025-09-01 - 2025-09-28 +2025-09-28 00:49:32,587 - src.api_client - INFO - Using cached currency rates +2025-09-28 00:49:33,863 - src.api_client - INFO - Using fallback stock prices +2025-09-28 00:51:13,698 - __main__ - INFO - ... +2025-09-28 00:51:13,699 - src.utils - INFO - data/operations.xlsx +2025-09-28 00:51:13,953 - src.utils - INFO - 1000 +2025-09-28 00:51:13,953 - __main__ - INFO - +2025-09-28 00:51:13,960 - src.services - INFO - simple_search +2025-09-28 00:51:13,961 - src.services - INFO - simple_search +2025-09-28 00:51:31,514 - __main__ - INFO - ... +2025-09-28 00:51:31,514 - src.utils - INFO - data/operations.xlsx +2025-09-28 00:51:31,759 - src.utils - INFO - 1000 +2025-09-28 00:51:31,759 - __main__ - INFO - +2025-09-28 00:51:31,760 - src.services - INFO - profitable_cashback_categories +2025-09-28 00:51:31,762 - src.services - WARNING - 9/2025 +2025-09-28 00:51:31,762 - src.services - INFO - profitable_cashback_categories +2025-09-28 00:51:31,767 - src.services - INFO - investment_bank +2025-09-28 00:51:31,768 - src.services - WARNING - +2025-09-28 00:51:31,768 - src.services - WARNING - +2025-09-28 00:51:31,768 - src.services - WARNING - +2025-09-28 00:51:31,768 - src.services - WARNING - +2025-09-28 00:51:31,768 - src.services - WARNING - +2025-09-28 00:51:31,769 - src.services - WARNING - +2025-09-28 00:51:31,769 - src.services - WARNING - +2025-09-28 00:51:31,769 - src.services - WARNING - +2025-09-28 00:51:31,769 - src.services - WARNING - +2025-09-28 00:51:31,769 - src.services - WARNING - +2025-09-28 00:51:31,769 - src.services - WARNING - +2025-09-28 00:51:31,769 - src.services - WARNING - +2025-09-28 00:51:31,769 - src.services - WARNING - +2025-09-28 00:51:31,769 - src.services - WARNING - +2025-09-28 00:51:31,769 - src.services - WARNING - +2025-09-28 00:51:31,770 - src.services - WARNING - +2025-09-28 00:51:31,770 - src.services - WARNING - +2025-09-28 00:51:31,770 - src.services - WARNING - +2025-09-28 00:51:31,770 - src.services - WARNING - +2025-09-28 00:51:31,770 - src.services - WARNING - +2025-09-28 00:51:31,770 - src.services - WARNING - +2025-09-28 00:51:31,770 - src.services - WARNING - +2025-09-28 00:51:31,770 - src.services - WARNING - +2025-09-28 00:51:31,770 - src.services - WARNING - +2025-09-28 00:51:31,770 - src.services - WARNING - +2025-09-28 00:51:31,770 - src.services - WARNING - +2025-09-28 00:51:31,770 - src.services - WARNING - +2025-09-28 00:51:31,770 - src.services - WARNING - +2025-09-28 00:51:31,770 - src.services - WARNING - +2025-09-28 00:51:31,770 - src.services - WARNING - +2025-09-28 00:51:31,771 - src.services - WARNING - +2025-09-28 00:51:31,771 - src.services - WARNING - +2025-09-28 00:51:31,771 - src.services - WARNING - +2025-09-28 00:51:31,771 - src.services - WARNING - +2025-09-28 00:51:31,771 - src.services - WARNING - +2025-09-28 00:51:31,771 - src.services - WARNING - +2025-09-28 00:51:31,771 - src.services - WARNING - +2025-09-28 00:51:31,771 - src.services - WARNING - +2025-09-28 00:51:31,771 - src.services - WARNING - +2025-09-28 00:51:31,771 - src.services - WARNING - +2025-09-28 00:51:31,771 - src.services - WARNING - +2025-09-28 00:51:31,771 - src.services - WARNING - +2025-09-28 00:51:31,771 - src.services - WARNING - +2025-09-28 00:51:31,771 - src.services - WARNING - +2025-09-28 00:51:31,771 - src.services - WARNING - +2025-09-28 00:51:31,772 - src.services - WARNING - +2025-09-28 00:51:31,772 - src.services - WARNING - +2025-09-28 00:51:31,772 - src.services - WARNING - +2025-09-28 00:51:31,772 - src.services - WARNING - +2025-09-28 00:51:31,772 - src.services - WARNING - +2025-09-28 00:51:31,772 - src.services - WARNING - +2025-09-28 00:51:31,772 - src.services - WARNING - +2025-09-28 00:51:31,772 - src.services - WARNING - +2025-09-28 00:51:31,772 - src.services - WARNING - +2025-09-28 00:51:31,772 - src.services - WARNING - +2025-09-28 00:51:31,772 - src.services - WARNING - +2025-09-28 00:51:31,772 - src.services - WARNING - +2025-09-28 00:51:31,772 - src.services - WARNING - +2025-09-28 00:51:31,772 - src.services - WARNING - +2025-09-28 00:51:31,772 - src.services - WARNING - +2025-09-28 00:51:31,772 - src.services - WARNING - +2025-09-28 00:51:31,773 - src.services - WARNING - +2025-09-28 00:51:31,773 - src.services - WARNING - +2025-09-28 00:51:31,773 - src.services - WARNING - +2025-09-28 00:51:31,773 - src.services - WARNING - +2025-09-28 00:51:31,773 - src.services - WARNING - +2025-09-28 00:51:31,773 - src.services - WARNING - +2025-09-28 00:51:31,773 - src.services - WARNING - +2025-09-28 00:51:31,773 - src.services - WARNING - +2025-09-28 00:51:31,773 - src.services - WARNING - +2025-09-28 00:51:31,773 - src.services - WARNING - +2025-09-28 00:51:31,773 - src.services - WARNING - +2025-09-28 00:51:31,773 - src.services - WARNING - +2025-09-28 00:51:31,773 - src.services - WARNING - +2025-09-28 00:51:31,773 - src.services - WARNING - +2025-09-28 00:51:31,773 - src.services - WARNING - +2025-09-28 00:51:31,773 - src.services - WARNING - +2025-09-28 00:51:31,774 - src.services - WARNING - +2025-09-28 00:51:31,774 - src.services - WARNING - +2025-09-28 00:51:31,774 - src.services - WARNING - +2025-09-28 00:51:31,774 - src.services - WARNING - +2025-09-28 00:51:31,774 - src.services - WARNING - +2025-09-28 00:51:31,774 - src.services - WARNING - +2025-09-28 00:51:31,774 - src.services - WARNING - +2025-09-28 00:51:31,774 - src.services - WARNING - +2025-09-28 00:51:31,774 - src.services - WARNING - +2025-09-28 00:51:31,775 - src.services - WARNING - +2025-09-28 00:51:31,775 - src.services - WARNING - +2025-09-28 00:51:31,775 - src.services - WARNING - +2025-09-28 00:51:31,775 - src.services - WARNING - +2025-09-28 00:51:31,775 - src.services - WARNING - +2025-09-28 00:51:31,775 - src.services - WARNING - +2025-09-28 00:51:31,775 - src.services - WARNING - +2025-09-28 00:51:31,775 - src.services - WARNING - +2025-09-28 00:51:31,775 - src.services - WARNING - +2025-09-28 00:51:31,775 - src.services - WARNING - +2025-09-28 00:51:31,775 - src.services - WARNING - +2025-09-28 00:51:31,775 - src.services - WARNING - +2025-09-28 00:51:31,776 - src.services - WARNING - +2025-09-28 00:51:31,776 - src.services - WARNING - +2025-09-28 00:51:31,776 - src.services - WARNING - +2025-09-28 00:51:31,776 - src.services - WARNING - +2025-09-28 00:51:31,776 - src.services - WARNING - +2025-09-28 00:51:31,776 - src.services - WARNING - +2025-09-28 00:51:31,776 - src.services - WARNING - +2025-09-28 00:51:31,776 - src.services - WARNING - +2025-09-28 00:51:31,776 - src.services - WARNING - +2025-09-28 00:51:31,776 - src.services - WARNING - +2025-09-28 00:51:31,776 - src.services - WARNING - +2025-09-28 00:51:31,776 - src.services - WARNING - +2025-09-28 00:51:31,776 - src.services - WARNING - +2025-09-28 00:51:31,776 - src.services - WARNING - +2025-09-28 00:51:31,776 - src.services - WARNING - +2025-09-28 00:51:31,777 - src.services - WARNING - +2025-09-28 00:51:31,777 - src.services - WARNING - +2025-09-28 00:51:31,777 - src.services - WARNING - +2025-09-28 00:51:31,777 - src.services - WARNING - +2025-09-28 00:51:31,777 - src.services - WARNING - +2025-09-28 00:51:31,777 - src.services - WARNING - +2025-09-28 00:51:31,777 - src.services - WARNING - +2025-09-28 00:51:31,777 - src.services - WARNING - +2025-09-28 00:51:31,777 - src.services - WARNING - +2025-09-28 00:51:31,777 - src.services - WARNING - +2025-09-28 00:51:31,777 - src.services - WARNING - +2025-09-28 00:51:31,777 - src.services - WARNING - +2025-09-28 00:51:31,777 - src.services - WARNING - +2025-09-28 00:51:31,777 - src.services - WARNING - +2025-09-28 00:51:31,777 - src.services - WARNING - +2025-09-28 00:51:31,778 - src.services - WARNING - +2025-09-28 00:51:31,778 - src.services - WARNING - +2025-09-28 00:51:31,778 - src.services - WARNING - +2025-09-28 00:51:31,778 - src.services - WARNING - +2025-09-28 00:51:31,778 - src.services - WARNING - +2025-09-28 00:51:31,778 - src.services - WARNING - +2025-09-28 00:51:31,778 - src.services - WARNING - +2025-09-28 00:51:31,778 - src.services - WARNING - +2025-09-28 00:51:31,778 - src.services - WARNING - +2025-09-28 00:51:31,778 - src.services - WARNING - +2025-09-28 00:51:31,778 - src.services - WARNING - +2025-09-28 00:51:31,778 - src.services - WARNING - +2025-09-28 00:51:31,778 - src.services - WARNING - +2025-09-28 00:51:31,778 - src.services - WARNING - +2025-09-28 00:51:31,778 - src.services - WARNING - +2025-09-28 00:51:31,778 - src.services - WARNING - +2025-09-28 00:51:31,779 - src.services - WARNING - +2025-09-28 00:51:31,779 - src.services - WARNING - +2025-09-28 00:51:31,779 - src.services - WARNING - +2025-09-28 00:51:31,779 - src.services - WARNING - +2025-09-28 00:51:31,779 - src.services - WARNING - +2025-09-28 00:51:31,779 - src.services - WARNING - +2025-09-28 00:51:31,779 - src.services - WARNING - +2025-09-28 00:51:31,779 - src.services - WARNING - +2025-09-28 00:51:31,779 - src.services - WARNING - +2025-09-28 00:51:31,779 - src.services - WARNING - +2025-09-28 00:51:31,779 - src.services - WARNING - +2025-09-28 00:51:31,779 - src.services - WARNING - +2025-09-28 00:51:31,779 - src.services - WARNING - +2025-09-28 00:51:31,779 - src.services - WARNING - +2025-09-28 00:51:31,779 - src.services - WARNING - +2025-09-28 00:51:31,780 - src.services - WARNING - +2025-09-28 00:51:31,780 - src.services - WARNING - +2025-09-28 00:51:31,780 - src.services - WARNING - +2025-09-28 00:51:31,780 - src.services - WARNING - +2025-09-28 00:51:31,780 - src.services - WARNING - +2025-09-28 00:51:31,780 - src.services - WARNING - +2025-09-28 00:51:31,780 - src.services - WARNING - +2025-09-28 00:51:31,780 - src.services - WARNING - +2025-09-28 00:51:31,780 - src.services - WARNING - +2025-09-28 00:51:31,780 - src.services - WARNING - +2025-09-28 00:51:31,780 - src.services - WARNING - +2025-09-28 00:51:31,780 - src.services - WARNING - +2025-09-28 00:51:31,780 - src.services - WARNING - +2025-09-28 00:51:31,781 - src.services - WARNING - +2025-09-28 00:51:31,781 - src.services - WARNING - +2025-09-28 00:51:31,781 - src.services - WARNING - +2025-09-28 00:51:31,781 - src.services - WARNING - +2025-09-28 00:51:31,781 - src.services - WARNING - +2025-09-28 00:51:31,781 - src.services - WARNING - +2025-09-28 00:51:31,781 - src.services - WARNING - +2025-09-28 00:51:31,781 - src.services - WARNING - +2025-09-28 00:51:31,781 - src.services - WARNING - +2025-09-28 00:51:31,781 - src.services - WARNING - +2025-09-28 00:51:31,781 - src.services - WARNING - +2025-09-28 00:51:31,781 - src.services - WARNING - +2025-09-28 00:51:31,781 - src.services - WARNING - +2025-09-28 00:51:31,782 - src.services - WARNING - +2025-09-28 00:51:31,782 - src.services - WARNING - +2025-09-28 00:51:31,782 - src.services - WARNING - +2025-09-28 00:51:31,782 - src.services - WARNING - +2025-09-28 00:51:31,782 - src.services - WARNING - +2025-09-28 00:51:31,782 - src.services - WARNING - +2025-09-28 00:51:31,782 - src.services - WARNING - +2025-09-28 00:51:31,782 - src.services - WARNING - +2025-09-28 00:51:31,782 - src.services - WARNING - +2025-09-28 00:51:31,782 - src.services - WARNING - +2025-09-28 00:51:31,782 - src.services - WARNING - +2025-09-28 00:51:31,782 - src.services - WARNING - +2025-09-28 00:51:31,782 - src.services - WARNING - +2025-09-28 00:51:31,782 - src.services - WARNING - +2025-09-28 00:51:31,782 - src.services - WARNING - +2025-09-28 00:51:31,782 - src.services - WARNING - +2025-09-28 00:51:31,783 - src.services - WARNING - +2025-09-28 00:51:31,783 - src.services - WARNING - +2025-09-28 00:51:31,783 - src.services - WARNING - +2025-09-28 00:51:31,783 - src.services - WARNING - +2025-09-28 00:51:31,783 - src.services - WARNING - +2025-09-28 00:51:31,783 - src.services - WARNING - +2025-09-28 00:51:31,783 - src.services - WARNING - +2025-09-28 00:51:31,783 - src.services - WARNING - +2025-09-28 00:51:31,783 - src.services - WARNING - +2025-09-28 00:51:31,783 - src.services - WARNING - +2025-09-28 00:51:31,783 - src.services - WARNING - +2025-09-28 00:51:31,783 - src.services - WARNING - +2025-09-28 00:51:31,783 - src.services - WARNING - +2025-09-28 00:51:31,783 - src.services - WARNING - +2025-09-28 00:51:31,783 - src.services - WARNING - +2025-09-28 00:51:31,784 - src.services - WARNING - +2025-09-28 00:51:31,784 - src.services - WARNING - +2025-09-28 00:51:31,784 - src.services - WARNING - +2025-09-28 00:51:31,784 - src.services - WARNING - +2025-09-28 00:51:31,784 - src.services - WARNING - +2025-09-28 00:51:31,784 - src.services - WARNING - +2025-09-28 00:51:31,784 - src.services - WARNING - +2025-09-28 00:51:31,784 - src.services - WARNING - +2025-09-28 00:51:31,784 - src.services - WARNING - +2025-09-28 00:51:31,784 - src.services - WARNING - +2025-09-28 00:51:31,784 - src.services - WARNING - +2025-09-28 00:51:31,784 - src.services - WARNING - +2025-09-28 00:51:31,784 - src.services - WARNING - +2025-09-28 00:51:31,784 - src.services - WARNING - +2025-09-28 00:51:31,785 - src.services - WARNING - +2025-09-28 00:51:31,785 - src.services - WARNING - +2025-09-28 00:51:31,785 - src.services - WARNING - +2025-09-28 00:51:31,785 - src.services - WARNING - +2025-09-28 00:51:31,785 - src.services - WARNING - +2025-09-28 00:51:31,785 - src.services - WARNING - +2025-09-28 00:51:31,785 - src.services - WARNING - +2025-09-28 00:51:31,785 - src.services - WARNING - +2025-09-28 00:51:31,785 - src.services - WARNING - +2025-09-28 00:51:31,785 - src.services - WARNING - +2025-09-28 00:51:31,785 - src.services - WARNING - +2025-09-28 00:51:31,785 - src.services - WARNING - +2025-09-28 00:51:31,785 - src.services - WARNING - +2025-09-28 00:51:31,785 - src.services - WARNING - +2025-09-28 00:51:31,785 - src.services - WARNING - +2025-09-28 00:51:31,785 - src.services - WARNING - +2025-09-28 00:51:31,786 - src.services - WARNING - +2025-09-28 00:51:31,786 - src.services - WARNING - +2025-09-28 00:51:31,786 - src.services - WARNING - +2025-09-28 00:51:31,786 - src.services - WARNING - +2025-09-28 00:51:31,786 - src.services - WARNING - +2025-09-28 00:51:31,786 - src.services - WARNING - +2025-09-28 00:51:31,786 - src.services - WARNING - +2025-09-28 00:51:31,786 - src.services - WARNING - +2025-09-28 00:51:31,786 - src.services - WARNING - +2025-09-28 00:51:31,786 - src.services - WARNING - +2025-09-28 00:51:31,786 - src.services - WARNING - +2025-09-28 00:51:31,786 - src.services - WARNING - +2025-09-28 00:51:31,786 - src.services - WARNING - +2025-09-28 00:51:31,786 - src.services - WARNING - +2025-09-28 00:51:31,786 - src.services - WARNING - +2025-09-28 00:51:31,787 - src.services - WARNING - +2025-09-28 00:51:31,787 - src.services - WARNING - +2025-09-28 00:51:31,787 - src.services - WARNING - +2025-09-28 00:51:31,787 - src.services - WARNING - +2025-09-28 00:51:31,787 - src.services - WARNING - +2025-09-28 00:51:31,787 - src.services - WARNING - +2025-09-28 00:51:31,787 - src.services - WARNING - +2025-09-28 00:51:31,787 - src.services - WARNING - +2025-09-28 00:51:31,787 - src.services - WARNING - +2025-09-28 00:51:31,787 - src.services - WARNING - +2025-09-28 00:51:31,787 - src.services - WARNING - +2025-09-28 00:51:31,787 - src.services - WARNING - +2025-09-28 00:51:31,787 - src.services - WARNING - +2025-09-28 00:51:31,787 - src.services - WARNING - +2025-09-28 00:51:31,787 - src.services - WARNING - +2025-09-28 00:51:31,788 - src.services - WARNING - +2025-09-28 00:51:31,788 - src.services - WARNING - +2025-09-28 00:51:31,788 - src.services - WARNING - +2025-09-28 00:51:31,788 - src.services - WARNING - +2025-09-28 00:51:31,788 - src.services - WARNING - +2025-09-28 00:51:31,788 - src.services - WARNING - +2025-09-28 00:51:31,788 - src.services - WARNING - +2025-09-28 00:51:31,788 - src.services - WARNING - +2025-09-28 00:51:31,788 - src.services - WARNING - +2025-09-28 00:51:31,788 - src.services - WARNING - +2025-09-28 00:51:31,788 - src.services - WARNING - +2025-09-28 00:51:31,788 - src.services - WARNING - +2025-09-28 00:51:31,788 - src.services - WARNING - +2025-09-28 00:51:31,788 - src.services - WARNING - +2025-09-28 00:51:31,788 - src.services - WARNING - +2025-09-28 00:51:31,788 - src.services - WARNING - +2025-09-28 00:51:31,789 - src.services - WARNING - +2025-09-28 00:51:31,789 - src.services - WARNING - +2025-09-28 00:51:31,789 - src.services - WARNING - +2025-09-28 00:51:31,789 - src.services - WARNING - +2025-09-28 00:51:31,789 - src.services - WARNING - +2025-09-28 00:51:31,789 - src.services - WARNING - +2025-09-28 00:51:31,789 - src.services - WARNING - +2025-09-28 00:51:31,789 - src.services - WARNING - +2025-09-28 00:51:31,789 - src.services - WARNING - +2025-09-28 00:51:31,789 - src.services - WARNING - +2025-09-28 00:51:31,789 - src.services - WARNING - +2025-09-28 00:51:31,789 - src.services - WARNING - +2025-09-28 00:51:31,789 - src.services - WARNING - +2025-09-28 00:51:31,789 - src.services - WARNING - +2025-09-28 00:51:31,789 - src.services - WARNING - +2025-09-28 00:51:31,789 - src.services - WARNING - +2025-09-28 00:51:31,790 - src.services - WARNING - +2025-09-28 00:51:31,790 - src.services - WARNING - +2025-09-28 00:51:31,790 - src.services - WARNING - +2025-09-28 00:51:31,790 - src.services - WARNING - +2025-09-28 00:51:31,790 - src.services - WARNING - +2025-09-28 00:51:31,790 - src.services - WARNING - +2025-09-28 00:51:31,790 - src.services - WARNING - +2025-09-28 00:51:31,790 - src.services - WARNING - +2025-09-28 00:51:31,791 - src.services - WARNING - +2025-09-28 00:51:31,791 - src.services - WARNING - +2025-09-28 00:51:31,791 - src.services - WARNING - +2025-09-28 00:51:31,791 - src.services - WARNING - +2025-09-28 00:51:31,791 - src.services - WARNING - +2025-09-28 00:51:31,791 - src.services - WARNING - +2025-09-28 00:51:31,791 - src.services - WARNING - +2025-09-28 00:51:31,791 - src.services - WARNING - +2025-09-28 00:51:31,791 - src.services - WARNING - +2025-09-28 00:51:31,792 - src.services - WARNING - +2025-09-28 00:51:31,792 - src.services - WARNING - +2025-09-28 00:51:31,792 - src.services - WARNING - +2025-09-28 00:51:31,792 - src.services - WARNING - +2025-09-28 00:51:31,792 - src.services - WARNING - +2025-09-28 00:51:31,792 - src.services - WARNING - +2025-09-28 00:51:31,792 - src.services - WARNING - +2025-09-28 00:51:31,792 - src.services - WARNING - +2025-09-28 00:51:31,792 - src.services - WARNING - +2025-09-28 00:51:31,792 - src.services - WARNING - +2025-09-28 00:51:31,792 - src.services - WARNING - +2025-09-28 00:51:31,792 - src.services - WARNING - +2025-09-28 00:51:31,793 - src.services - WARNING - +2025-09-28 00:51:31,793 - src.services - WARNING - +2025-09-28 00:51:31,793 - src.services - WARNING - +2025-09-28 00:51:31,793 - src.services - WARNING - +2025-09-28 00:51:31,793 - src.services - WARNING - +2025-09-28 00:51:31,793 - src.services - WARNING - +2025-09-28 00:51:31,793 - src.services - WARNING - +2025-09-28 00:51:31,793 - src.services - WARNING - +2025-09-28 00:51:31,793 - src.services - WARNING - +2025-09-28 00:51:31,793 - src.services - WARNING - +2025-09-28 00:51:31,793 - src.services - WARNING - +2025-09-28 00:51:31,793 - src.services - WARNING - +2025-09-28 00:51:31,793 - src.services - WARNING - +2025-09-28 00:51:31,793 - src.services - WARNING - +2025-09-28 00:51:31,793 - src.services - WARNING - +2025-09-28 00:51:31,794 - src.services - WARNING - +2025-09-28 00:51:31,794 - src.services - WARNING - +2025-09-28 00:51:31,794 - src.services - WARNING - +2025-09-28 00:51:31,794 - src.services - WARNING - +2025-09-28 00:51:31,794 - src.services - WARNING - +2025-09-28 00:51:31,794 - src.services - WARNING - +2025-09-28 00:51:31,794 - src.services - WARNING - +2025-09-28 00:51:31,794 - src.services - WARNING - +2025-09-28 00:51:31,794 - src.services - WARNING - +2025-09-28 00:51:31,794 - src.services - WARNING - +2025-09-28 00:51:31,794 - src.services - WARNING - +2025-09-28 00:51:31,794 - src.services - WARNING - +2025-09-28 00:51:31,794 - src.services - WARNING - +2025-09-28 00:51:31,794 - src.services - WARNING - +2025-09-28 00:51:31,794 - src.services - WARNING - +2025-09-28 00:51:31,795 - src.services - WARNING - +2025-09-28 00:51:31,795 - src.services - WARNING - +2025-09-28 00:51:31,795 - src.services - WARNING - +2025-09-28 00:51:31,795 - src.services - WARNING - +2025-09-28 00:51:31,795 - src.services - WARNING - +2025-09-28 00:51:31,795 - src.services - WARNING - +2025-09-28 00:51:31,795 - src.services - WARNING - +2025-09-28 00:51:31,795 - src.services - WARNING - +2025-09-28 00:51:31,795 - src.services - WARNING - +2025-09-28 00:51:31,795 - src.services - WARNING - +2025-09-28 00:51:31,795 - src.services - WARNING - +2025-09-28 00:51:31,795 - src.services - WARNING - +2025-09-28 00:51:31,795 - src.services - WARNING - +2025-09-28 00:51:31,795 - src.services - WARNING - +2025-09-28 00:51:31,795 - src.services - WARNING - +2025-09-28 00:51:31,796 - src.services - WARNING - +2025-09-28 00:51:31,796 - src.services - WARNING - +2025-09-28 00:51:31,796 - src.services - WARNING - +2025-09-28 00:51:31,796 - src.services - WARNING - +2025-09-28 00:51:31,796 - src.services - WARNING - +2025-09-28 00:51:31,796 - src.services - WARNING - +2025-09-28 00:51:31,796 - src.services - WARNING - +2025-09-28 00:51:31,796 - src.services - WARNING - +2025-09-28 00:51:31,796 - src.services - WARNING - +2025-09-28 00:51:31,796 - src.services - WARNING - +2025-09-28 00:51:31,796 - src.services - WARNING - +2025-09-28 00:51:31,796 - src.services - WARNING - +2025-09-28 00:51:31,796 - src.services - WARNING - +2025-09-28 00:51:31,796 - src.services - WARNING - +2025-09-28 00:51:31,796 - src.services - WARNING - +2025-09-28 00:51:31,796 - src.services - WARNING - +2025-09-28 00:51:31,797 - src.services - WARNING - +2025-09-28 00:51:31,797 - src.services - WARNING - +2025-09-28 00:51:31,797 - src.services - WARNING - +2025-09-28 00:51:31,797 - src.services - WARNING - +2025-09-28 00:51:31,797 - src.services - WARNING - +2025-09-28 00:51:31,797 - src.services - WARNING - +2025-09-28 00:51:31,797 - src.services - WARNING - +2025-09-28 00:51:31,797 - src.services - WARNING - +2025-09-28 00:51:31,797 - src.services - WARNING - +2025-09-28 00:51:31,797 - src.services - WARNING - +2025-09-28 00:51:31,797 - src.services - WARNING - +2025-09-28 00:51:31,797 - src.services - WARNING - +2025-09-28 00:51:31,797 - src.services - WARNING - +2025-09-28 00:51:31,797 - src.services - WARNING - +2025-09-28 00:51:31,797 - src.services - WARNING - +2025-09-28 00:51:31,798 - src.services - WARNING - +2025-09-28 00:51:31,798 - src.services - WARNING - +2025-09-28 00:51:31,798 - src.services - WARNING - +2025-09-28 00:51:31,798 - src.services - WARNING - +2025-09-28 00:51:31,798 - src.services - WARNING - +2025-09-28 00:51:31,798 - src.services - WARNING - +2025-09-28 00:51:31,798 - src.services - WARNING - +2025-09-28 00:51:31,798 - src.services - WARNING - +2025-09-28 00:51:31,798 - src.services - WARNING - +2025-09-28 00:51:31,798 - src.services - WARNING - +2025-09-28 00:51:31,798 - src.services - WARNING - +2025-09-28 00:51:31,798 - src.services - WARNING - +2025-09-28 00:51:31,798 - src.services - WARNING - +2025-09-28 00:51:31,798 - src.services - WARNING - +2025-09-28 00:51:31,798 - src.services - WARNING - +2025-09-28 00:51:31,799 - src.services - WARNING - +2025-09-28 00:51:31,799 - src.services - WARNING - +2025-09-28 00:51:31,799 - src.services - WARNING - +2025-09-28 00:51:31,799 - src.services - WARNING - +2025-09-28 00:51:31,799 - src.services - WARNING - +2025-09-28 00:51:31,799 - src.services - WARNING - +2025-09-28 00:51:31,799 - src.services - WARNING - +2025-09-28 00:51:31,799 - src.services - WARNING - +2025-09-28 00:51:31,799 - src.services - WARNING - +2025-09-28 00:51:31,799 - src.services - WARNING - +2025-09-28 00:51:31,799 - src.services - WARNING - +2025-09-28 00:51:31,799 - src.services - WARNING - +2025-09-28 00:51:31,799 - src.services - WARNING - +2025-09-28 00:51:31,799 - src.services - WARNING - +2025-09-28 00:51:31,799 - src.services - WARNING - +2025-09-28 00:51:31,800 - src.services - WARNING - +2025-09-28 00:51:31,800 - src.services - WARNING - +2025-09-28 00:51:31,800 - src.services - WARNING - +2025-09-28 00:51:31,800 - src.services - WARNING - +2025-09-28 00:51:31,800 - src.services - WARNING - +2025-09-28 00:51:31,800 - src.services - WARNING - +2025-09-28 00:51:31,800 - src.services - WARNING - +2025-09-28 00:51:31,800 - src.services - WARNING - +2025-09-28 00:51:31,800 - src.services - WARNING - +2025-09-28 00:51:31,800 - src.services - WARNING - +2025-09-28 00:51:31,800 - src.services - WARNING - +2025-09-28 00:51:31,800 - src.services - WARNING - +2025-09-28 00:51:31,800 - src.services - WARNING - +2025-09-28 00:51:31,800 - src.services - WARNING - +2025-09-28 00:51:31,800 - src.services - WARNING - +2025-09-28 00:51:31,800 - src.services - WARNING - +2025-09-28 00:51:31,801 - src.services - WARNING - +2025-09-28 00:51:31,801 - src.services - WARNING - +2025-09-28 00:51:31,801 - src.services - WARNING - +2025-09-28 00:51:31,801 - src.services - WARNING - +2025-09-28 00:51:31,801 - src.services - WARNING - +2025-09-28 00:51:31,801 - src.services - WARNING - +2025-09-28 00:51:31,801 - src.services - WARNING - +2025-09-28 00:51:31,801 - src.services - WARNING - +2025-09-28 00:51:31,801 - src.services - WARNING - +2025-09-28 00:51:31,801 - src.services - WARNING - +2025-09-28 00:51:31,801 - src.services - WARNING - +2025-09-28 00:51:31,801 - src.services - WARNING - +2025-09-28 00:51:31,801 - src.services - WARNING - +2025-09-28 00:51:31,801 - src.services - WARNING - +2025-09-28 00:51:31,802 - src.services - WARNING - +2025-09-28 00:51:31,802 - src.services - WARNING - +2025-09-28 00:51:31,802 - src.services - WARNING - +2025-09-28 00:51:31,802 - src.services - WARNING - +2025-09-28 00:51:31,802 - src.services - WARNING - +2025-09-28 00:51:31,802 - src.services - WARNING - +2025-09-28 00:51:31,802 - src.services - WARNING - +2025-09-28 00:51:31,802 - src.services - WARNING - +2025-09-28 00:51:31,802 - src.services - WARNING - +2025-09-28 00:51:31,802 - src.services - WARNING - +2025-09-28 00:51:31,802 - src.services - WARNING - +2025-09-28 00:51:31,802 - src.services - WARNING - +2025-09-28 00:51:31,802 - src.services - WARNING - +2025-09-28 00:51:31,802 - src.services - WARNING - +2025-09-28 00:51:31,802 - src.services - WARNING - +2025-09-28 00:51:31,803 - src.services - WARNING - +2025-09-28 00:51:31,803 - src.services - WARNING - +2025-09-28 00:51:31,803 - src.services - WARNING - +2025-09-28 00:51:31,803 - src.services - WARNING - +2025-09-28 00:51:31,803 - src.services - WARNING - +2025-09-28 00:51:31,803 - src.services - WARNING - +2025-09-28 00:51:31,803 - src.services - WARNING - +2025-09-28 00:51:31,803 - src.services - WARNING - +2025-09-28 00:51:31,803 - src.services - WARNING - +2025-09-28 00:51:31,803 - src.services - WARNING - +2025-09-28 00:51:31,803 - src.services - WARNING - +2025-09-28 00:51:31,803 - src.services - WARNING - +2025-09-28 00:51:31,803 - src.services - WARNING - +2025-09-28 00:51:31,803 - src.services - WARNING - +2025-09-28 00:51:31,804 - src.services - WARNING - +2025-09-28 00:51:31,804 - src.services - WARNING - +2025-09-28 00:51:31,804 - src.services - WARNING - +2025-09-28 00:51:31,804 - src.services - WARNING - +2025-09-28 00:51:31,804 - src.services - WARNING - +2025-09-28 00:51:31,804 - src.services - WARNING - +2025-09-28 00:51:31,804 - src.services - WARNING - +2025-09-28 00:51:31,804 - src.services - WARNING - +2025-09-28 00:51:31,804 - src.services - WARNING - +2025-09-28 00:51:31,804 - src.services - WARNING - +2025-09-28 00:51:31,804 - src.services - WARNING - +2025-09-28 00:51:31,804 - src.services - WARNING - +2025-09-28 00:51:31,804 - src.services - WARNING - +2025-09-28 00:51:31,804 - src.services - WARNING - +2025-09-28 00:51:31,804 - src.services - WARNING - +2025-09-28 00:51:31,804 - src.services - WARNING - +2025-09-28 00:51:31,805 - src.services - WARNING - +2025-09-28 00:51:31,805 - src.services - WARNING - +2025-09-28 00:51:31,805 - src.services - WARNING - +2025-09-28 00:51:31,805 - src.services - WARNING - +2025-09-28 00:51:31,805 - src.services - WARNING - +2025-09-28 00:51:31,805 - src.services - WARNING - +2025-09-28 00:51:31,805 - src.services - WARNING - +2025-09-28 00:51:31,805 - src.services - WARNING - +2025-09-28 00:51:31,805 - src.services - WARNING - +2025-09-28 00:51:31,805 - src.services - WARNING - +2025-09-28 00:51:31,805 - src.services - WARNING - +2025-09-28 00:51:31,805 - src.services - WARNING - +2025-09-28 00:51:31,805 - src.services - WARNING - +2025-09-28 00:51:31,805 - src.services - WARNING - +2025-09-28 00:51:31,805 - src.services - WARNING - +2025-09-28 00:51:31,806 - src.services - WARNING - +2025-09-28 00:51:31,806 - src.services - WARNING - +2025-09-28 00:51:31,806 - src.services - WARNING - +2025-09-28 00:51:31,806 - src.services - WARNING - +2025-09-28 00:51:31,806 - src.services - WARNING - +2025-09-28 00:51:31,806 - src.services - WARNING - +2025-09-28 00:51:31,806 - src.services - WARNING - +2025-09-28 00:51:31,806 - src.services - WARNING - +2025-09-28 00:51:31,806 - src.services - WARNING - +2025-09-28 00:51:31,807 - src.services - WARNING - +2025-09-28 00:51:31,807 - src.services - WARNING - +2025-09-28 00:51:31,807 - src.services - WARNING - +2025-09-28 00:51:31,807 - src.services - WARNING - +2025-09-28 00:51:31,807 - src.services - WARNING - +2025-09-28 00:51:31,807 - src.services - WARNING - +2025-09-28 00:51:31,807 - src.services - WARNING - +2025-09-28 00:51:31,807 - src.services - WARNING - +2025-09-28 00:51:31,807 - src.services - WARNING - +2025-09-28 00:51:31,808 - src.services - WARNING - +2025-09-28 00:51:31,808 - src.services - WARNING - +2025-09-28 00:51:31,808 - src.services - WARNING - +2025-09-28 00:51:31,808 - src.services - WARNING - +2025-09-28 00:51:31,808 - src.services - WARNING - +2025-09-28 00:51:31,808 - src.services - WARNING - +2025-09-28 00:51:31,808 - src.services - WARNING - +2025-09-28 00:51:31,808 - src.services - WARNING - +2025-09-28 00:51:31,808 - src.services - WARNING - +2025-09-28 00:51:31,808 - src.services - WARNING - +2025-09-28 00:51:31,808 - src.services - WARNING - +2025-09-28 00:51:31,808 - src.services - WARNING - +2025-09-28 00:51:31,808 - src.services - WARNING - +2025-09-28 00:51:31,808 - src.services - WARNING - +2025-09-28 00:51:31,808 - src.services - WARNING - +2025-09-28 00:51:31,809 - src.services - WARNING - +2025-09-28 00:51:31,809 - src.services - WARNING - +2025-09-28 00:51:31,809 - src.services - WARNING - +2025-09-28 00:51:31,809 - src.services - WARNING - +2025-09-28 00:51:31,809 - src.services - WARNING - +2025-09-28 00:51:31,809 - src.services - WARNING - +2025-09-28 00:51:31,809 - src.services - WARNING - +2025-09-28 00:51:31,809 - src.services - WARNING - +2025-09-28 00:51:31,809 - src.services - WARNING - +2025-09-28 00:51:31,809 - src.services - WARNING - +2025-09-28 00:51:31,809 - src.services - WARNING - +2025-09-28 00:51:31,809 - src.services - WARNING - +2025-09-28 00:51:31,809 - src.services - WARNING - +2025-09-28 00:51:31,809 - src.services - WARNING - +2025-09-28 00:51:31,809 - src.services - WARNING - +2025-09-28 00:51:31,810 - src.services - WARNING - +2025-09-28 00:51:31,810 - src.services - WARNING - +2025-09-28 00:51:31,810 - src.services - WARNING - +2025-09-28 00:51:31,810 - src.services - WARNING - +2025-09-28 00:51:31,810 - src.services - WARNING - +2025-09-28 00:51:31,810 - src.services - WARNING - +2025-09-28 00:51:31,810 - src.services - WARNING - +2025-09-28 00:51:31,810 - src.services - WARNING - +2025-09-28 00:51:31,810 - src.services - WARNING - +2025-09-28 00:51:31,810 - src.services - WARNING - +2025-09-28 00:51:31,810 - src.services - WARNING - +2025-09-28 00:51:31,810 - src.services - WARNING - +2025-09-28 00:51:31,810 - src.services - WARNING - +2025-09-28 00:51:31,810 - src.services - WARNING - +2025-09-28 00:51:31,810 - src.services - WARNING - +2025-09-28 00:51:31,811 - src.services - WARNING - +2025-09-28 00:51:31,811 - src.services - WARNING - +2025-09-28 00:51:31,811 - src.services - WARNING - +2025-09-28 00:51:31,811 - src.services - WARNING - +2025-09-28 00:51:31,811 - src.services - WARNING - +2025-09-28 00:51:31,811 - src.services - WARNING - +2025-09-28 00:51:31,811 - src.services - WARNING - +2025-09-28 00:51:31,811 - src.services - WARNING - +2025-09-28 00:51:31,811 - src.services - WARNING - +2025-09-28 00:51:31,811 - src.services - WARNING - +2025-09-28 00:51:31,811 - src.services - WARNING - +2025-09-28 00:51:31,811 - src.services - WARNING - +2025-09-28 00:51:31,811 - src.services - WARNING - +2025-09-28 00:51:31,811 - src.services - WARNING - +2025-09-28 00:51:31,811 - src.services - WARNING - +2025-09-28 00:51:31,811 - src.services - WARNING - +2025-09-28 00:51:31,812 - src.services - WARNING - +2025-09-28 00:51:31,812 - src.services - WARNING - +2025-09-28 00:51:31,812 - src.services - WARNING - +2025-09-28 00:51:31,812 - src.services - WARNING - +2025-09-28 00:51:31,812 - src.services - WARNING - +2025-09-28 00:51:31,812 - src.services - WARNING - +2025-09-28 00:51:31,812 - src.services - WARNING - +2025-09-28 00:51:31,812 - src.services - WARNING - +2025-09-28 00:51:31,812 - src.services - WARNING - +2025-09-28 00:51:31,812 - src.services - WARNING - +2025-09-28 00:51:31,812 - src.services - WARNING - +2025-09-28 00:51:31,812 - src.services - WARNING - +2025-09-28 00:51:31,812 - src.services - WARNING - +2025-09-28 00:51:31,812 - src.services - WARNING - +2025-09-28 00:51:31,812 - src.services - WARNING - +2025-09-28 00:51:31,813 - src.services - WARNING - +2025-09-28 00:51:31,813 - src.services - WARNING - +2025-09-28 00:51:31,813 - src.services - WARNING - +2025-09-28 00:51:31,813 - src.services - WARNING - +2025-09-28 00:51:31,813 - src.services - WARNING - +2025-09-28 00:51:31,813 - src.services - WARNING - +2025-09-28 00:51:31,813 - src.services - WARNING - +2025-09-28 00:51:31,813 - src.services - WARNING - +2025-09-28 00:51:31,813 - src.services - WARNING - +2025-09-28 00:51:31,813 - src.services - WARNING - +2025-09-28 00:51:31,813 - src.services - WARNING - +2025-09-28 00:51:31,813 - src.services - WARNING - +2025-09-28 00:51:31,813 - src.services - WARNING - +2025-09-28 00:51:31,813 - src.services - WARNING - +2025-09-28 00:51:31,813 - src.services - WARNING - +2025-09-28 00:51:31,813 - src.services - WARNING - +2025-09-28 00:51:31,814 - src.services - WARNING - +2025-09-28 00:51:31,814 - src.services - WARNING - +2025-09-28 00:51:31,814 - src.services - WARNING - +2025-09-28 00:51:31,814 - src.services - WARNING - +2025-09-28 00:51:31,814 - src.services - WARNING - +2025-09-28 00:51:31,814 - src.services - WARNING - +2025-09-28 00:51:31,814 - src.services - WARNING - +2025-09-28 00:51:31,814 - src.services - WARNING - +2025-09-28 00:51:31,814 - src.services - WARNING - +2025-09-28 00:51:31,814 - src.services - WARNING - +2025-09-28 00:51:31,814 - src.services - WARNING - +2025-09-28 00:51:31,814 - src.services - WARNING - +2025-09-28 00:51:31,814 - src.services - WARNING - +2025-09-28 00:51:31,814 - src.services - WARNING - +2025-09-28 00:51:31,814 - src.services - WARNING - +2025-09-28 00:51:31,815 - src.services - WARNING - +2025-09-28 00:51:31,815 - src.services - WARNING - +2025-09-28 00:51:31,815 - src.services - WARNING - +2025-09-28 00:51:31,815 - src.services - WARNING - +2025-09-28 00:51:31,815 - src.services - WARNING - +2025-09-28 00:51:31,815 - src.services - WARNING - +2025-09-28 00:51:31,815 - src.services - WARNING - +2025-09-28 00:51:31,815 - src.services - WARNING - +2025-09-28 00:51:31,815 - src.services - WARNING - +2025-09-28 00:51:31,815 - src.services - WARNING - +2025-09-28 00:51:31,815 - src.services - WARNING - +2025-09-28 00:51:31,815 - src.services - WARNING - +2025-09-28 00:51:31,815 - src.services - WARNING - +2025-09-28 00:51:31,815 - src.services - WARNING - +2025-09-28 00:51:31,816 - src.services - WARNING - +2025-09-28 00:51:31,816 - src.services - WARNING - +2025-09-28 00:51:31,816 - src.services - WARNING - +2025-09-28 00:51:31,816 - src.services - WARNING - +2025-09-28 00:51:31,816 - src.services - WARNING - +2025-09-28 00:51:31,816 - src.services - WARNING - +2025-09-28 00:51:31,816 - src.services - WARNING - +2025-09-28 00:51:31,816 - src.services - WARNING - +2025-09-28 00:51:31,816 - src.services - WARNING - +2025-09-28 00:51:31,816 - src.services - WARNING - +2025-09-28 00:51:31,816 - src.services - WARNING - +2025-09-28 00:51:31,816 - src.services - WARNING - +2025-09-28 00:51:31,816 - src.services - WARNING - +2025-09-28 00:51:31,816 - src.services - WARNING - +2025-09-28 00:51:31,816 - src.services - WARNING - +2025-09-28 00:51:31,817 - src.services - WARNING - +2025-09-28 00:51:31,817 - src.services - WARNING - +2025-09-28 00:51:31,817 - src.services - WARNING - +2025-09-28 00:51:31,817 - src.services - WARNING - +2025-09-28 00:51:31,817 - src.services - WARNING - +2025-09-28 00:51:31,817 - src.services - WARNING - +2025-09-28 00:51:31,817 - src.services - WARNING - +2025-09-28 00:51:31,817 - src.services - WARNING - +2025-09-28 00:51:31,817 - src.services - WARNING - +2025-09-28 00:51:31,817 - src.services - WARNING - +2025-09-28 00:51:31,817 - src.services - WARNING - +2025-09-28 00:51:31,817 - src.services - WARNING - +2025-09-28 00:51:31,817 - src.services - WARNING - +2025-09-28 00:51:31,817 - src.services - WARNING - +2025-09-28 00:51:31,817 - src.services - WARNING - +2025-09-28 00:51:31,817 - src.services - WARNING - +2025-09-28 00:51:31,818 - src.services - WARNING - +2025-09-28 00:51:31,818 - src.services - WARNING - +2025-09-28 00:51:31,818 - src.services - WARNING - +2025-09-28 00:51:31,818 - src.services - WARNING - +2025-09-28 00:51:31,818 - src.services - WARNING - +2025-09-28 00:51:31,818 - src.services - WARNING - +2025-09-28 00:51:31,818 - src.services - WARNING - +2025-09-28 00:51:31,818 - src.services - WARNING - +2025-09-28 00:51:31,818 - src.services - WARNING - +2025-09-28 00:51:31,818 - src.services - WARNING - +2025-09-28 00:51:31,818 - src.services - WARNING - +2025-09-28 00:51:31,818 - src.services - WARNING - +2025-09-28 00:51:31,818 - src.services - WARNING - +2025-09-28 00:51:31,818 - src.services - WARNING - +2025-09-28 00:51:31,818 - src.services - WARNING - +2025-09-28 00:51:31,818 - src.services - WARNING - +2025-09-28 00:51:31,819 - src.services - WARNING - +2025-09-28 00:51:31,819 - src.services - WARNING - +2025-09-28 00:51:31,819 - src.services - WARNING - +2025-09-28 00:51:31,819 - src.services - WARNING - +2025-09-28 00:51:31,819 - src.services - WARNING - +2025-09-28 00:51:31,819 - src.services - WARNING - +2025-09-28 00:51:31,819 - src.services - WARNING - +2025-09-28 00:51:31,819 - src.services - WARNING - +2025-09-28 00:51:31,819 - src.services - WARNING - +2025-09-28 00:51:31,819 - src.services - WARNING - +2025-09-28 00:51:31,819 - src.services - WARNING - +2025-09-28 00:51:31,819 - src.services - WARNING - +2025-09-28 00:51:31,819 - src.services - WARNING - +2025-09-28 00:51:31,819 - src.services - WARNING - +2025-09-28 00:51:31,819 - src.services - WARNING - +2025-09-28 00:51:31,820 - src.services - WARNING - +2025-09-28 00:51:31,820 - src.services - WARNING - +2025-09-28 00:51:31,820 - src.services - WARNING - +2025-09-28 00:51:31,820 - src.services - WARNING - +2025-09-28 00:51:31,820 - src.services - WARNING - +2025-09-28 00:51:31,820 - src.services - WARNING - +2025-09-28 00:51:31,820 - src.services - WARNING - +2025-09-28 00:51:31,820 - src.services - WARNING - +2025-09-28 00:51:31,820 - src.services - WARNING - +2025-09-28 00:51:31,820 - src.services - WARNING - +2025-09-28 00:51:31,820 - src.services - WARNING - +2025-09-28 00:51:31,820 - src.services - WARNING - +2025-09-28 00:51:31,820 - src.services - WARNING - +2025-09-28 00:51:31,820 - src.services - WARNING - +2025-09-28 00:51:31,820 - src.services - WARNING - +2025-09-28 00:51:31,820 - src.services - WARNING - +2025-09-28 00:51:31,821 - src.services - WARNING - +2025-09-28 00:51:31,821 - src.services - WARNING - +2025-09-28 00:51:31,821 - src.services - WARNING - +2025-09-28 00:51:31,821 - src.services - WARNING - +2025-09-28 00:51:31,821 - src.services - WARNING - +2025-09-28 00:51:31,821 - src.services - WARNING - +2025-09-28 00:51:31,821 - src.services - WARNING - +2025-09-28 00:51:31,821 - src.services - WARNING - +2025-09-28 00:51:31,821 - src.services - WARNING - +2025-09-28 00:51:31,822 - src.services - WARNING - +2025-09-28 00:51:31,822 - src.services - WARNING - +2025-09-28 00:51:31,822 - src.services - WARNING - +2025-09-28 00:51:31,822 - src.services - WARNING - +2025-09-28 00:51:31,822 - src.services - WARNING - +2025-09-28 00:51:31,822 - src.services - WARNING - +2025-09-28 00:51:31,822 - src.services - WARNING - +2025-09-28 00:51:31,822 - src.services - WARNING - +2025-09-28 00:51:31,822 - src.services - WARNING - +2025-09-28 00:51:31,822 - src.services - WARNING - +2025-09-28 00:51:31,822 - src.services - WARNING - +2025-09-28 00:51:31,822 - src.services - WARNING - +2025-09-28 00:51:31,822 - src.services - WARNING - +2025-09-28 00:51:31,822 - src.services - WARNING - +2025-09-28 00:51:31,823 - src.services - WARNING - +2025-09-28 00:51:31,823 - src.services - WARNING - +2025-09-28 00:51:31,823 - src.services - WARNING - +2025-09-28 00:51:31,823 - src.services - WARNING - +2025-09-28 00:51:31,823 - src.services - WARNING - +2025-09-28 00:51:31,823 - src.services - WARNING - +2025-09-28 00:51:31,823 - src.services - WARNING - +2025-09-28 00:51:31,823 - src.services - WARNING - +2025-09-28 00:51:31,823 - src.services - WARNING - +2025-09-28 00:51:31,823 - src.services - WARNING - +2025-09-28 00:51:31,823 - src.services - WARNING - +2025-09-28 00:51:31,823 - src.services - WARNING - +2025-09-28 00:51:31,823 - src.services - WARNING - +2025-09-28 00:51:31,823 - src.services - WARNING - +2025-09-28 00:51:31,823 - src.services - WARNING - +2025-09-28 00:51:31,823 - src.services - WARNING - +2025-09-28 00:51:31,824 - src.services - WARNING - +2025-09-28 00:51:31,824 - src.services - WARNING - +2025-09-28 00:51:31,824 - src.services - WARNING - +2025-09-28 00:51:31,824 - src.services - WARNING - +2025-09-28 00:51:31,824 - src.services - WARNING - +2025-09-28 00:51:31,824 - src.services - WARNING - +2025-09-28 00:51:31,824 - src.services - WARNING - +2025-09-28 00:51:31,824 - src.services - WARNING - +2025-09-28 00:51:31,824 - src.services - WARNING - +2025-09-28 00:51:31,824 - src.services - WARNING - +2025-09-28 00:51:31,824 - src.services - WARNING - +2025-09-28 00:51:31,824 - src.services - WARNING - +2025-09-28 00:51:31,824 - src.services - WARNING - +2025-09-28 00:51:31,824 - src.services - WARNING - +2025-09-28 00:51:31,824 - src.services - WARNING - +2025-09-28 00:51:31,825 - src.services - WARNING - +2025-09-28 00:51:31,825 - src.services - WARNING - +2025-09-28 00:51:31,825 - src.services - WARNING - +2025-09-28 00:51:31,825 - src.services - WARNING - +2025-09-28 00:51:31,825 - src.services - WARNING - +2025-09-28 00:51:31,825 - src.services - WARNING - +2025-09-28 00:51:31,825 - src.services - WARNING - +2025-09-28 00:51:31,825 - src.services - WARNING - +2025-09-28 00:51:31,825 - src.services - WARNING - +2025-09-28 00:51:31,825 - src.services - WARNING - +2025-09-28 00:51:31,825 - src.services - WARNING - +2025-09-28 00:51:31,826 - src.services - WARNING - +2025-09-28 00:51:31,826 - src.services - WARNING - +2025-09-28 00:51:31,826 - src.services - WARNING - +2025-09-28 00:51:31,826 - src.services - WARNING - +2025-09-28 00:51:31,826 - src.services - WARNING - +2025-09-28 00:51:31,826 - src.services - WARNING - +2025-09-28 00:51:31,826 - src.services - WARNING - +2025-09-28 00:51:31,826 - src.services - WARNING - +2025-09-28 00:51:31,826 - src.services - WARNING - +2025-09-28 00:51:31,826 - src.services - WARNING - +2025-09-28 00:51:31,826 - src.services - WARNING - +2025-09-28 00:51:31,826 - src.services - WARNING - +2025-09-28 00:51:31,827 - src.services - WARNING - +2025-09-28 00:51:31,827 - src.services - WARNING - +2025-09-28 00:51:31,827 - src.services - WARNING - +2025-09-28 00:51:31,827 - src.services - WARNING - +2025-09-28 00:51:31,827 - src.services - WARNING - +2025-09-28 00:51:31,827 - src.services - WARNING - +2025-09-28 00:51:31,827 - src.services - WARNING - +2025-09-28 00:51:31,827 - src.services - WARNING - +2025-09-28 00:51:31,827 - src.services - WARNING - +2025-09-28 00:51:31,827 - src.services - WARNING - +2025-09-28 00:51:31,827 - src.services - WARNING - +2025-09-28 00:51:31,827 - src.services - WARNING - +2025-09-28 00:51:31,827 - src.services - WARNING - +2025-09-28 00:51:31,827 - src.services - WARNING - +2025-09-28 00:51:31,827 - src.services - WARNING - +2025-09-28 00:51:31,828 - src.services - WARNING - +2025-09-28 00:51:31,828 - src.services - WARNING - +2025-09-28 00:51:31,828 - src.services - WARNING - +2025-09-28 00:51:31,828 - src.services - WARNING - +2025-09-28 00:51:31,828 - src.services - WARNING - +2025-09-28 00:51:31,828 - src.services - WARNING - +2025-09-28 00:51:31,828 - src.services - WARNING - +2025-09-28 00:51:31,828 - src.services - WARNING - +2025-09-28 00:51:31,828 - src.services - WARNING - +2025-09-28 00:51:31,828 - src.services - WARNING - +2025-09-28 00:51:31,828 - src.services - WARNING - +2025-09-28 00:51:31,828 - src.services - WARNING - +2025-09-28 00:51:31,828 - src.services - WARNING - +2025-09-28 00:51:31,828 - src.services - WARNING - +2025-09-28 00:51:31,828 - src.services - WARNING - +2025-09-28 00:51:31,829 - src.services - WARNING - +2025-09-28 00:51:31,829 - src.services - WARNING - +2025-09-28 00:51:31,829 - src.services - WARNING - +2025-09-28 00:51:31,829 - src.services - WARNING - +2025-09-28 00:51:31,829 - src.services - WARNING - +2025-09-28 00:51:31,829 - src.services - WARNING - +2025-09-28 00:51:31,829 - src.services - WARNING - +2025-09-28 00:51:31,829 - src.services - WARNING - +2025-09-28 00:51:31,829 - src.services - WARNING - +2025-09-28 00:51:31,829 - src.services - WARNING - +2025-09-28 00:51:31,829 - src.services - WARNING - +2025-09-28 00:51:31,829 - src.services - WARNING - +2025-09-28 00:51:31,829 - src.services - WARNING - +2025-09-28 00:51:31,829 - src.services - WARNING - +2025-09-28 00:51:31,829 - src.services - WARNING - +2025-09-28 00:51:31,829 - src.services - WARNING - +2025-09-28 00:51:31,830 - src.services - WARNING - +2025-09-28 00:51:31,830 - src.services - WARNING - +2025-09-28 00:51:31,830 - src.services - WARNING - +2025-09-28 00:51:31,830 - src.services - WARNING - +2025-09-28 00:51:31,830 - src.services - WARNING - +2025-09-28 00:51:31,830 - src.services - WARNING - +2025-09-28 00:51:31,830 - src.services - WARNING - +2025-09-28 00:51:31,830 - src.services - WARNING - +2025-09-28 00:51:31,830 - src.services - WARNING - +2025-09-28 00:51:31,830 - src.services - WARNING - +2025-09-28 00:51:31,830 - src.services - WARNING - +2025-09-28 00:51:31,830 - src.services - WARNING - +2025-09-28 00:51:31,830 - src.services - WARNING - +2025-09-28 00:51:31,830 - src.services - WARNING - +2025-09-28 00:51:31,830 - src.services - WARNING - +2025-09-28 00:51:31,830 - src.services - WARNING - +2025-09-28 00:51:31,831 - src.services - WARNING - +2025-09-28 00:51:31,831 - src.services - WARNING - +2025-09-28 00:51:31,831 - src.services - WARNING - +2025-09-28 00:51:31,831 - src.services - WARNING - +2025-09-28 00:51:31,831 - src.services - WARNING - +2025-09-28 00:51:31,831 - src.services - WARNING - +2025-09-28 00:51:31,831 - src.services - WARNING - +2025-09-28 00:51:31,831 - src.services - WARNING - +2025-09-28 00:51:31,831 - src.services - WARNING - +2025-09-28 00:51:31,831 - src.services - WARNING - +2025-09-28 00:51:31,831 - src.services - WARNING - +2025-09-28 00:51:31,831 - src.services - WARNING - +2025-09-28 00:51:31,831 - src.services - WARNING - +2025-09-28 00:51:31,831 - src.services - WARNING - +2025-09-28 00:51:31,831 - src.services - WARNING - +2025-09-28 00:51:31,832 - src.services - WARNING - +2025-09-28 00:51:31,832 - src.services - WARNING - +2025-09-28 00:51:31,832 - src.services - WARNING - +2025-09-28 00:51:31,832 - src.services - WARNING - +2025-09-28 00:51:31,832 - src.services - WARNING - +2025-09-28 00:51:31,832 - src.services - WARNING - +2025-09-28 00:51:31,832 - src.services - WARNING - +2025-09-28 00:51:31,832 - src.services - WARNING - +2025-09-28 00:51:31,832 - src.services - WARNING - +2025-09-28 00:51:31,832 - src.services - WARNING - +2025-09-28 00:51:31,832 - src.services - WARNING - +2025-09-28 00:51:31,832 - src.services - WARNING - +2025-09-28 00:51:31,832 - src.services - WARNING - +2025-09-28 00:51:31,832 - src.services - WARNING - +2025-09-28 00:51:31,832 - src.services - WARNING - +2025-09-28 00:51:31,832 - src.services - WARNING - +2025-09-28 00:51:31,833 - src.services - WARNING - +2025-09-28 00:51:31,833 - src.services - WARNING - +2025-09-28 00:51:31,833 - src.services - WARNING - +2025-09-28 00:51:31,833 - src.services - WARNING - +2025-09-28 00:51:31,833 - src.services - WARNING - +2025-09-28 00:51:31,833 - src.services - WARNING - +2025-09-28 00:51:31,833 - src.services - WARNING - +2025-09-28 00:51:31,833 - src.services - WARNING - +2025-09-28 00:51:31,833 - src.services - WARNING - +2025-09-28 00:51:31,833 - src.services - WARNING - +2025-09-28 00:51:31,833 - src.services - WARNING - +2025-09-28 00:51:31,833 - src.services - WARNING - +2025-09-28 00:51:31,833 - src.services - WARNING - +2025-09-28 00:51:31,833 - src.services - WARNING - +2025-09-28 00:51:31,833 - src.services - WARNING - +2025-09-28 00:51:31,833 - src.services - WARNING - +2025-09-28 00:51:31,834 - src.services - WARNING - +2025-09-28 00:51:31,834 - src.services - WARNING - +2025-09-28 00:51:31,834 - src.services - WARNING - +2025-09-28 00:51:31,834 - src.services - WARNING - +2025-09-28 00:51:31,834 - src.services - WARNING - +2025-09-28 00:51:31,834 - src.services - WARNING - +2025-09-28 00:51:31,834 - src.services - WARNING - +2025-09-28 00:51:31,834 - src.services - WARNING - +2025-09-28 00:51:31,834 - src.services - WARNING - +2025-09-28 00:51:31,834 - src.services - WARNING - +2025-09-28 00:51:31,834 - src.services - WARNING - +2025-09-28 00:51:31,834 - src.services - WARNING - +2025-09-28 00:51:31,834 - src.services - WARNING - +2025-09-28 00:51:31,835 - src.services - WARNING - +2025-09-28 00:51:31,835 - src.services - WARNING - +2025-09-28 00:51:31,835 - src.services - WARNING - +2025-09-28 00:51:31,835 - src.services - WARNING - +2025-09-28 00:51:31,835 - src.services - WARNING - +2025-09-28 00:51:31,835 - src.services - WARNING - +2025-09-28 00:51:31,835 - src.services - WARNING - +2025-09-28 00:51:31,835 - src.services - WARNING - +2025-09-28 00:51:31,835 - src.services - WARNING - +2025-09-28 00:51:31,835 - src.services - WARNING - +2025-09-28 00:51:31,835 - src.services - WARNING - +2025-09-28 00:51:31,835 - src.services - WARNING - +2025-09-28 00:51:31,835 - src.services - WARNING - +2025-09-28 00:51:31,835 - src.services - WARNING - +2025-09-28 00:51:31,836 - src.services - WARNING - +2025-09-28 00:51:31,836 - src.services - WARNING - +2025-09-28 00:51:31,836 - src.services - WARNING - +2025-09-28 00:51:31,836 - src.services - WARNING - +2025-09-28 00:51:31,836 - src.services - WARNING - +2025-09-28 00:51:31,836 - src.services - WARNING - +2025-09-28 00:51:31,836 - src.services - WARNING - +2025-09-28 00:51:31,836 - src.services - WARNING - +2025-09-28 00:51:31,836 - src.services - WARNING - +2025-09-28 00:51:31,836 - src.services - WARNING - +2025-09-28 00:51:31,836 - src.services - WARNING - +2025-09-28 00:51:31,836 - src.services - WARNING - +2025-09-28 00:51:31,836 - src.services - WARNING - +2025-09-28 00:51:31,836 - src.services - WARNING - +2025-09-28 00:51:31,836 - src.services - WARNING - +2025-09-28 00:51:31,836 - src.services - WARNING - +2025-09-28 00:51:31,837 - src.services - WARNING - +2025-09-28 00:51:31,837 - src.services - WARNING - +2025-09-28 00:51:31,837 - src.services - WARNING - +2025-09-28 00:51:31,837 - src.services - WARNING - +2025-09-28 00:51:31,837 - src.services - WARNING - +2025-09-28 00:51:31,837 - src.services - WARNING - +2025-09-28 00:51:31,837 - src.services - WARNING - +2025-09-28 00:51:31,837 - src.services - WARNING - +2025-09-28 00:51:31,837 - src.services - WARNING - +2025-09-28 00:51:31,837 - src.services - WARNING - +2025-09-28 00:51:31,837 - src.services - WARNING - +2025-09-28 00:51:31,837 - src.services - WARNING - +2025-09-28 00:51:31,837 - src.services - WARNING - +2025-09-28 00:51:31,837 - src.services - WARNING - +2025-09-28 00:51:31,837 - src.services - WARNING - +2025-09-28 00:51:31,838 - src.services - WARNING - +2025-09-28 00:51:31,838 - src.services - WARNING - +2025-09-28 00:51:31,838 - src.services - WARNING - +2025-09-28 00:51:31,838 - src.services - WARNING - +2025-09-28 00:51:31,838 - src.services - WARNING - +2025-09-28 00:51:31,838 - src.services - WARNING - +2025-09-28 00:51:31,838 - src.services - INFO - investment_bank From 13db42da7c33042141fa5b755ef0e68fd8695ff7 Mon Sep 17 00:00:00 2001 From: ilya_kim Date: Sun, 28 Sep 2025 00:58:52 +0300 Subject: [PATCH 03/12] appdate git --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 71c7e50..0438420 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ poetry.lock *.xlsx *.json .DS_Store +run_app.py +test_app.py From d635de515c527d6839991579474750055de615e7 Mon Sep 17 00:00:00 2001 From: ilya_kim Date: Sun, 28 Sep 2025 01:01:19 +0300 Subject: [PATCH 04/12] appdate git --- .gitignore | 2 -- 1 file changed, 2 deletions(-) diff --git a/.gitignore b/.gitignore index 0438420..71c7e50 100644 --- a/.gitignore +++ b/.gitignore @@ -6,5 +6,3 @@ poetry.lock *.xlsx *.json .DS_Store -run_app.py -test_app.py From 60d1d895bd40f99da687610551c6c9621692f76f Mon Sep 17 00:00:00 2001 From: ilya_kim Date: Tue, 30 Sep 2025 19:39:47 +0300 Subject: [PATCH 05/12] Update .gitignore --- .gitignore | 218 +++++++++++++++++- .idea/.gitignore | 3 - .idea/PythonProject9.iml | 10 - .../inspectionProfiles/profiles_settings.xml | 6 - .idea/misc.xml | 26 --- .idea/modules.xml | 8 - .idea/vcs.xml | 6 - 7 files changed, 213 insertions(+), 64 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/PythonProject9.iml delete mode 100644 .idea/inspectionProfiles/profiles_settings.xml delete mode 100644 .idea/misc.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/vcs.xml diff --git a/.gitignore b/.gitignore index 71c7e50..e0b3257 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,216 @@ +# Byte-compiled / optimized / DLL files __pycache__/ -*.pyc +*.py[codz] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py.cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +# Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +# poetry.lock +# poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +# pdm.lock +# pdm.toml +.pdm-python +.pdm-build/ + +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +# pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# Redis +*.rdb +*.aof +*.pid + +# RabbitMQ +mnesia/ +rabbitmq/ +rabbitmq-data/ + +# ActiveMQ +activemq-data/ + +# SageMath parsed files +*.sage.py + +# Environments .env +.envrc .venv -poetry.lock -*.xlsx -*.json -.DS_Store +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +# .idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ + +# Streamlit +.streamlit/secrets.toml \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 26d3352..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/.idea/PythonProject9.iml b/.idea/PythonProject9.iml deleted file mode 100644 index ee944cf..0000000 --- a/.idea/PythonProject9.iml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml deleted file mode 100644 index 105ce2d..0000000 --- a/.idea/inspectionProfiles/profiles_settings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 48c6ff1..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - Markdown - - - Python - - - - - UvPackageVersionsInspection - - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 1570b0d..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file From f4530202993d3fabee3ac48dee42af26d8e5bf44 Mon Sep 17 00:00:00 2001 From: ilya_kim Date: Tue, 30 Sep 2025 22:01:19 +0300 Subject: [PATCH 06/12] appdate to flake8 --- src/api_client.py | 24 +++++--- src/config.py | 5 +- src/main.py | 28 ++++------ src/reports.py | 8 +-- src/services.py | 23 ++++---- src/utils.py | 138 +++++++++++++++++++++++----------------------- src/views.py | 51 ++++------------- 7 files changed, 124 insertions(+), 153 deletions(-) diff --git a/src/api_client.py b/src/api_client.py index 0629b72..db2f0b6 100644 --- a/src/api_client.py +++ b/src/api_client.py @@ -1,10 +1,10 @@ -import aiohttp import asyncio -import json -import cachetools -from datetime import datetime -from typing import Dict, List, Any, Optional import logging +from typing import Dict, List, Optional + +import aiohttp +import cachetools + from .config import settings logger = logging.getLogger(__name__) @@ -20,6 +20,7 @@ def __init__(self): self.session: Optional[aiohttp.ClientSession] = None async def __aenter__(self): + import aiohttp self.session = aiohttp.ClientSession() return self @@ -62,7 +63,8 @@ async def _get_currency_rates_exchangerate(self, currencies: List[str]) -> List[ return [{"currency": "RUB", "rate": 1.0}] async with self.session.get( - f"{settings.exchangerate_url}?base={base_currency}&symbols={','.join(target_currencies)}" + f"{settings.exchangerate_url}?base={base_currency}" + f"&symbols={','.join(target_currencies)}" ) as response: if response.status == 200: data = await response.json() @@ -88,7 +90,8 @@ async def _get_currency_rates_currencyapi(self, currencies: List[str]) -> List[D target_currencies = [c for c in currencies if c != "RUB"] async with self.session.get( - f"{settings.currency_api_url}?apikey={settings.currency_api_key}&base_currency={base_currency}" + f"{settings.currency_api_url}?apikey={settings.currency_api_key}" + f"&base_currency={base_currency}" ) as response: if response.status == 200: data = await response.json() @@ -136,7 +139,8 @@ async def _get_stock_prices_alphavantage(self, stocks: List[str]) -> List[Dict[s continue async with self.session.get( - f"{settings.alpha_vantage_url}?function=GLOBAL_QUOTE&symbol={stock}&apikey={settings.alpha_vantage_api_key}" + f"{settings.alpha_vantage_url}?function=GLOBAL_QUOTE" + f"&symbol={stock}&apikey={settings.alpha_vantage_api_key}" ) as response: if response.status == 200: data = await response.json() @@ -199,6 +203,8 @@ class SyncAPIClient: @staticmethod def get_currency_rates(currencies: List[str]) -> List[Dict[str, float]]: + """Получение курсов валют""" + async def _fetch(): async with APIClient() as client: return await client.get_currency_rates(currencies) @@ -207,6 +213,8 @@ async def _fetch(): @staticmethod def get_stock_prices(stocks: List[str]) -> List[Dict[str, float]]: + """Получение цен акций""" + async def _fetch(): async with APIClient() as client: return await client.get_stock_prices(stocks) diff --git a/src/config.py b/src/config.py index 7f3cdd4..cb8271e 100644 --- a/src/config.py +++ b/src/config.py @@ -1,7 +1,8 @@ import os -from typing import List, Dict, Any -from pydantic import BaseSettings, validator +from typing import List + from dotenv import load_dotenv +from pydantic import BaseSettings load_dotenv() diff --git a/src/main.py b/src/main.py index 54fcec7..4483084 100644 --- a/src/main.py +++ b/src/main.py @@ -2,11 +2,9 @@ """ Основной модуль приложения для анализа транзакций """ - -import os -import logging import argparse import json +import logging from datetime import datetime from pathlib import Path @@ -79,27 +77,23 @@ def generate_reports(self): """Генерация всех отчетов""" from src.reports import ReportGenerator # Добавлен импорт - reports = {} + reports = {'spending_by_category': ReportGenerator.spending_by_category( + self.transactions_df, 'Супермаркеты' + ), 'spending_by_weekday': ReportGenerator.spending_by_weekday( + self.transactions_df + ), 'spending_by_workday': ReportGenerator.spending_by_workday( + self.transactions_df + ), 'monthly_summary': ReportGenerator.monthly_summary( + self.transactions_df + )} # Отчет по категориям - reports['spending_by_category'] = ReportGenerator.spending_by_category( - self.transactions_df, 'Супермаркеты' - ) # Отчет по дням недели - reports['spending_by_weekday'] = ReportGenerator.spending_by_weekday( - self.transactions_df - ) # Отчет по рабочим/выходным дням - reports['spending_by_workday'] = ReportGenerator.spending_by_workday( - self.transactions_df - ) # Сводный отчет - reports['monthly_summary'] = ReportGenerator.monthly_summary( - self.transactions_df - ) return reports @@ -146,7 +140,7 @@ def main(): print(f"Тип данных: {type(report_data)}") print(report_data) - print(f"\n📊 Отчеты сохранены в папке 'reports/'") + print("\n Отчеты сохранены в папке 'reports/'") elif args.command == 'analyze': # Анализ данных diff --git a/src/reports.py b/src/reports.py index f2a4727..40bc114 100644 --- a/src/reports.py +++ b/src/reports.py @@ -1,11 +1,11 @@ -import pandas as pd import json import logging +import os from datetime import datetime, timedelta -from typing import Optional, Callable, Any, Dict from functools import wraps -import os -from .utils import load_transactions +from typing import Any, Callable, Dict, Optional + +import pandas as pd logger = logging.getLogger(__name__) diff --git a/src/services.py b/src/services.py index 63e0295..b2ae28c 100644 --- a/src/services.py +++ b/src/services.py @@ -1,9 +1,9 @@ -import re import logging +import re from datetime import datetime -from typing import Dict, List, Any, Callable, Optional -from functools import wraps, reduce -import operator +from functools import reduce, wraps +from typing import Any, Callable, Dict, List + import pandas as pd logger = logging.getLogger(__name__) @@ -39,7 +39,6 @@ def analyze_profitable_categories(self, data: pd.DataFrame, year: int, continue category_data = filtered_data[filtered_data['Категория'] == category] - total_spent = category_data['Сумма операции'].sum() # Расчет кешбэка по сложной логике from .utils import calculate_cashback @@ -80,8 +79,8 @@ def investment_bank(month: str, transactions: List[Dict[str, Any]], trans_date = datetime.strptime(transaction['Дата операции'], '%Y-%m-%d') - if (trans_date.year == target_month.year and - trans_date.month == target_month.month): + if (trans_date.year == target_month.year + and trans_date.month == target_month.month): amount = float(transaction['Сумма операции']) if amount > 0: # Только расходы @@ -130,8 +129,8 @@ def search_filter(transaction: Dict[str, Any]) -> bool: category = str(transaction.get('Категория', '')).lower() search_lower = search_string.lower() - return (search_lower in description or - search_lower in category) + return (search_lower in description + or search_lower in category) return list(filter(search_filter, transactions)) @@ -165,8 +164,8 @@ def transfer_filter(transaction: Dict[str, Any]) -> bool: category = transaction.get('Категория', '') description = str(transaction.get('Описание', '')) - return (category == 'Переводы' and - bool(re.search(name_pattern, description))) + return (category == 'Переводы' + and bool(re.search(name_pattern, description))) return list(filter(transfer_filter, transactions)) @@ -230,4 +229,4 @@ def search_by_phone(transactions: List[Dict[str, Any]]) -> List[Dict[str, Any]]: @log_service_call("search_by_person_transfers") def search_by_person_transfers(transactions: List[Dict[str, Any]]) -> List[Dict[str, Any]]: - return TransactionSearcher.search_by_person_transfers(transactions) \ No newline at end of file + return TransactionSearcher.search_by_person_transfers(transactions) diff --git a/src/utils.py b/src/utils.py index 593d344..9af4160 100644 --- a/src/utils.py +++ b/src/utils.py @@ -1,28 +1,27 @@ -import pandas as pd import json import logging from datetime import datetime, timedelta -from typing import Dict, List, Any, Optional, Tuple, Union -from dateutil.parser import parse -from dateutil.relativedelta import relativedelta -import re -from .config import settings +from typing import Any, Dict, List, Tuple + +import pandas as pd + +from src.config import settings logger = logging.getLogger(__name__) -class DataValidator: +class DataValidator : """Валидатор данных транзакций""" @staticmethod - def validate_transaction_data(df: pd.DataFrame) -> Tuple[pd.DataFrame, List[str]]: + def validate_transaction_data(df: pd.DataFrame) -> Tuple[pd.DataFrame, List[str]] : """Проверка и очистка данных транзакций""" errors = [] # Проверка обязательных колонок required_columns = ['Дата операции', 'Сумма операции', 'Статус'] missing_columns = [col for col in required_columns if col not in df.columns] - if missing_columns: + if missing_columns : raise ValueError(f"Отсутствуют обязательные колонки: {missing_columns}") # Копируем данные для очистки @@ -43,67 +42,66 @@ def validate_transaction_data(df: pd.DataFrame) -> Tuple[pd.DataFrame, List[str] # Удаление дубликатов initial_count = len(clean_df) clean_df = clean_df.drop_duplicates() - if len(clean_df) < initial_count: + if len(clean_df) < initial_count : errors.append(f"Удалено {initial_count - len(clean_df)} дубликатов") # Удаление строк с критическими ошибками initial_count = len(clean_df) clean_df = clean_df.dropna(subset=['Дата операции', 'Сумма операции', 'Статус']) - if len(clean_df) < initial_count: + if len(clean_df) < initial_count : errors.append(f"Удалено {initial_count - len(clean_df)} строк с некорректными данными") return clean_df, errors @staticmethod - def _process_dates(df: pd.DataFrame) -> Tuple[pd.DataFrame, List[str]]: + def _process_dates(df: pd.DataFrame) -> Tuple[pd.DataFrame, List[str]] : """Обработка и валидация дат""" errors = [] clean_df = df.copy() date_columns = ['Дата операции', 'Дата платежа'] - for col in date_columns: - if col not in clean_df.columns: + for col in date_columns : + if col not in clean_df.columns : continue original_non_null = clean_df[col].notna().sum() # Пробуем разные форматы дат - for date_format in settings.date_formats: - try: + for date_format in settings.date_formats : + try : clean_df[col] = pd.to_datetime( clean_df[col], format=date_format, errors='coerce' ) # Если удалось преобразовать большинство дат, используем этот формат - if clean_df[col].notna().sum() > original_non_null * 0.8: + if clean_df[col].notna().sum() > original_non_null * 0.8 : break - except: + except Exception: continue # Убираем устаревший параметр infer_datetime_format # Просто используем errors='coerce' для оставшихся проблемных значений - if clean_df[col].isna().any(): + if clean_df[col].isna().any() : clean_df[col] = pd.to_datetime(clean_df[col], errors='coerce') # Проверяем разумность дат (не в будущем и не слишком в прошлом) - if col in clean_df.columns and clean_df[col].notna().any(): + if col in clean_df.columns and clean_df[col].notna().any() : max_date = datetime.now() + timedelta(days=1) # Завтра min_date = datetime(2000, 1, 1) # 2000 год invalid_dates = clean_df[ - (clean_df[col] > max_date) | (clean_df[col] < min_date) - ] + (clean_df[col] > max_date) | (clean_df[col] < min_date)] - if len(invalid_dates) > 0: + if len(invalid_dates) > 0 : errors.append(f"Найдено {len(invalid_dates)} некорректных дат в колонке {col}") clean_df.loc[invalid_dates.index, col] = pd.NaT return clean_df, errors @staticmethod - def _process_numeric_fields(df: pd.DataFrame) -> Tuple[pd.DataFrame, List[str]]: + def _process_numeric_fields(df: pd.DataFrame) -> Tuple[pd.DataFrame, List[str]] : """Обработка числовых полей""" errors = [] clean_df = df.copy() @@ -111,8 +109,8 @@ def _process_numeric_fields(df: pd.DataFrame) -> Tuple[pd.DataFrame, List[str]]: numeric_columns = ['Сумма операции', 'Сумма платежа', 'Кешбэк', 'Бонусы (включая кешбэк)', 'Округление на «Инвесткопилку»', 'Сумма операции с округлением'] - for col in numeric_columns: - if col not in clean_df.columns: + for col in numeric_columns : + if col not in clean_df.columns : continue # Заменяем запятые на точки и преобразуем в числа @@ -122,23 +120,23 @@ def _process_numeric_fields(df: pd.DataFrame) -> Tuple[pd.DataFrame, List[str]]: ) # Проверяем на выбросы (суммы больше 10 млн) - if clean_df[col].notna().any(): + if clean_df[col].notna().any() : outliers = clean_df[clean_df[col].abs() > 10000000] - if len(outliers) > 0: + if len(outliers) > 0 : errors.append(f"Найдено {len(outliers)} выбросов в колонке {col}") return clean_df, errors @staticmethod - def _process_text_fields(df: pd.DataFrame) -> Tuple[pd.DataFrame, List[str]]: + def _process_text_fields(df: pd.DataFrame) -> Tuple[pd.DataFrame, List[str]] : """Обработка текстовых полей""" errors = [] clean_df = df.copy() text_columns = ['Статус', 'Категория', 'Описание', 'Номер карты'] - for col in text_columns: - if col not in clean_df.columns: + for col in text_columns : + if col not in clean_df.columns : continue clean_df[col] = clean_df[col].astype(str).str.strip() @@ -147,75 +145,75 @@ def _process_text_fields(df: pd.DataFrame) -> Tuple[pd.DataFrame, List[str]]: clean_df[col] = clean_df[col].replace('nan', '').replace('None', '') # Проверка на слишком длинные тексты - if col == 'Описание': + if col == 'Описание' : too_long = clean_df[clean_df[col].str.len() > 500] - if len(too_long) > 0: + if len(too_long) > 0 : errors.append(f"Найдено {len(too_long)} очень длинных описаний") clean_df.loc[too_long.index, col] = clean_df.loc[too_long.index, col].str[:500] return clean_df, errors -def load_transactions(file_path: str = settings.data_file_path) -> pd.DataFrame: +def load_transactions(file_path: str = settings.data_file_path) -> pd.DataFrame : """Загрузка и валидация транзакций из Excel файла""" - try: + try : logger.info(f"Загрузка данных из {file_path}") # Чтение файла df = pd.read_excel(file_path) - if df.empty: + if df.empty : raise ValueError("Файл не содержит данных") # Валидация и очистка данных clean_df, errors = DataValidator.validate_transaction_data(df) - if errors: + if errors : logger.warning(f"Обнаружены проблемы при загрузке данных: {errors}") logger.info(f"Успешно загружено {len(clean_df)} транзакций") return clean_df - except Exception as e: + except Exception as e : logger.error(f"Ошибка загрузки транзакций: {e}") raise -def get_date_range(date_str: str, period: str = 'M') -> Tuple[datetime, datetime]: +def get_date_range(date_str: str, period: str = 'M') -> Tuple[datetime, datetime] : """Получение диапазона дат для анализа""" - try: + try : # Парсим дату с учетом времени - if ' ' in date_str: + if ' ' in date_str : date = datetime.strptime(date_str, '%Y-%m-%d %H:%M:%S') - else: + else : date = datetime.strptime(date_str, '%Y-%m-%d') - if period == 'W': # Неделя + if period == 'W' : # Неделя start_date = date - timedelta(days=date.weekday()) start_date = start_date.replace(hour=0, minute=0, second=0, microsecond=0) end_date = start_date + timedelta(days=6) - elif period == 'M': # Месяц + elif period == 'M' : # Месяц start_date = date.replace(day=1, hour=0, minute=0, second=0, microsecond=0) next_month = date.replace(day=28) + timedelta(days=4) end_date = min(next_month.replace(day=1) - timedelta(days=1), date) - elif period == 'Y': # Год + elif period == 'Y' : # Год start_date = date.replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0) end_date = date - elif period == 'ALL': # Все данные + elif period == 'ALL' : # Все данные start_date = datetime(2000, 1, 1) end_date = date - else: + else : raise ValueError(f"Неизвестный период: {period}") return start_date, end_date - except ValueError as e: + except ValueError as e : logger.error(f"Ошибка парсинга даты {date_str}: {e}") raise def filter_transactions_by_date(df: pd.DataFrame, start_date: datetime, - end_date: datetime) -> pd.DataFrame: + end_date: datetime) -> pd.DataFrame : """Фильтрация транзакций по диапазону дат""" mask = (df['Дата операции'] >= start_date) & (df['Дата операции'] <= end_date) filtered_df = df.loc[mask].copy() @@ -224,52 +222,52 @@ def filter_transactions_by_date(df: pd.DataFrame, start_date: datetime, return filtered_df -def get_greeting(time_str: str) -> str: +def get_greeting(time_str: str) -> str : """Получение приветствия в зависимости от времени""" - try: - if ' ' in time_str: + try : + if ' ' in time_str : hour = datetime.strptime(time_str, '%Y-%m-%d %H:%M:%S').hour - else: + else : hour = datetime.strptime(time_str, '%Y-%m-%d').hour - if 5 <= hour < 12: + if 5 <= hour < 12 : return "Доброе утро" - elif 12 <= hour < 17: + elif 12 <= hour < 17 : return "Добрый день" - elif 17 <= hour < 23: + elif 17 <= hour < 23 : return "Добрый вечер" - else: + else : return "Доброй ночи" - except ValueError: + except ValueError : return "Добрый день" # По умолчанию -def load_user_settings() -> Dict[str, Any]: +def load_user_settings() -> Dict[str, Any] : """Загрузка пользовательских настроек""" - try: - with open(settings.user_settings_path, 'r', encoding='utf-8') as f: + try : + with open(settings.user_settings_path, 'r', encoding='utf-8') as f : settings_data = json.load(f) # Валидация настроек required_sections = ['user_currencies', 'user_stocks'] - for section in required_sections: - if section not in settings_data: + for section in required_sections : + if section not in settings_data : raise ValueError(f"Отсутствует обязательный раздел {section} в настройках") return settings_data - except FileNotFoundError: + except FileNotFoundError : logger.error(f"Файл настроек {settings.user_settings_path} не найден") raise - except json.JSONDecodeError as e: + except json.JSONDecodeError as e : logger.error(f"Ошибка парсинга JSON в настройках: {e}") raise -def calculate_cashback(amount: float, category: str, cashback_rules: Dict[str, float]) -> float: +def calculate_cashback(amount: float, category: str, cashback_rules: Dict[str, float]) -> float : """Расчет кешбэка по сложной логике""" - try: + try : # Базовая ставка base_rate = cashback_rules.get('default', 0.01) @@ -278,9 +276,9 @@ def calculate_cashback(amount: float, category: str, cashback_rules: Dict[str, f # Дополнительный бонус для больших покупок (ИСПРАВЛЕНО) bonus_rate = 0.0 - if amount > 10000: + if amount > 10000 : bonus_rate = 0.02 # +2% для покупок > 10,000 - elif amount > 5000: + elif amount > 5000 : bonus_rate = 0.01 # +1% для покупок > 5,000 total_rate = category_rate + bonus_rate @@ -293,6 +291,6 @@ def calculate_cashback(amount: float, category: str, cashback_rules: Dict[str, f # Округление до 2 знаков return round(cashback, 2) - except Exception as e: + except Exception as e : logger.warning(f"Ошибка расчета кешбэка: {e}") return round(amount * 0.01, 2) # Fallback 1% diff --git a/src/views.py b/src/views.py index 716d004..e3a7ce1 100644 --- a/src/views.py +++ b/src/views.py @@ -1,12 +1,17 @@ import logging -from typing import Dict, List, Any, Optional +from typing import Any, Dict, List, Optional import pandas as pd -import pytest from .api_client import SyncAPIClient -from .utils import (load_transactions, get_date_range, filter_transactions_by_date, - get_greeting, load_user_settings, calculate_cashback) +from .utils import ( + calculate_cashback, + filter_transactions_by_date, + get_date_range, + get_greeting, + load_transactions, + load_user_settings, +) logger = logging.getLogger(__name__) @@ -65,8 +70,8 @@ def _get_cards_data(df: pd.DataFrame, cashback_rules: Dict[str, float]) -> List[ for card in card_numbers: card_df = df[df['Номер карты'] == card] # Только успешные операции расходов - expenses_df = card_df[(card_df['Статус'] == 'OK') & - (card_df['Сумма операции'] > 0)] + expenses_df = card_df[(card_df['Статус'] == 'OK') + & (card_df['Сумма операции'] > 0)] if expenses_df.empty: continue @@ -206,37 +211,3 @@ def events_page(date: str, period: str = 'M', except Exception as e: logger.error(f"Ошибка генерации страницы событий: {e}") return {'error': str(e)} - - -# tests/test_views.py - исправим тестовые данные -@pytest.fixture -def sample_transactions(): - return pd.DataFrame({ - 'Дата операции': pd.to_datetime(['2023-12-01', '2023-12-05', '2023-12-10']), - 'Номер карты': ['1234567812345814', '1234567812345814', '1234567812347512'], - 'Статус': ['OK', 'OK', 'OK'], # Все успешные операции - 'Сумма операции': [1000.0, 500.0, 300.0], # Все расходы (положительные) - 'Категория': ['Супермаркеты', 'Фастфуд', 'Транспорт'], - 'Описание': ['Магазин', 'Кафе', 'Такси'], - 'Сумма платежа': [1000.0, 500.0, 300.0] - }) - - -def test_data_processor(sample_transactions, sample_settings): - processor = DataProcessor() - - result = processor.process_main_page_data( - sample_transactions, '2023-12-20 15:30:00', sample_settings - ) - - # Теперь должно быть 2 карты с расходами - assert len(result['cards']) == 2 - assert len(result['top_transactions']) == 3 - - # Проверяем расчет кешбэка (ИСПРАВЛЕНО ожидание) - card_1 = result['cards'][0] - assert 'last_digits' in card_1 - assert 'total_spent' in card_1 - assert 'cashback' in card_1 - # 1% от 1500 (1000 + 500) = 15.0 - assert card_1['cashback'] == 15.0 From bd63c8db4e697e5f1fd6da905854e55cabb82d2a Mon Sep 17 00:00:00 2001 From: ilya_kim Date: Tue, 30 Sep 2025 22:01:49 +0300 Subject: [PATCH 07/12] appdate to flake8 --- tests/conftest.py | 9 +++++---- tests/test_api_client.py | 3 +-- tests/test_services.py | 1 + tests/test_utils.py | 15 ++++++--------- tests/test_views.py | 9 +++++---- 5 files changed, 18 insertions(+), 19 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 6f8e834..c1a7056 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,6 @@ -import pytest import pandas as pd -from datetime import datetime, timedelta -import os +import pytest + @pytest.fixture def sample_transactions(): @@ -17,6 +16,7 @@ def sample_transactions(): 'Сумма платежа': [1000.0, 500.0] * 50 }) + @pytest.fixture def sample_settings(): """Фикстура с настройками""" @@ -30,9 +30,10 @@ def sample_settings(): } } + @pytest.fixture(autouse=True) def setup_teardown(): """Фикстура для настройки перед каждым тестом""" # Настройка перед тестом yield - # Очистка после теста \ No newline at end of file + # Очистка после теста diff --git a/tests/test_api_client.py b/tests/test_api_client.py index 18e9f3a..1793af3 100644 --- a/tests/test_api_client.py +++ b/tests/test_api_client.py @@ -1,7 +1,6 @@ +from unittest.mock import AsyncMock, patch import pytest -from unittest.mock import AsyncMock, patch -import asyncio class TestAPIClient: diff --git a/tests/test_services.py b/tests/test_services.py index 46b76e6..8db7a35 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -1,4 +1,5 @@ import pytest + from src.services import investment_bank, simple_search diff --git a/tests/test_utils.py b/tests/test_utils.py index f2ac33d..383964b 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,14 +1,12 @@ # tests/test_utils.py -import pytest -from src.utils import ( - get_greeting, get_date_range, calculate_cashback, - DataValidator, load_user_settings -) -from datetime import datetime -import pandas as pd import json -import tempfile import os +import tempfile +from datetime import datetime + +import pandas as pd + +from src.utils import DataValidator, calculate_cashback, get_date_range, get_greeting, load_user_settings class TestUtils: @@ -84,4 +82,3 @@ def test_load_user_settings(self): finally: src.utils.settings.user_settings_path = original_path os.unlink(temp_path) - diff --git a/tests/test_views.py b/tests/test_views.py index 3193ef3..2fadf0d 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -1,9 +1,10 @@ # tests/test_views.py -import pytest -from src.views import main_page, events_page, DataProcessor -from unittest.mock import patch, MagicMock +from unittest.mock import patch + import pandas as pd -from datetime import datetime +import pytest + +from src.views import DataProcessor, events_page, main_page class TestViews: From dbd470477dbfbe8aa286e75b77685e0bb153da73 Mon Sep 17 00:00:00 2001 From: ilya_kim Date: Tue, 30 Sep 2025 22:03:35 +0300 Subject: [PATCH 08/12] appdate to flake8 --- data/sample_data.py | 6 +- run_app.py | 9 +- test_app.py | 204 ++++++++++---------------------------------- 3 files changed, 51 insertions(+), 168 deletions(-) diff --git a/data/sample_data.py b/data/sample_data.py index ba9459c..5eee45a 100644 --- a/data/sample_data.py +++ b/data/sample_data.py @@ -2,10 +2,10 @@ Скрипт для генерации тестовых данных """ -import pandas as pd -import numpy as np -from datetime import datetime, timedelta import random +from datetime import datetime, timedelta + +import pandas as pd def generate_sample_data(num_records: int = 1000) -> pd.DataFrame: diff --git a/run_app.py b/run_app.py index 0323643..8de1f9b 100644 --- a/run_app.py +++ b/run_app.py @@ -3,7 +3,6 @@ Упрощенный запуск приложения """ -import os import sys from pathlib import Path @@ -19,14 +18,14 @@ def main(): """Основная функция""" try: - from src.views import main_page, events_page from src.utils import load_transactions, load_user_settings + from src.views import events_page, main_page print("🚀 Запуск Transaction Analyzer...") # Загрузка данных - df = load_transactions() - settings = load_user_settings() + load_transactions() + load_user_settings() # Генерация данных для веб-страниц current_time = "2023-12-20 15:30:00" @@ -51,4 +50,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/test_app.py b/test_app.py index e40b542..3410c30 100644 --- a/test_app.py +++ b/test_app.py @@ -1,181 +1,65 @@ #!/usr/bin/env python3 """ -Основной модуль приложения для анализа транзакций +Простой скрипт для тестирования приложения """ -import os -import logging -import argparse -import json -from datetime import datetime +import sys from pathlib import Path +# Добавляем src в путь Python +sys.path.insert(0, str(Path(__file__).parent)) + # Создаем необходимые директории Path('logs').mkdir(exist_ok=True) Path('reports').mkdir(exist_ok=True) Path('data').mkdir(exist_ok=True) -# Настройка логирования -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - handlers=[ - logging.FileHandler('logs/transaction_analyzer.log'), - logging.StreamHandler() - ] -) - -logger = logging.getLogger(__name__) - -class TransactionAnalyzer: - """Основной класс приложения""" - - def __init__(self, data_file: str = None): - from src.config import settings - self.data_file = data_file or settings.data_file_path - self.transactions_df = None - self.settings = None +def test_basic_functionality(): + """Тест базовой функциональности""" + try: + print("🧪 Тестирование Transaction Analyzer...") - def load_data(self): - """Загрузка данных""" + # Импортируем модули + from src.reports import ReportGenerator from src.utils import load_transactions, load_user_settings - logger.info("Загрузка данных...") - self.transactions_df = load_transactions(self.data_file) - self.settings = load_user_settings() - logger.info("Данные успешно загружены") - - def generate_main_page(self, date_time: str) -> dict: - """Генерация данных для главной страницы""" - from src.views import main_page - return main_page(date_time, self.data_file) - - def generate_events_page(self, date: str, period: str = 'M') -> dict: - """Генерация данных для страницы событий""" - from src.views import events_page - return events_page(date, period, self.data_file) - - def analyze_cashback_categories(self, year: int, month: int) -> dict: - """Анализ выгодных категорий кешбэка""" - from src.services import profitable_cashback_categories - cashback_rules = self.settings.get('cashback_rules', {'default': 0.01}) - return profitable_cashback_categories( - self.transactions_df, year, month, cashback_rules - ) - - def calculate_investment(self, month: str, limit: int) -> float: - """Расчет инвесткопилки""" - from src.services import investment_bank - transactions_list = self.transactions_df.to_dict('records') - return investment_bank(month, transactions_list, limit) - - def search_transactions(self, search_string: str) -> list: - """Поиск транзакций""" + from src.views import events_page, main_page + + # 1. Загрузка данных + print("1. Загрузка данных...") + df = load_transactions() + settings = load_user_settings() + print(f" ✅ Транзакций: {len(df)}") + print(f" ✅ Настроек: {len(settings)}") + + # 2. Тест веб-страниц + print("2. Тест веб-страниц...") + main_data = main_page("2023-12-20 15:30:00") + events_data = events_page("2023-12-20", "M") + print(f" ✅ Главная страница: {main_data['greeting']}") + print(f" ✅ Страница событий: расходы {events_data['expenses']['total_amount']}") + + # 3. Тест отчетов + print("3. Тест отчетов...") + report = ReportGenerator.spending_by_category(df, "Супермаркеты") + print(f" ✅ Отчет по категории: {len(report)} записей") + + # 4. Тест сервисов + print("4. Тест сервисов...") from src.services import simple_search - transactions_list = self.transactions_df.to_dict('records') - return simple_search(transactions_list, search_string) - - def generate_reports(self): - """Генерация всех отчетов""" - from src.reports import ReportGenerator # Добавлен импорт - - reports = {} - - # Отчет по категориям - reports['spending_by_category'] = ReportGenerator.spending_by_category( - self.transactions_df, 'Супермаркеты' - ) - - # Отчет по дням недели - reports['spending_by_weekday'] = ReportGenerator.spending_by_weekday( - self.transactions_df - ) - - # Отчет по рабочим/выходным дням - reports['spending_by_workday'] = ReportGenerator.spending_by_workday( - self.transactions_df - ) - - # Сводный отчет - reports['monthly_summary'] = ReportGenerator.monthly_summary( - self.transactions_df - ) - - return reports - - -def main(): - """Основная функция приложения""" - parser = argparse.ArgumentParser(description='Анализатор банковских транзакций') - parser.add_argument('--data-file', help='Путь к файлу с транзакциями') - parser.add_argument('--command', choices=['web', 'report', 'analyze', 'test'], - default='web', help='Режим работы') - - args = parser.parse_args() - - try: - # Инициализация анализатора - analyzer = TransactionAnalyzer(args.data_file) - analyzer.load_data() - - if args.command == 'web': - # Пример генерации данных для веб-страниц - current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - - main_data = analyzer.generate_main_page(current_time) - events_data = analyzer.generate_events_page(current_time.split()[0]) - - print("=== ДАННЫЕ ДЛЯ ГЛАВНОЙ СТРАНИЦЫ ===") - print(json.dumps(main_data, ensure_ascii=False, indent=2)) - - print("\n=== ДАННЫЕ ДЛЯ СТРАНИЦЫ СОБЫТИЙ ===") - print(json.dumps(events_data, ensure_ascii=False, indent=2)) - - elif args.command == 'report': - # Генерация отчетов - reports = analyzer.generate_reports() - print("=== ОТЧЕТЫ СГЕНЕРИРОВАНЫ ===") - - for report_name, report_data in reports.items(): - print(f"\n--- {report_name} ---") - if isinstance(report_data, dict): - print(json.dumps(report_data, ensure_ascii=False, indent=2)) - elif hasattr(report_data, 'head'): # DataFrame - print(report_data.head()) - else: - print(f"Тип данных: {type(report_data)}") - print(report_data) - - print(f"\n📊 Отчеты сохранены в папке 'reports/'") - - elif args.command == 'analyze': - # Анализ данных - current_year = datetime.now().year - current_month = datetime.now().month - - cashback_analysis = analyzer.analyze_cashback_categories(current_year, current_month) - investment = analyzer.calculate_investment( - f"{current_year}-{current_month:02d}", 50 - ) - - print("=== АНАЛИЗ ДАННЫХ ===") - print(f"Выгодные категории кешбэка: {cashback_analysis}") - print(f"Инвесткопилка за месяц: {investment} руб.") - - elif args.command == 'test': - # Простой тест функциональности - print("=== ТЕСТ ФУНКЦИОНАЛЬНОСТИ ===") - print(f"Загружено транзакций: {len(analyzer.transactions_df)}") - print(f"Настройки: {list(analyzer.settings.keys())}") + transactions_list = df.head(10).to_dict('records') + results = simple_search(transactions_list, "магазин") + print(f" ✅ Поиск: {len(results)} результатов") - # Тест поиска - search_results = analyzer.search_transactions("магазин") - print(f"Результатов поиска 'магазин': {len(search_results)}") + print("\n🎉 ВСЕ ТЕСТЫ ПРОЙДЕНЫ УСПЕШНО!") + return True except Exception as e: - logger.error(f"Ошибка приложения: {e}") - raise + print(f"\n❌ ОШИБКА: {e}") + import traceback + traceback.print_exc() + return False if __name__ == "__main__": - main() \ No newline at end of file + test_basic_functionality() From 7f25e0ea78496be7ce7d1f6b01ee0b129a164b91 Mon Sep 17 00:00:00 2001 From: ilya_kim Date: Tue, 30 Sep 2025 22:06:38 +0300 Subject: [PATCH 09/12] appdate to flake8 --- poetry.lock | 195 +++++++++++++++++++++++++++++++++------------ pyproject.toml | 51 ++++++++++++ user_settings.json | 11 +++ 3 files changed, 204 insertions(+), 53 deletions(-) create mode 100644 user_settings.json diff --git a/poetry.lock b/poetry.lock index a48cd34..bc5a275 100644 --- a/poetry.lock +++ b/poetry.lock @@ -176,7 +176,7 @@ version = "25.9.0" description = "The uncompromising code formatter." optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "black-25.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ce41ed2614b706fd55fd0b4a6909d06b5bab344ffbfadc6ef34ae50adba3d4f7"}, {file = "black-25.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2ab0ce111ef026790e9b13bd216fa7bc48edd934ffc4cbf78808b235793cbc92"}, @@ -337,7 +337,7 @@ version = "8.1.8" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" -groups = ["main"] +groups = ["main", "dev"] markers = "python_version < \"3.11\"" files = [ {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, @@ -353,7 +353,7 @@ version = "8.3.0" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["main", "dev"] markers = "python_version >= \"3.11\"" files = [ {file = "click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc"}, @@ -370,11 +370,11 @@ description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" groups = ["main", "dev"] +markers = "sys_platform == \"win32\" or platform_system == \"Windows\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -markers = {main = "sys_platform == \"win32\" or platform_system == \"Windows\"", dev = "sys_platform == \"win32\""} [[package]] name = "coverage" @@ -668,6 +668,22 @@ files = [ {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, ] +[[package]] +name = "isort" +version = "6.0.1" +description = "A Python utility / library to sort Python imports." +optional = false +python-versions = ">=3.9.0" +groups = ["dev"] +files = [ + {file = "isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615"}, + {file = "isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450"}, +] + +[package.extras] +colors = ["colorama"] +plugins = ["setuptools"] + [[package]] name = "multidict" version = "6.6.4" @@ -791,13 +807,74 @@ files = [ [package.dependencies] typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""} +[[package]] +name = "mypy" +version = "1.18.2" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "mypy-1.18.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c1eab0cf6294dafe397c261a75f96dc2c31bffe3b944faa24db5def4e2b0f77c"}, + {file = "mypy-1.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a780ca61fc239e4865968ebc5240bb3bf610ef59ac398de9a7421b54e4a207e"}, + {file = "mypy-1.18.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448acd386266989ef11662ce3c8011fd2a7b632e0ec7d61a98edd8e27472225b"}, + {file = "mypy-1.18.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9e171c465ad3901dc652643ee4bffa8e9fef4d7d0eece23b428908c77a76a66"}, + {file = "mypy-1.18.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:592ec214750bc00741af1f80cbf96b5013d81486b7bb24cb052382c19e40b428"}, + {file = "mypy-1.18.2-cp310-cp310-win_amd64.whl", hash = "sha256:7fb95f97199ea11769ebe3638c29b550b5221e997c63b14ef93d2e971606ebed"}, + {file = "mypy-1.18.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f"}, + {file = "mypy-1.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341"}, + {file = "mypy-1.18.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d"}, + {file = "mypy-1.18.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86"}, + {file = "mypy-1.18.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37"}, + {file = "mypy-1.18.2-cp311-cp311-win_amd64.whl", hash = "sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8"}, + {file = "mypy-1.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34"}, + {file = "mypy-1.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764"}, + {file = "mypy-1.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893"}, + {file = "mypy-1.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914"}, + {file = "mypy-1.18.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8"}, + {file = "mypy-1.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074"}, + {file = "mypy-1.18.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc"}, + {file = "mypy-1.18.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e"}, + {file = "mypy-1.18.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986"}, + {file = "mypy-1.18.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d"}, + {file = "mypy-1.18.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba"}, + {file = "mypy-1.18.2-cp313-cp313-win_amd64.whl", hash = "sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544"}, + {file = "mypy-1.18.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce"}, + {file = "mypy-1.18.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d"}, + {file = "mypy-1.18.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c"}, + {file = "mypy-1.18.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb"}, + {file = "mypy-1.18.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075"}, + {file = "mypy-1.18.2-cp314-cp314-win_amd64.whl", hash = "sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf"}, + {file = "mypy-1.18.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:25a9c8fb67b00599f839cf472713f54249a62efd53a54b565eb61956a7e3296b"}, + {file = "mypy-1.18.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c2b9c7e284ee20e7598d6f42e13ca40b4928e6957ed6813d1ab6348aa3f47133"}, + {file = "mypy-1.18.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6985ed057513e344e43a26cc1cd815c7a94602fb6a3130a34798625bc2f07b6"}, + {file = "mypy-1.18.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22f27105f1525ec024b5c630c0b9f36d5c1cc4d447d61fe51ff4bd60633f47ac"}, + {file = "mypy-1.18.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:030c52d0ea8144e721e49b1f68391e39553d7451f0c3f8a7565b59e19fcb608b"}, + {file = "mypy-1.18.2-cp39-cp39-win_amd64.whl", hash = "sha256:aa5e07ac1a60a253445797e42b8b2963c9675563a94f11291ab40718b016a7a0"}, + {file = "mypy-1.18.2-py3-none-any.whl", hash = "sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e"}, + {file = "mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b"}, +] + +[package.dependencies] +mypy_extensions = ">=1.0.0" +pathspec = ">=0.9.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing_extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + [[package]] name = "mypy-extensions" version = "1.1.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, @@ -973,54 +1050,67 @@ files = [ [[package]] name = "pandas" -version = "2.3.2" +version = "2.3.3" description = "Powerful data structures for data analysis, time series, and statistics" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pandas-2.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52bc29a946304c360561974c6542d1dd628ddafa69134a7131fdfd6a5d7a1a35"}, - {file = "pandas-2.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:220cc5c35ffaa764dd5bb17cf42df283b5cb7fdf49e10a7b053a06c9cb48ee2b"}, - {file = "pandas-2.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42c05e15111221384019897df20c6fe893b2f697d03c811ee67ec9e0bb5a3424"}, - {file = "pandas-2.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc03acc273c5515ab69f898df99d9d4f12c4d70dbfc24c3acc6203751d0804cf"}, - {file = "pandas-2.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d25c20a03e8870f6339bcf67281b946bd20b86f1a544ebbebb87e66a8d642cba"}, - {file = "pandas-2.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21bb612d148bb5860b7eb2c10faacf1a810799245afd342cf297d7551513fbb6"}, - {file = "pandas-2.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:b62d586eb25cb8cb70a5746a378fc3194cb7f11ea77170d59f889f5dfe3cec7a"}, - {file = "pandas-2.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1333e9c299adcbb68ee89a9bb568fc3f20f9cbb419f1dd5225071e6cddb2a743"}, - {file = "pandas-2.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:76972bcbd7de8e91ad5f0ca884a9f2c477a2125354af624e022c49e5bd0dfff4"}, - {file = "pandas-2.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b98bdd7c456a05eef7cd21fd6b29e3ca243591fe531c62be94a2cc987efb5ac2"}, - {file = "pandas-2.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d81573b3f7db40d020983f78721e9bfc425f411e616ef019a10ebf597aedb2e"}, - {file = "pandas-2.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e190b738675a73b581736cc8ec71ae113d6c3768d0bd18bffa5b9a0927b0b6ea"}, - {file = "pandas-2.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c253828cb08f47488d60f43c5fc95114c771bbfff085da54bfc79cb4f9e3a372"}, - {file = "pandas-2.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:9467697b8083f9667b212633ad6aa4ab32436dcbaf4cd57325debb0ddef2012f"}, - {file = "pandas-2.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fbb977f802156e7a3f829e9d1d5398f6192375a3e2d1a9ee0803e35fe70a2b9"}, - {file = "pandas-2.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b9b52693123dd234b7c985c68b709b0b009f4521000d0525f2b95c22f15944b"}, - {file = "pandas-2.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bd281310d4f412733f319a5bc552f86d62cddc5f51d2e392c8787335c994175"}, - {file = "pandas-2.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96d31a6b4354e3b9b8a2c848af75d31da390657e3ac6f30c05c82068b9ed79b9"}, - {file = "pandas-2.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:df4df0b9d02bb873a106971bb85d448378ef14b86ba96f035f50bbd3688456b4"}, - {file = "pandas-2.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:213a5adf93d020b74327cb2c1b842884dbdd37f895f42dcc2f09d451d949f811"}, - {file = "pandas-2.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c13b81a9347eb8c7548f53fd9a4f08d4dfe996836543f805c987bafa03317ae"}, - {file = "pandas-2.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0c6ecbac99a354a051ef21c5307601093cb9e0f4b1855984a084bfec9302699e"}, - {file = "pandas-2.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c6f048aa0fd080d6a06cc7e7537c09b53be6642d330ac6f54a600c3ace857ee9"}, - {file = "pandas-2.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0064187b80a5be6f2f9c9d6bdde29372468751dfa89f4211a3c5871854cfbf7a"}, - {file = "pandas-2.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ac8c320bded4718b298281339c1a50fb00a6ba78cb2a63521c39bec95b0209b"}, - {file = "pandas-2.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:114c2fe4f4328cf98ce5716d1532f3ab79c5919f95a9cfee81d9140064a2e4d6"}, - {file = "pandas-2.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:48fa91c4dfb3b2b9bfdb5c24cd3567575f4e13f9636810462ffed8925352be5a"}, - {file = "pandas-2.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:12d039facec710f7ba305786837d0225a3444af7bbd9c15c32ca2d40d157ed8b"}, - {file = "pandas-2.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c624b615ce97864eb588779ed4046186f967374185c047070545253a52ab2d57"}, - {file = "pandas-2.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0cee69d583b9b128823d9514171cabb6861e09409af805b54459bd0c821a35c2"}, - {file = "pandas-2.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2319656ed81124982900b4c37f0e0c58c015af9a7bbc62342ba5ad07ace82ba9"}, - {file = "pandas-2.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b37205ad6f00d52f16b6d09f406434ba928c1a1966e2771006a9033c736d30d2"}, - {file = "pandas-2.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:837248b4fc3a9b83b9c6214699a13f069dc13510a6a6d7f9ba33145d2841a012"}, - {file = "pandas-2.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d2c3554bd31b731cd6490d94a28f3abb8dd770634a9e06eb6d2911b9827db370"}, - {file = "pandas-2.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:88080a0ff8a55eac9c84e3ff3c7665b3b5476c6fbc484775ca1910ce1c3e0b87"}, - {file = "pandas-2.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d4a558c7620340a0931828d8065688b3cc5b4c8eb674bcaf33d18ff4a6870b4a"}, - {file = "pandas-2.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45178cf09d1858a1509dc73ec261bf5b25a625a389b65be2e47b559905f0ab6a"}, - {file = "pandas-2.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77cefe00e1b210f9c76c697fedd8fdb8d3dd86563e9c8adc9fa72b90f5e9e4c2"}, - {file = "pandas-2.3.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:13bd629c653856f00c53dc495191baa59bcafbbf54860a46ecc50d3a88421a96"}, - {file = "pandas-2.3.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:36d627906fd44b5fd63c943264e11e96e923f8de77d6016dc2f667b9ad193438"}, - {file = "pandas-2.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:a9d7ec92d71a420185dec44909c32e9a362248c4ae2238234b76d5be37f208cc"}, - {file = "pandas-2.3.2.tar.gz", hash = "sha256:ab7b58f8f82706890924ccdfb5f48002b83d2b5a3845976a9fb705d36c34dcdb"}, + {file = "pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c"}, + {file = "pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a"}, + {file = "pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1"}, + {file = "pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838"}, + {file = "pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250"}, + {file = "pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4"}, + {file = "pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826"}, + {file = "pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523"}, + {file = "pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45"}, + {file = "pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66"}, + {file = "pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b"}, + {file = "pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791"}, + {file = "pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151"}, + {file = "pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c"}, + {file = "pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53"}, + {file = "pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35"}, + {file = "pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908"}, + {file = "pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89"}, + {file = "pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98"}, + {file = "pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084"}, + {file = "pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b"}, + {file = "pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713"}, + {file = "pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8"}, + {file = "pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d"}, + {file = "pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac"}, + {file = "pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c"}, + {file = "pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493"}, + {file = "pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee"}, + {file = "pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5"}, + {file = "pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21"}, + {file = "pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78"}, + {file = "pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110"}, + {file = "pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86"}, + {file = "pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc"}, + {file = "pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0"}, + {file = "pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593"}, + {file = "pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c"}, + {file = "pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b"}, + {file = "pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6"}, + {file = "pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3"}, + {file = "pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5"}, + {file = "pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec"}, + {file = "pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7"}, + {file = "pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450"}, + {file = "pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5"}, + {file = "pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788"}, + {file = "pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87"}, + {file = "pandas-2.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c503ba5216814e295f40711470446bc3fd00f0faea8a086cbc688808e26f92a2"}, + {file = "pandas-2.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a637c5cdfa04b6d6e2ecedcb81fc52ffb0fd78ce2ebccc9ea964df9f658de8c8"}, + {file = "pandas-2.3.3-cp39-cp39-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:854d00d556406bffe66a4c0802f334c9ad5a96b4f1f868adf036a21b11ef13ff"}, + {file = "pandas-2.3.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf1f8a81d04ca90e32a0aceb819d34dbd378a98bf923b6398b9a3ec0bf44de29"}, + {file = "pandas-2.3.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:23ebd657a4d38268c7dfbdf089fbc31ea709d82e4923c5ffd4fbd5747133ce73"}, + {file = "pandas-2.3.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5554c929ccc317d41a5e3d1234f3be588248e61f08a74dd17c9eabb535777dc9"}, + {file = "pandas-2.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:d3e28b3e83862ccf4d85ff19cf8c20b2ae7e503881711ff2d534dc8f761131aa"}, + {file = "pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b"}, ] [package.dependencies] @@ -1064,7 +1154,7 @@ version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, @@ -1076,7 +1166,7 @@ version = "4.4.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85"}, {file = "platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf"}, @@ -1393,7 +1483,7 @@ version = "0.1.10" description = "A Fast, spec compliant Python 3.12+ tokenizer that runs on older Pythons." optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "pytokens-0.1.10-py3-none-any.whl", hash = "sha256:db7b72284e480e69fb085d9f251f66b3d2df8b7166059261258ff35f50fb711b"}, {file = "pytokens-0.1.10.tar.gz", hash = "sha256:c9a4bfa0be1d26aebce03e6884ba454e842f186a59ea43a6d3b25af58223c044"}, @@ -1502,7 +1592,6 @@ files = [ {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, ] -markers = {dev = "python_version < \"3.11\""} [[package]] name = "tzdata" @@ -1656,4 +1745,4 @@ propcache = ">=0.2.1" [metadata] lock-version = "2.1" python-versions = "^3.9" -content-hash = "f7a7687a4b766dfe6b494236768695d541b587629fc4f7be3b74fa18534258b2" +content-hash = "f4ffc54e3bc3ec1c6356a31f19e3051f6c80267f33996611da2fdc875190131a" diff --git a/pyproject.toml b/pyproject.toml index b08b769..5f247f6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,4 +22,55 @@ black = ">=23.12.1" pytest = "^7.4.0" pytest-mock = "^3.11.0" pytest-cov = "^4.1.0" +black = "^25.9.0" +isort = "^6.0.1" +mypy = "^1.18.2" +[tool.black] +line-length = 120 +target-version = ['py39'] +include = '\.pyi?$' +extend-exclude = ''' +/( + # directories + | \.eggs + | \.git + | \.venv + | build + | dist + # files + | poetry\.lock +)/ +''' + +[tool.isort] +profile = "black" +multi_line_output = 3 +line_length = 120 +known_first_party = ["your_package_name"] # Замените на имя вашего пакета +skip_glob = ["**/migrations/*"] # Опционально: исключить миграции + +[tool.mypy] +python_version = "3.9" +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = true +check_untyped_defs = true +no_implicit_optional = true +strict_equality = true +show_error_codes = true +enable_error_code = ["ignore-without-code"] + +[[tool.mypy.overrides]] +module = [ + "django.*", # Если используете Django + "celery.*", # Если используете Celery +] +ignore_missing_imports = true + +[tool.pytest.ini_options] +asyncio_mode = "auto" +testpaths = ["tests"] +python_files = ["test_*.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] diff --git a/user_settings.json b/user_settings.json new file mode 100644 index 0000000..9a7f774 --- /dev/null +++ b/user_settings.json @@ -0,0 +1,11 @@ +{ + "user_currencies": ["USD", "EUR", "GBP", "CNY"], + "user_stocks": ["AAPL", "AMZN", "GOOGL", "MSFT", "TSLA", "META", "NVDA"], + "cashback_rules": { + "Супермаркеты": 0.05, + "Фастфуд": 0.03, + "Транспорт": 0.02, + "Развлечения": 0.02, + "default": 0.01 + } +} \ No newline at end of file From 4194daa48a0cf3d26a5547ac012adfb06deddeb0 Mon Sep 17 00:00:00 2001 From: ilya_kim Date: Tue, 30 Sep 2025 22:07:43 +0300 Subject: [PATCH 10/12] appdate --- data/operations.xlsx | Bin 0 -> 83065 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 data/operations.xlsx diff --git a/data/operations.xlsx b/data/operations.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..b65d76af34235c70b566f99d6cf360922663914f GIT binary patch literal 83065 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 IG Date: Tue, 30 Sep 2025 23:20:30 +0300 Subject: [PATCH 11/12] appdate tests and new tests --- .../monthly_summary_20231220_000000.json | 3 + .../monthly_summary_20250930_221851.json | 3 + .../monthly_summary_20250930_222642.json | 3 + .../monthly_summary_20250930_222950.json | 3 + .../monthly_summary_20250930_222959.json | 3 + .../monthly_summary_20250930_223102.json | 3 + .../monthly_summary_20250930_224838.json | 3 + .../monthly_summary_20250930_225857.json | 3 + .../monthly_summary_20250930_225902.json | 3 + .../monthly_summary_20250930_230017.json | 3 + .../monthly_summary_20250930_230123.json | 3 + .../monthly_summary_20250930_230203.json | 3 + .../monthly_summary_20250930_230204.json | 3 + .../monthly_summary_20250930_230205.json | 3 + .../spending_by_category_20250930_221851.json | 1 + .../spending_by_category_20250930_222642.json | 1 + .../spending_by_category_20250930_222949.json | 1 + .../spending_by_category_20250930_222959.json | 1 + .../spending_by_category_20250930_223101.json | 1 + .../spending_by_category_20250930_224838.json | 1 + .../spending_by_category_20250930_225857.json | 14 + .../spending_by_weekday_20250930_221851.json | 1 + .../spending_by_weekday_20250930_222642.json | 1 + .../spending_by_weekday_20250930_222949.json | 1 + .../spending_by_weekday_20250930_222959.json | 1 + .../spending_by_weekday_20250930_223101.json | 1 + .../spending_by_weekday_20250930_224838.json | 1 + .../spending_by_weekday_20250930_225857.json | 30 +++ .../spending_by_workday_20250930_221851.json | 1 + .../spending_by_workday_20250930_222642.json | 1 + .../spending_by_workday_20250930_222950.json | 1 + .../spending_by_workday_20250930_222959.json | 1 + .../spending_by_workday_20250930_223102.json | 1 + .../spending_by_workday_20250930_224838.json | 1 + .../spending_by_workday_20250930_224839.json | 1 + .../spending_by_workday_20250930_225857.json | 10 + .../test_function_20231220_153000.json | 10 + tests/test_main.py | 199 ++++++++++++++ tests/test_reports.py | 249 ++++++++++++++++++ tests/test_services.py | 244 +++++++++++++++++ 40 files changed, 817 insertions(+) create mode 100644 tests/reports/monthly_summary_20231220_000000.json create mode 100644 tests/reports/monthly_summary_20250930_221851.json create mode 100644 tests/reports/monthly_summary_20250930_222642.json create mode 100644 tests/reports/monthly_summary_20250930_222950.json create mode 100644 tests/reports/monthly_summary_20250930_222959.json create mode 100644 tests/reports/monthly_summary_20250930_223102.json create mode 100644 tests/reports/monthly_summary_20250930_224838.json create mode 100644 tests/reports/monthly_summary_20250930_225857.json create mode 100644 tests/reports/monthly_summary_20250930_225902.json create mode 100644 tests/reports/monthly_summary_20250930_230017.json create mode 100644 tests/reports/monthly_summary_20250930_230123.json create mode 100644 tests/reports/monthly_summary_20250930_230203.json create mode 100644 tests/reports/monthly_summary_20250930_230204.json create mode 100644 tests/reports/monthly_summary_20250930_230205.json create mode 100644 tests/reports/spending_by_category_20250930_221851.json create mode 100644 tests/reports/spending_by_category_20250930_222642.json create mode 100644 tests/reports/spending_by_category_20250930_222949.json create mode 100644 tests/reports/spending_by_category_20250930_222959.json create mode 100644 tests/reports/spending_by_category_20250930_223101.json create mode 100644 tests/reports/spending_by_category_20250930_224838.json create mode 100644 tests/reports/spending_by_category_20250930_225857.json create mode 100644 tests/reports/spending_by_weekday_20250930_221851.json create mode 100644 tests/reports/spending_by_weekday_20250930_222642.json create mode 100644 tests/reports/spending_by_weekday_20250930_222949.json create mode 100644 tests/reports/spending_by_weekday_20250930_222959.json create mode 100644 tests/reports/spending_by_weekday_20250930_223101.json create mode 100644 tests/reports/spending_by_weekday_20250930_224838.json create mode 100644 tests/reports/spending_by_weekday_20250930_225857.json create mode 100644 tests/reports/spending_by_workday_20250930_221851.json create mode 100644 tests/reports/spending_by_workday_20250930_222642.json create mode 100644 tests/reports/spending_by_workday_20250930_222950.json create mode 100644 tests/reports/spending_by_workday_20250930_222959.json create mode 100644 tests/reports/spending_by_workday_20250930_223102.json create mode 100644 tests/reports/spending_by_workday_20250930_224838.json create mode 100644 tests/reports/spending_by_workday_20250930_224839.json create mode 100644 tests/reports/spending_by_workday_20250930_225857.json create mode 100644 tests/reports/test_function_20231220_153000.json create mode 100644 tests/test_main.py create mode 100644 tests/test_reports.py diff --git a/tests/reports/monthly_summary_20231220_000000.json b/tests/reports/monthly_summary_20231220_000000.json new file mode 100644 index 0000000..c62910a --- /dev/null +++ b/tests/reports/monthly_summary_20231220_000000.json @@ -0,0 +1,3 @@ +{ + "error": "'numpy.float64' object has no attribute 'abs'" +} \ No newline at end of file diff --git a/tests/reports/monthly_summary_20250930_221851.json b/tests/reports/monthly_summary_20250930_221851.json new file mode 100644 index 0000000..b19b0ab --- /dev/null +++ b/tests/reports/monthly_summary_20250930_221851.json @@ -0,0 +1,3 @@ +{ + "error": "Нет данных за указанный период" +} \ No newline at end of file diff --git a/tests/reports/monthly_summary_20250930_222642.json b/tests/reports/monthly_summary_20250930_222642.json new file mode 100644 index 0000000..b19b0ab --- /dev/null +++ b/tests/reports/monthly_summary_20250930_222642.json @@ -0,0 +1,3 @@ +{ + "error": "Нет данных за указанный период" +} \ No newline at end of file diff --git a/tests/reports/monthly_summary_20250930_222950.json b/tests/reports/monthly_summary_20250930_222950.json new file mode 100644 index 0000000..b19b0ab --- /dev/null +++ b/tests/reports/monthly_summary_20250930_222950.json @@ -0,0 +1,3 @@ +{ + "error": "Нет данных за указанный период" +} \ No newline at end of file diff --git a/tests/reports/monthly_summary_20250930_222959.json b/tests/reports/monthly_summary_20250930_222959.json new file mode 100644 index 0000000..b19b0ab --- /dev/null +++ b/tests/reports/monthly_summary_20250930_222959.json @@ -0,0 +1,3 @@ +{ + "error": "Нет данных за указанный период" +} \ No newline at end of file diff --git a/tests/reports/monthly_summary_20250930_223102.json b/tests/reports/monthly_summary_20250930_223102.json new file mode 100644 index 0000000..b19b0ab --- /dev/null +++ b/tests/reports/monthly_summary_20250930_223102.json @@ -0,0 +1,3 @@ +{ + "error": "Нет данных за указанный период" +} \ No newline at end of file diff --git a/tests/reports/monthly_summary_20250930_224838.json b/tests/reports/monthly_summary_20250930_224838.json new file mode 100644 index 0000000..b19b0ab --- /dev/null +++ b/tests/reports/monthly_summary_20250930_224838.json @@ -0,0 +1,3 @@ +{ + "error": "Нет данных за указанный период" +} \ No newline at end of file diff --git a/tests/reports/monthly_summary_20250930_225857.json b/tests/reports/monthly_summary_20250930_225857.json new file mode 100644 index 0000000..b19b0ab --- /dev/null +++ b/tests/reports/monthly_summary_20250930_225857.json @@ -0,0 +1,3 @@ +{ + "error": "Нет данных за указанный период" +} \ No newline at end of file diff --git a/tests/reports/monthly_summary_20250930_225902.json b/tests/reports/monthly_summary_20250930_225902.json new file mode 100644 index 0000000..b19b0ab --- /dev/null +++ b/tests/reports/monthly_summary_20250930_225902.json @@ -0,0 +1,3 @@ +{ + "error": "Нет данных за указанный период" +} \ No newline at end of file diff --git a/tests/reports/monthly_summary_20250930_230017.json b/tests/reports/monthly_summary_20250930_230017.json new file mode 100644 index 0000000..b19b0ab --- /dev/null +++ b/tests/reports/monthly_summary_20250930_230017.json @@ -0,0 +1,3 @@ +{ + "error": "Нет данных за указанный период" +} \ No newline at end of file diff --git a/tests/reports/monthly_summary_20250930_230123.json b/tests/reports/monthly_summary_20250930_230123.json new file mode 100644 index 0000000..b19b0ab --- /dev/null +++ b/tests/reports/monthly_summary_20250930_230123.json @@ -0,0 +1,3 @@ +{ + "error": "Нет данных за указанный период" +} \ No newline at end of file diff --git a/tests/reports/monthly_summary_20250930_230203.json b/tests/reports/monthly_summary_20250930_230203.json new file mode 100644 index 0000000..b19b0ab --- /dev/null +++ b/tests/reports/monthly_summary_20250930_230203.json @@ -0,0 +1,3 @@ +{ + "error": "Нет данных за указанный период" +} \ No newline at end of file diff --git a/tests/reports/monthly_summary_20250930_230204.json b/tests/reports/monthly_summary_20250930_230204.json new file mode 100644 index 0000000..b19b0ab --- /dev/null +++ b/tests/reports/monthly_summary_20250930_230204.json @@ -0,0 +1,3 @@ +{ + "error": "Нет данных за указанный период" +} \ No newline at end of file diff --git a/tests/reports/monthly_summary_20250930_230205.json b/tests/reports/monthly_summary_20250930_230205.json new file mode 100644 index 0000000..b19b0ab --- /dev/null +++ b/tests/reports/monthly_summary_20250930_230205.json @@ -0,0 +1,3 @@ +{ + "error": "Нет данных за указанный период" +} \ No newline at end of file diff --git a/tests/reports/spending_by_category_20250930_221851.json b/tests/reports/spending_by_category_20250930_221851.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/tests/reports/spending_by_category_20250930_221851.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/reports/spending_by_category_20250930_222642.json b/tests/reports/spending_by_category_20250930_222642.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/tests/reports/spending_by_category_20250930_222642.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/reports/spending_by_category_20250930_222949.json b/tests/reports/spending_by_category_20250930_222949.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/tests/reports/spending_by_category_20250930_222949.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/reports/spending_by_category_20250930_222959.json b/tests/reports/spending_by_category_20250930_222959.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/tests/reports/spending_by_category_20250930_222959.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/reports/spending_by_category_20250930_223101.json b/tests/reports/spending_by_category_20250930_223101.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/tests/reports/spending_by_category_20250930_223101.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/reports/spending_by_category_20250930_224838.json b/tests/reports/spending_by_category_20250930_224838.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/tests/reports/spending_by_category_20250930_224838.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/reports/spending_by_category_20250930_225857.json b/tests/reports/spending_by_category_20250930_225857.json new file mode 100644 index 0000000..685786d --- /dev/null +++ b/tests/reports/spending_by_category_20250930_225857.json @@ -0,0 +1,14 @@ +[ + { + "Месяц": "2023-10", + "Сумма": 1000 + }, + { + "Месяц": "2023-11", + "Сумма": 2000 + }, + { + "Месяц": "2023-12", + "Сумма": 3000 + } +] \ No newline at end of file diff --git a/tests/reports/spending_by_weekday_20250930_221851.json b/tests/reports/spending_by_weekday_20250930_221851.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/tests/reports/spending_by_weekday_20250930_221851.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/reports/spending_by_weekday_20250930_222642.json b/tests/reports/spending_by_weekday_20250930_222642.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/tests/reports/spending_by_weekday_20250930_222642.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/reports/spending_by_weekday_20250930_222949.json b/tests/reports/spending_by_weekday_20250930_222949.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/tests/reports/spending_by_weekday_20250930_222949.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/reports/spending_by_weekday_20250930_222959.json b/tests/reports/spending_by_weekday_20250930_222959.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/tests/reports/spending_by_weekday_20250930_222959.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/reports/spending_by_weekday_20250930_223101.json b/tests/reports/spending_by_weekday_20250930_223101.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/tests/reports/spending_by_weekday_20250930_223101.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/reports/spending_by_weekday_20250930_224838.json b/tests/reports/spending_by_weekday_20250930_224838.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/tests/reports/spending_by_weekday_20250930_224838.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/reports/spending_by_weekday_20250930_225857.json b/tests/reports/spending_by_weekday_20250930_225857.json new file mode 100644 index 0000000..c0ffc11 --- /dev/null +++ b/tests/reports/spending_by_weekday_20250930_225857.json @@ -0,0 +1,30 @@ +[ + { + "День недели": "Понедельник", + "Средняя сумма": 100.0 + }, + { + "День недели": "Вторник", + "Средняя сумма": 200.0 + }, + { + "День недели": "Среда", + "Средняя сумма": 300.0 + }, + { + "День недели": "Четверг", + "Средняя сумма": 400.0 + }, + { + "День недели": "Пятница", + "Средняя сумма": 500.0 + }, + { + "День недели": "Суббота", + "Средняя сумма": 600.0 + }, + { + "День недели": "Воскресенье", + "Средняя сумма": 700.0 + } +] \ No newline at end of file diff --git a/tests/reports/spending_by_workday_20250930_221851.json b/tests/reports/spending_by_workday_20250930_221851.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/tests/reports/spending_by_workday_20250930_221851.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/reports/spending_by_workday_20250930_222642.json b/tests/reports/spending_by_workday_20250930_222642.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/tests/reports/spending_by_workday_20250930_222642.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/reports/spending_by_workday_20250930_222950.json b/tests/reports/spending_by_workday_20250930_222950.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/tests/reports/spending_by_workday_20250930_222950.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/reports/spending_by_workday_20250930_222959.json b/tests/reports/spending_by_workday_20250930_222959.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/tests/reports/spending_by_workday_20250930_222959.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/reports/spending_by_workday_20250930_223102.json b/tests/reports/spending_by_workday_20250930_223102.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/tests/reports/spending_by_workday_20250930_223102.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/reports/spending_by_workday_20250930_224838.json b/tests/reports/spending_by_workday_20250930_224838.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/tests/reports/spending_by_workday_20250930_224838.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/reports/spending_by_workday_20250930_224839.json b/tests/reports/spending_by_workday_20250930_224839.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/tests/reports/spending_by_workday_20250930_224839.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/reports/spending_by_workday_20250930_225857.json b/tests/reports/spending_by_workday_20250930_225857.json new file mode 100644 index 0000000..ddf1af6 --- /dev/null +++ b/tests/reports/spending_by_workday_20250930_225857.json @@ -0,0 +1,10 @@ +[ + { + "Тип дня": "Выходной", + "Средняя сумма": 450.0 + }, + { + "Тип дня": "Рабочий", + "Средняя сумма": 200.0 + } +] \ No newline at end of file diff --git a/tests/reports/test_function_20231220_153000.json b/tests/reports/test_function_20231220_153000.json new file mode 100644 index 0000000..d6faa34 --- /dev/null +++ b/tests/reports/test_function_20231220_153000.json @@ -0,0 +1,10 @@ +[ + { + "col1": 1, + "col2": 3 + }, + { + "col1": 2, + "col2": 4 + } +] \ No newline at end of file diff --git a/tests/test_main.py b/tests/test_main.py new file mode 100644 index 0000000..abbbc44 --- /dev/null +++ b/tests/test_main.py @@ -0,0 +1,199 @@ +import pytest +from unittest.mock import patch, MagicMock +from src.main import TransactionAnalyzer, main + + +class TestTransactionAnalyzer: + """Тесты для основного класса приложения""" + + @pytest.fixture + def analyzer(self): + return TransactionAnalyzer() + + def test_initialization(self, analyzer): + """Тест инициализации анализатора""" + assert analyzer.data_file is not None + assert analyzer.transactions_df is None + assert analyzer.settings is None + + @patch('src.views.main_page') + def test_generate_main_page(self, mock_main_page, analyzer): + """Тест генерации главной страницы""" + # Мокируем данные + analyzer.transactions_df = MagicMock() + analyzer.settings = MagicMock() + expected_result = {'greeting': 'Добрый день'} + mock_main_page.return_value = expected_result + + # Генерируем страницу + result = analyzer.generate_main_page('2023-12-20 15:30:00') + + # Проверяем + assert result == expected_result + mock_main_page.assert_called_once_with('2023-12-20 15:30:00', 'data/operations.xlsx') + + @patch('src.views.events_page') + def test_generate_events_page(self, mock_events_page, analyzer): + """Тест генерации страницы событий""" + # Мокируем данные + analyzer.transactions_df = MagicMock() + analyzer.settings = MagicMock() + expected_result = {'expenses': {'total_amount': 1000}} + mock_events_page.return_value = expected_result + + # Генерируем страницу + result = analyzer.generate_events_page('2023-12-20', 'M') + + # Проверяем + assert result == expected_result + mock_events_page.assert_called_once_with('2023-12-20', 'M', 'data/operations.xlsx') + + @patch('src.services.profitable_cashback_categories') + def test_analyze_cashback_categories(self, mock_cashback, analyzer): + """Тест анализа кешбэка""" + # Мокируем данные + analyzer.transactions_df = MagicMock() + analyzer.settings = {'cashback_rules': {'default': 0.01}} + expected_result = {'Супермаркеты': 150.0} + mock_cashback.return_value = expected_result + + # Анализируем + result = analyzer.analyze_cashback_categories(2023, 12) + + # Проверяем + assert result == expected_result + mock_cashback.assert_called_once() + + @patch('src.services.investment_bank') + def test_calculate_investment(self, mock_investment, analyzer): + """Тест расчета инвесткопилки""" + # Мокируем данные + mock_df = MagicMock() + mock_df.to_dict.return_value = [{'Дата операции': '2023-12-01', 'Сумма операции': 1000}] + analyzer.transactions_df = mock_df + mock_investment.return_value = 50.0 + + # Рассчитываем + result = analyzer.calculate_investment('2023-12', 50) + + # Проверяем + assert result == 50.0 + mock_investment.assert_called_once() + + @patch('src.services.simple_search') + def test_search_transactions(self, mock_search, analyzer): + """Тест поиска транзакций""" + # Мокируем данные + mock_df = MagicMock() + mock_df.to_dict.return_value = [{'Описание': 'Магазин'}] + analyzer.transactions_df = mock_df + mock_search.return_value = [{'Описание': 'Магазин'}] + + # Ищем + result = analyzer.search_transactions('магазин') + + # Проверяем + assert len(result) == 1 + mock_search.assert_called_once() + + @patch('src.reports.ReportGenerator') + def test_generate_reports(self, mock_report_generator, analyzer): + """Тест генерации отчетов""" + # Мокируем данные + analyzer.transactions_df = MagicMock() + mock_report_generator.spending_by_category.return_value = MagicMock() + mock_report_generator.spending_by_weekday.return_value = MagicMock() + mock_report_generator.spending_by_workday.return_value = MagicMock() + mock_report_generator.monthly_summary.return_value = {'total': 1000} + + # Генерируем отчеты + reports = analyzer.generate_reports() + + # Проверяем + assert 'spending_by_category' in reports + assert 'monthly_summary' in reports + assert mock_report_generator.spending_by_category.call_count == 1 + + +class TestMainFunction: + """Тесты для основной функции""" + + @patch('src.main.TransactionAnalyzer') + @patch('builtins.print') + def test_main_web_command(self, mock_print, mock_analyzer): + """Тест main с командой web""" + # Мокируем анализатор + mock_instance = MagicMock() + mock_instance.load_data.return_value = None + mock_instance.generate_main_page.return_value = {'greeting': 'Добрый день'} + mock_instance.generate_events_page.return_value = {'expenses': {'total_amount': 1000}} + mock_analyzer.return_value = mock_instance + + # Запускаем с командой web + with patch('sys.argv', ['main.py', '--command', 'web']): + main() + + # Проверяем вызовы + mock_instance.load_data.assert_called_once() + mock_instance.generate_main_page.assert_called_once() + mock_instance.generate_events_page.assert_called_once() + + @patch('src.main.TransactionAnalyzer') + @patch('builtins.print') + def test_main_report_command(self, mock_print, mock_analyzer): + """Тест main с командой report""" + # Мокируем анализатор + mock_instance = MagicMock() + mock_instance.load_data.return_value = None + mock_instance.generate_reports.return_value = { + 'monthly_summary': {'total': 1000} + } + mock_analyzer.return_value = mock_instance + + # Запускаем с командой report + with patch('sys.argv', ['main.py', '--command', 'report']): + main() + + # Проверяем вызовы + mock_instance.load_data.assert_called_once() + mock_instance.generate_reports.assert_called_once() + + @patch('src.main.TransactionAnalyzer') + @patch('builtins.print') + def test_main_analyze_command(self, mock_print, mock_analyzer): + """Тест main с командой analyze""" + # Мокируем анализатор + mock_instance = MagicMock() + mock_instance.load_data.return_value = None + mock_instance.analyze_cashback_categories.return_value = {'Категория': 100} + mock_instance.calculate_investment.return_value = 50.0 + mock_analyzer.return_value = mock_instance + + # Запускаем с командой analyze + with patch('sys.argv', ['main.py', '--command', 'analyze']): + main() + + # Проверяем вызовы + mock_instance.load_data.assert_called_once() + mock_instance.analyze_cashback_categories.assert_called_once() + mock_instance.calculate_investment.assert_called_once() + + @patch('src.main.TransactionAnalyzer') + @patch('builtins.print') + def test_main_test_command(self, mock_print, mock_analyzer): + """Тест main с командой test""" + # Мокируем анализатор + mock_instance = MagicMock() + mock_instance.load_data.return_value = None + mock_instance.transactions_df = MagicMock() + mock_instance.settings = {'user_currencies': ['USD']} + mock_instance.search_transactions.return_value = [{'Описание': 'Магазин'}] + mock_analyzer.return_value = mock_instance + + # Запускаем с командой test + with patch('sys.argv', ['main.py', '--command', 'test']): + main() + + # Проверяем вызовы + mock_instance.load_data.assert_called_once() + mock_instance.search_transactions.assert_called_once() \ No newline at end of file diff --git a/tests/test_reports.py b/tests/test_reports.py new file mode 100644 index 0000000..808f79f --- /dev/null +++ b/tests/test_reports.py @@ -0,0 +1,249 @@ +import pytest +import pandas as pd +import numpy as np +from datetime import datetime, timedelta +import tempfile +import json +import os +from unittest.mock import patch, MagicMock + +from src.reports import ( + ReportGenerator, spending_by_category, spending_by_weekday, + spending_by_workday, report_decorator +) + + +class TestReportGeneratorExtended: + """Расширенные тесты для ReportGenerator""" + + @pytest.fixture + def sample_transactions_complex(self): + """Фикстура с комплексными данными для отчетов""" + dates = pd.date_range(start='2023-09-01', end='2023-12-31', freq='D') + return pd.DataFrame({ + 'Дата операции': dates, + 'Статус': ['OK'] * len(dates), + 'Категория': ['Супермаркеты'] * 30 + ['Фастфуд'] * 30 + ['Транспорт'] * 30 + ['Развлечения'] * 31, + 'Сумма операции': np.random.uniform(100, 5000, len(dates)) + }) + + def test_spending_by_category_empty_data(self): + """Тест отчета по категориям с пустыми данными""" + df = pd.DataFrame(columns=['Дата операции', 'Категория', 'Сумма операции', 'Статус']) + + result = ReportGenerator.spending_by_category(df, 'Супермаркеты', '2023-12-20') + + assert result.empty + assert list(result.columns) == ['Месяц', 'Сумма'] + + def test_spending_by_category_no_matching_category(self): + """Тест отчета по категориям без совпадающих категорий""" + df = pd.DataFrame({ + 'Дата операции': pd.to_datetime(['2023-12-01', '2023-12-02']), + 'Статус': ['OK', 'OK'], + 'Категория': ['Фастфуд', 'Транспорт'], # Нет Супермаркетов + 'Сумма операции': [1000, 2000] + }) + + result = ReportGenerator.spending_by_category(df, 'Супермаркеты', '2023-12-20') + + assert result.empty + + def test_spending_by_category_only_failed_transactions(self): + """Тест отчета по категориям только с неудачными транзакциями""" + df = pd.DataFrame({ + 'Дата операции': pd.to_datetime(['2023-12-01', '2023-12-02']), + 'Статус': ['FAILED', 'FAILED'], + 'Категория': ['Супермаркеты', 'Супермаркеты'], + 'Сумма операции': [1000, 2000] + }) + + result = ReportGenerator.spending_by_category(df, 'Супермаркеты', '2023-12-20') + + assert result.empty + + def test_spending_by_category_only_income(self): + """Тест отчета по категориям только с доходами""" + df = pd.DataFrame({ + 'Дата операции': pd.to_datetime(['2023-12-01', '2023-12-02']), + 'Статус': ['OK', 'OK'], + 'Категория': ['Супермаркеты', 'Супермаркеты'], + 'Сумма операции': [-1000, -2000] # Доходы + }) + + result = ReportGenerator.spending_by_category(df, 'Супермаркеты', '2023-12-20') + + assert result.empty + + def test_spending_by_category_different_months(self): + """Тест отчета по категориям за несколько месяцев""" + df = pd.DataFrame({ + 'Дата операции': pd.to_datetime(['2023-10-15', '2023-11-15', '2023-12-15']), + 'Статус': ['OK', 'OK', 'OK'], + 'Категория': ['Супермаркеты', 'Супермаркеты', 'Супермаркеты'], + 'Сумма операции': [1000, 2000, 3000] + }) + + result = ReportGenerator.spending_by_category(df, 'Супермаркеты', '2023-12-20') + + # Должны быть данные за 3 месяца (октябрь, ноябрь, декабрь) + assert len(result) == 3 + assert set(result['Месяц']) == {'2023-10', '2023-11', '2023-12'} + assert result['Сумма'].sum() == 6000 + + def test_spending_by_weekday_empty_data(self): + """Тест отчета по дням недели с пустыми данными""" + df = pd.DataFrame(columns=['Дата операции', 'Сумма операции', 'Статус']) + + result = ReportGenerator.spending_by_weekday(df, '2023-12-20') + + assert result.empty + assert list(result.columns) == ['День недели', 'Средняя сумма'] + + def test_spending_by_weekday_correct_ordering(self): + """Тест правильного порядка дней недели в отчете""" + df = pd.DataFrame({ + 'Дата операции': pd.to_datetime([ + '2023-12-18', '2023-12-19', '2023-12-20', # Пн, Вт, Ср + '2023-12-21', '2023-12-22', '2023-12-23', '2023-12-24' # Чт, Пт, Сб, Вс + ]), + 'Статус': ['OK'] * 7, + 'Сумма операции': [100, 200, 300, 400, 500, 600, 700] + }) + + result = ReportGenerator.spending_by_weekday(df, '2023-12-24') + + # Проверяем порядок дней недели + expected_order = ['Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота', 'Воскресенье'] + assert list(result['День недели']) == expected_order + + # Проверяем средние значения (в данном случае они равны суммам, так по одной транзакции на день) + assert list(result['Средняя сумма']) == [100, 200, 300, 400, 500, 600, 700] + + def test_spending_by_workday_empty_data(self): + """Тест отчета по рабочим/выходным с пустыми данными""" + df = pd.DataFrame(columns=['Дата операции', 'Сумма операции', 'Статус']) + + result = ReportGenerator.spending_by_workday(df, '2023-12-20') + + assert result.empty + assert list(result.columns) == ['Тип дня', 'Средняя сумма'] + + def test_spending_by_workday_correct_classification(self): + """Тест правильной классификации рабочих и выходных дней""" + df = pd.DataFrame({ + 'Дата операции': pd.to_datetime([ + '2023-12-18', '2023-12-19', '2023-12-20', # Пн, Вт, Ср - рабочие + '2023-12-23', '2023-12-24' # Сб, Вс - выходные + ]), + 'Статус': ['OK'] * 5, + 'Сумма операции': [100, 200, 300, 400, 500] + }) + + result = ReportGenerator.spending_by_workday(df, '2023-12-24') + + assert len(result) == 2 + + workday_data = result[result['Тип дня'] == 'Рабочий'] + weekend_data = result[result['Тип дня'] == 'Выходной'] + + # Средняя за рабочие дни: (100 + 200 + 300) / 3 = 200 + assert workday_data['Средняя сумма'].iloc[0] == 200.0 + # Средняя за выходные: (400 + 500) / 2 = 450 + assert weekend_data['Средняя сумма'].iloc[0] == 450.0 + + def test_monthly_summary_empty_data(self): + """Тест сводного отчета с пустыми данными""" + df = pd.DataFrame(columns=['Дата операции', 'Сумма операции', 'Статус', 'Категория']) + + result = ReportGenerator.monthly_summary(df, 6) + + assert 'error' in result + assert result['error'] == "Нет данных за указанный период" + + +class TestReportDecorator: + """Тесты декоратора отчетов""" + + def test_report_decorator_with_filename(self, tmp_path): + """Тест декоратора с указанием имени файла""" + test_filename = tmp_path / "test_report.json" + + @report_decorator(filename=str(test_filename)) + def test_function(): + return {"test": "data"} + + result = test_function() + + assert result == {"test": "data"} + assert test_filename.exists() + + with open(test_filename, 'r') as f: + saved_data = json.load(f) + assert saved_data == {"test": "data"} + + def test_report_decorator_with_dataframe(self, tmp_path): + """Тест декоратора с DataFrame""" + + @report_decorator() + def test_function(): + return pd.DataFrame({'col1': [1, 2], 'col2': [3, 4]}) + + with patch('src.reports.datetime') as mock_datetime: + mock_datetime.now.return_value = datetime(2023, 12, 20, 15, 30, 0) + + result = test_function() + + # Проверяем, что функция возвращает правильный результат + assert isinstance(result, pd.DataFrame) + assert len(result) == 2 + + def test_report_decorator_exception(self, tmp_path): + """Тест декоратора при исключении в функции""" + + @report_decorator(filename=str(tmp_path / "error_report.json")) + def failing_function(): + raise ValueError("Test error") + + with pytest.raises(ValueError, match="Test error"): + failing_function() + + # Файл не должен быть создан при исключении + assert not (tmp_path / "error_report.json").exists() + + +class TestReportsFunctions: + """Тесты функций-оберток отчетов""" + + def test_spending_by_category_wrapper(self): + """Тест обертки spending_by_category""" + with patch('src.reports.ReportGenerator.spending_by_category') as mock_method: + mock_method.return_value = pd.DataFrame({'test': [1, 2, 3]}) + + df = pd.DataFrame() + result = spending_by_category(df, 'Супермаркеты', '2023-12-20') + + assert isinstance(result, pd.DataFrame) + mock_method.assert_called_once_with(df, 'Супермаркеты', '2023-12-20') + + def test_spending_by_weekday_wrapper(self): + """Тест обертки spending_by_weekday""" + with patch('src.reports.ReportGenerator.spending_by_weekday') as mock_method: + mock_method.return_value = pd.DataFrame({'test': [1, 2, 3]}) + + df = pd.DataFrame() + result = spending_by_weekday(df, '2023-12-20') + + assert isinstance(result, pd.DataFrame) + mock_method.assert_called_once_with(df, '2023-12-20') + + def test_spending_by_workday_wrapper(self): + """Тест обертки spending_by_workday""" + with patch('src.reports.ReportGenerator.spending_by_workday') as mock_method: + mock_method.return_value = pd.DataFrame({'test': [1, 2, 3]}) + + df = pd.DataFrame() + result = spending_by_workday(df, '2023-12-20') + + assert isinstance(result, pd.DataFrame) + mock_method.assert_called_once_with(df, '2023-12-20') \ No newline at end of file diff --git a/tests/test_services.py b/tests/test_services.py index 8db7a35..8d64ad7 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -34,3 +34,247 @@ def test_simple_search(sample_transactions_list): results = simple_search(sample_transactions_list, "кафе") assert len(results) == 1 assert results[0]['Категория'] == 'Кафе' + + +import pytest +from unittest.mock import patch +from src.services import ( + profitable_cashback_categories, + investment_bank, + simple_search, + search_by_phone, + search_by_person_transfers, + CashbackAnalyzer, + InvestmentCalculator, + TransactionSearcher, + compose, + pipe, + log_service_call +) +import pandas as pd +from datetime import datetime + + +class TestCashbackAnalyzer: + """Тесты для анализатора кешбэка""" + + def test_analyze_profitable_categories(self): + """Тест анализа выгодных категорий""" + analyzer = CashbackAnalyzer({'Супермаркеты': 0.05, 'default': 0.01}) + + # Создаем тестовые данные + data = pd.DataFrame({ + 'Дата операции': pd.to_datetime(['2023-12-01', '2023-12-05']), + 'Статус': ['OK', 'OK'], + 'Сумма операции': [1000.0, 2000.0], + 'Категория': ['Супермаркеты', 'Фастфуд'] + }) + + result = analyzer.analyze_profitable_categories(data, 2023, 12) + + assert 'Супермаркеты' in result + assert 'Фастфуд' in result + assert result['Супермаркеты'] == 50.0 # 5% от 1000 + assert result['Фастфуд'] == 20.0 # 1% от 2000 + + def test_analyze_profitable_categories_no_data(self): + """Тест анализа без данных""" + analyzer = CashbackAnalyzer({'default': 0.01}) + + # Пустые данные + data = pd.DataFrame(columns=['Дата операции', 'Статус', 'Сумма операции', 'Категория']) + + result = analyzer.analyze_profitable_categories(data, 2023, 12) + + assert result == {} + + def test_analyze_profitable_categories_with_empty_category(self): + """Тест анализа с пустыми категориями""" + analyzer = CashbackAnalyzer({'default': 0.01}) + + data = pd.DataFrame({ + 'Дата операции': pd.to_datetime(['2023-12-01']), + 'Статус': ['OK'], + 'Сумма операции': [1000.0], + 'Категория': [''] + }) + + result = analyzer.analyze_profitable_categories(data, 2023, 12) + + # Пустые категории должны игнорироваться + assert '' not in result + + +class TestInvestmentCalculator: + """Тесты для калькулятора инвесткопилки""" + + def test_investment_bank_valid_transactions(self): + """Тест расчета с валидными транзакциями""" + transactions = [ + { + 'Дата операции': '2023-12-01', + 'Сумма операции': '1047.0' # Округление до 1050 = +3 + }, + { + 'Дата операции': '2023-12-15', + 'Сумма операции': '1982.0' # Округление до 2000 = +18 + } + ] + + result = InvestmentCalculator.investment_bank('2023-12', transactions, 50) + + assert result == 21.0 # 3 + 18 + + def test_investment_bank_invalid_transaction(self): + """Тест с невалидной транзакцией""" + transactions = [ + {'Дата операции': 'invalid-date', 'Сумма операции': 'not-a-number'}, + {'Дата операции': '2023-12-01', 'Сумма операции': '1000.0'} + ] + + result = InvestmentCalculator.investment_bank('2023-12', transactions, 50) + + # Только вторая транзакция должна учитываться + assert result == 0.0 # 1000 округляется до 1000 = 0 + + def test_validate_transaction_valid(self): + """Тест валидации корректной транзакции""" + transaction = {'Дата операции': '2023-12-01', 'Сумма операции': '1000.0'} + + assert InvestmentCalculator._validate_transaction(transaction) is True + + def test_validate_transaction_missing_fields(self): + """Тест валидации транзакции с отсутствующими полями""" + transaction = {'Дата операции': '2023-12-01'} # Нет Сумма операции + + assert InvestmentCalculator._validate_transaction(transaction) is False + + def test_validate_transaction_invalid_data(self): + """Тест валидации транзакции с невалидными данными""" + transaction = {'Дата операции': 'invalid-date', 'Сумма операции': 'not-a-number'} + + assert InvestmentCalculator._validate_transaction(transaction) is False + + +class TestTransactionSearcher: + """Тесты для поисковика транзакций""" + + @pytest.fixture + def sample_transactions(self): + return [ + {'Описание': 'Покупка в магазине', 'Категория': 'Супермаркеты'}, + {'Описание': 'Обед в кафе', 'Категория': 'Фастфуд'}, + {'Описание': 'Перевод Ивану И.', 'Категория': 'Переводы'}, + {'Описание': 'Пополнение +7 921 123-45-67', 'Категория': 'Мобильная связь'} + ] + + def test_simple_search_by_description(self, sample_transactions): + """Тест поиска по описанию""" + results = TransactionSearcher.simple_search(sample_transactions, 'магазин') + + assert len(results) == 1 + assert results[0]['Описание'] == 'Покупка в магазине' + + def test_simple_search_by_category(self, sample_transactions): + """Тест поиска по категории""" + results = TransactionSearcher.simple_search(sample_transactions, 'Фастфуд') + + assert len(results) == 1 + assert results[0]['Категория'] == 'Фастфуд' + + def test_simple_search_short_string(self): + """Тест поиска с короткой строкой""" + with pytest.raises(ValueError, match="Строка поиска должна содержать минимум 2 символа"): + TransactionSearcher.simple_search([], 'а') + + def test_search_by_phone(self, sample_transactions): + """Тест поиска по телефонным номерам""" + results = TransactionSearcher.search_by_phone(sample_transactions) + + assert len(results) == 1 + assert '+7 921 123-45-67' in results[0]['Описание'] + + def test_search_by_person_transfers(self, sample_transactions): + """Тест поиска переводов физлицам""" + results = TransactionSearcher.search_by_person_transfers(sample_transactions) + + assert len(results) == 1 + assert results[0]['Категория'] == 'Переводы' + assert 'Ивану И.' in results[0]['Описание'] + + +class TestFunctionalUtilities: + """Тесты функциональных утилит""" + + def test_compose(self): + """Тест композиции функций""" + + def double(x): + return x * 2 + + def square(x): + return x * x + + composed = compose(double, square) + result = composed(3) # square(3) = 9, double(9) = 18 + + assert result == 18 + +class TestServiceDecorators: + """Тесты декораторов сервисов""" + + def test_log_service_call_success(self): + """Тест декоратора при успешном выполнении""" + + @log_service_call("test_service") + def test_function(): + return "success" + + with patch('src.services.logger') as mock_logger: + result = test_function() + + assert result == "success" + mock_logger.info.assert_any_call("Вызов сервиса test_service") + mock_logger.info.assert_any_call("Сервис test_service выполнен успешно") + + def test_log_service_call_exception(self): + """Тест декоратора при исключении""" + + @log_service_call("test_service") + def test_function(): + raise ValueError("Test error") + + with patch('src.services.logger') as mock_logger: + with pytest.raises(ValueError, match="Test error"): + test_function() + + mock_logger.info.assert_called_once_with("Вызов сервиса test_service") + mock_logger.error.assert_called_once_with("Ошибка в сервисе test_service: Test error") + + +# Тесты для основных функций с декораторами +def test_profitable_cashback_categories_integration(): + """Интеграционный тест для profitable_cashback_categories""" + data = pd.DataFrame({ + 'Дата операции': pd.to_datetime(['2023-12-01']), + 'Статус': ['OK'], + 'Сумма операции': [1000.0], + 'Категория': ['Супермаркеты'] + }) + + with patch('src.services.logger') as mock_logger: + result = profitable_cashback_categories(data, 2023, 12, {'Супермаркеты': 0.05}) + + assert 'Супермаркеты' in result + mock_logger.info.assert_called() + + +def test_investment_bank_integration(): + """Интеграционный тест для investment_bank""" + transactions = [{'Дата операции': '2023-12-01', 'Сумма операции': '1047.0'}] + + with patch('src.services.logger') as mock_logger: + result = investment_bank('2023-12', transactions, 50) + + assert result == 3.0 + mock_logger.info.assert_called() \ No newline at end of file From 5bee02a669b448d24bb7535095f2bcc206b0e170 Mon Sep 17 00:00:00 2001 From: ilya_kim Date: Tue, 30 Sep 2025 23:38:45 +0300 Subject: [PATCH 12/12] appdate tests and new tests --- tests/test_main.py | 6 ++++-- tests/test_reports.py | 22 ++++++++++++---------- tests/test_services.py | 36 +++++++++++++++--------------------- 3 files changed, 31 insertions(+), 33 deletions(-) diff --git a/tests/test_main.py b/tests/test_main.py index abbbc44..e4eebaf 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,5 +1,7 @@ +from unittest.mock import MagicMock, patch + import pytest -from unittest.mock import patch, MagicMock + from src.main import TransactionAnalyzer, main @@ -196,4 +198,4 @@ def test_main_test_command(self, mock_print, mock_analyzer): # Проверяем вызовы mock_instance.load_data.assert_called_once() - mock_instance.search_transactions.assert_called_once() \ No newline at end of file + mock_instance.search_transactions.assert_called_once() diff --git a/tests/test_reports.py b/tests/test_reports.py index 808f79f..f89bee8 100644 --- a/tests/test_reports.py +++ b/tests/test_reports.py @@ -1,15 +1,17 @@ -import pytest -import pandas as pd -import numpy as np -from datetime import datetime, timedelta -import tempfile import json -import os -from unittest.mock import patch, MagicMock +from datetime import datetime +from unittest.mock import patch + +import numpy as np +import pandas as pd +import pytest from src.reports import ( - ReportGenerator, spending_by_category, spending_by_weekday, - spending_by_workday, report_decorator + ReportGenerator, + report_decorator, + spending_by_category, + spending_by_weekday, + spending_by_workday, ) @@ -246,4 +248,4 @@ def test_spending_by_workday_wrapper(self): result = spending_by_workday(df, '2023-12-20') assert isinstance(result, pd.DataFrame) - mock_method.assert_called_once_with(df, '2023-12-20') \ No newline at end of file + mock_method.assert_called_once_with(df, '2023-12-20') diff --git a/tests/test_services.py b/tests/test_services.py index 8d64ad7..69ab348 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -1,6 +1,18 @@ +from unittest.mock import patch + +import pandas as pd import pytest -from src.services import investment_bank, simple_search +from src.services import ( + CashbackAnalyzer, + InvestmentCalculator, + TransactionSearcher, + compose, + investment_bank, + log_service_call, + profitable_cashback_categories, + simple_search, +) @pytest.fixture @@ -36,25 +48,6 @@ def test_simple_search(sample_transactions_list): assert results[0]['Категория'] == 'Кафе' -import pytest -from unittest.mock import patch -from src.services import ( - profitable_cashback_categories, - investment_bank, - simple_search, - search_by_phone, - search_by_person_transfers, - CashbackAnalyzer, - InvestmentCalculator, - TransactionSearcher, - compose, - pipe, - log_service_call -) -import pandas as pd -from datetime import datetime - - class TestCashbackAnalyzer: """Тесты для анализатора кешбэка""" @@ -220,6 +213,7 @@ def square(x): assert result == 18 + class TestServiceDecorators: """Тесты декораторов сервисов""" @@ -277,4 +271,4 @@ def test_investment_bank_integration(): result = investment_bank('2023-12', transactions, 50) assert result == 3.0 - mock_logger.info.assert_called() \ No newline at end of file + mock_logger.info.assert_called()